Add OpenAPI spec and SDK authoring guide with TypeScript example

- Add machine-readable OpenAPI spec for Storage, Database, PubSub -
Document HTTP endpoints, auth, migrations, and SDK patterns - Provide
minimal TypeScript SDK example and code - Update README and add example
SDK files and configs - Bump version to 0.40.0-beta
This commit is contained in:
anonpenguin 2025-08-23 12:10:54 +03:00
parent 829991da03
commit 3fe78ee62a
8 changed files with 547 additions and 1 deletions

View File

@ -259,6 +259,70 @@ Notes:
- The gateway uses internal DB context for validation and execution to avoid circular auth checks.
- Perform migrations by POSTing DDL statements to `/v1/db/transaction`.
OpenAPI specification: see `openapi/gateway.yaml` for a machine-readable contract covering Storage, Database, and PubSub REST endpoints.
---
## SDK Authoring Guide
### Base concepts
- Auth: send `X-API-Key: <key>` or `Authorization: Bearer <key|JWT>` on every request.
- Versioning: all endpoints live under `/v1/`.
- Response envelopes today are pragmatic:
- Success varies by endpoint (e.g., `{status:"ok"}` for mutations, JSON bodies for queries/lists)
- Errors are returned as `{ "error": "message" }` with appropriate HTTP status
### HTTP Endpoints (recap)
- Storage
- PUT: `POST /v1/storage/put?key=<k>` body: raw bytes
- GET: `GET /v1/storage/get?key=<k>` (returns bytes; some backends may base64-encode)
- EXISTS: `GET /v1/storage/exists?key=<k>``{exists:boolean}`
- LIST: `GET /v1/storage/list?prefix=<p>``{keys:[...]}`
- DELETE: `POST /v1/storage/delete` body `{key}``{status:"ok"}`
- Database
- Create Table: `POST /v1/db/create-table` `{schema}``{status:"ok"}`
- Drop Table: `POST /v1/db/drop-table` `{table}``{status:"ok"}`
- Query: `POST /v1/db/query` `{sql, args?}``{columns, rows, count}`
- Transaction: `POST /v1/db/transaction` `{statements:[...]}``{status:"ok"}`
- Schema: `GET /v1/db/schema` → schema JSON
- PubSub
- WebSocket: `GET /v1/pubsub/ws?topic=<topic>` (binary frames; ping keepalive)
- Publish: `POST /v1/pubsub/publish` `{topic, data_base64}``{status:"ok"}`
- Topics: `GET /v1/pubsub/topics``{topics:[...]}` (already trimmed to namespace)
### Migrations
- Add a column: `ALTER TABLE users ADD COLUMN age INTEGER`
- Change type / add FK (recreate pattern):
1. `CREATE TABLE users_new (... updated schema ...)`
2. `INSERT INTO users_new (...) SELECT ... FROM users`
3. `DROP TABLE users`
4. `ALTER TABLE users_new RENAME TO users`
- Wrap the above in a single `POST /v1/db/transaction` call.
### Suggested SDK surface
- Database
- `createTable(schema: string): Promise<void>`
- `dropTable(name: string): Promise<void>`
- `query<T>(sql: string, args?: any[]): Promise<{ rows: T[] }>`
- `transaction(statements: string[]): Promise<void>`
- `schema(): Promise<any>`
- Storage
- `put(key: string, value: Uint8Array | string): Promise<void>`
- `get(key: string): Promise<Uint8Array>`
- `exists(key: string): Promise<boolean>`
- `list(prefix?: string): Promise<string[]>`
- `delete(key: string): Promise<void>`
- PubSub
- `subscribe(topic: string, handler: (msg: Uint8Array) => void): Promise<() => void>`
- `publish(topic: string, data: Uint8Array | string): Promise<void>`
- `listTopics(): Promise<string[]>`
### Reliability guidelines
- Timeouts: 1030s defaults with per-call overrides
- Retries: exponential backoff on 429/5xx; respect `Retry-After`
- Idempotency: for transactions, consider an `Idempotency-Key` header client-side (gateway may ignore)
- WebSocket: auto-reconnect with jitter; re-subscribe after reconnect
### Authentication Improvements
The gateway authentication system has been significantly enhanced with the following features:

View File

@ -21,7 +21,7 @@ test-e2e:
.PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports
VERSION := 0.39.0-beta
VERSION := 0.40.0-beta
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'

View File

@ -478,6 +478,75 @@ POST /v1/pubsub/publish # Publish message to topic
GET /v1/pubsub/topics # List active topics
```
---
## SDK Authoring Guide
### Base concepts
- OpenAPI: a machine-readable spec is available at `openapi/gateway.yaml` for SDK code generation.
- **Auth**: send `X-API-Key: <key>` or `Authorization: Bearer <key|JWT>` with every request.
- **Versioning**: all endpoints are under `/v1/`.
- **Responses**: mutations return `{status:"ok"}`; queries/lists return JSON; errors return `{ "error": "message" }` with proper HTTP status.
### Key HTTP endpoints for SDKs
- **Storage**
- PUT: `POST /v1/storage/put?key=<k>` body is raw bytes
- GET: `GET /v1/storage/get?key=<k>` returns bytes (may be base64-encoded by some backends)
- EXISTS: `GET /v1/storage/exists?key=<k>``{exists}`
- LIST: `GET /v1/storage/list?prefix=<p>``{keys}`
- DELETE: `POST /v1/storage/delete` `{key}``{status:"ok"}`
- **Database**
- Create Table: `POST /v1/db/create-table` `{schema}``{status:"ok"}`
- Drop Table: `POST /v1/db/drop-table` `{table}``{status:"ok"}`
- Query: `POST /v1/db/query` `{sql, args?}``{columns, rows, count}`
- Transaction: `POST /v1/db/transaction` `{statements:[...]}``{status:"ok"}`
- Schema: `GET /v1/db/schema` → schema JSON
- **PubSub**
- WS Subscribe: `GET /v1/pubsub/ws?topic=<topic>`
- Publish: `POST /v1/pubsub/publish` `{topic, data_base64}``{status:"ok"}`
- Topics: `GET /v1/pubsub/topics``{topics:[...]}`
### Migrations
- Add column: `ALTER TABLE users ADD COLUMN age INTEGER`
- Change type / add FK (recreate pattern): create `_new` table, copy data, drop old, rename.
- Always send as one `POST /v1/db/transaction`.
### Minimal examples
TypeScript (Node)
```ts
import { GatewayClient } from "../examples/sdk-typescript/src/client";
const client = new GatewayClient(process.env.GATEWAY_BASE_URL!, process.env.GATEWAY_API_KEY!);
await client.createTable("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
const res = await client.query("SELECT name FROM users WHERE id = ?", [1]);
```
Python
```python
import os, requests
BASE = os.environ['GATEWAY_BASE_URL']
KEY = os.environ['GATEWAY_API_KEY']
H = { 'X-API-Key': KEY, 'Content-Type': 'application/json' }
def query(sql, args=None):
r = requests.post(f'{BASE}/v1/db/query', json={ 'sql': sql, 'args': args or [] }, headers=H, timeout=15)
r.raise_for_status()
return r.json()['rows']
```
Go
```go
req, _ := http.NewRequest(http.MethodPost, base+"/v1/db/create-table", bytes.NewBufferString(`{"schema":"CREATE TABLE ..."}`))
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
```
### Security Features
- **Namespace Enforcement:** All operations are automatically prefixed with namespace for isolation

View File

@ -0,0 +1,23 @@
# DeBros Gateway TypeScript SDK (Minimal Example)
Minimal, dependency-light wrapper around the HTTP Gateway.
Usage:
```bash
npm i
export GATEWAY_BASE_URL=http://127.0.0.1:8080
export GATEWAY_API_KEY=your_api_key
```
```ts
import { GatewayClient } from './src/client';
const c = new GatewayClient(process.env.GATEWAY_BASE_URL!, process.env.GATEWAY_API_KEY!);
await c.createTable('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
await c.transaction([
'INSERT INTO users (id,name) VALUES (1,\'Alice\')'
]);
const res = await c.query('SELECT name FROM users WHERE id = ?', [1]);
console.log(res.rows);
```

View File

@ -0,0 +1,17 @@
{
"name": "debros-gateway-sdk",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"isomorphic-ws": "^5.0.0"
},
"devDependencies": {
"typescript": "^5.5.4"
}
}

View File

@ -0,0 +1,112 @@
import WebSocket from 'isomorphic-ws';
export class GatewayClient {
constructor(private baseUrl: string, private apiKey: string, private http = fetch) {}
private headers(json = true): Record<string, string> {
const h: Record<string, string> = { 'X-API-Key': this.apiKey };
if (json) h['Content-Type'] = 'application/json';
return h;
}
// Database
async createTable(schema: string): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/db/create-table`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ schema })
});
if (!r.ok) throw new Error(`createTable failed: ${r.status}`);
}
async dropTable(table: string): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/db/drop-table`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ table })
});
if (!r.ok) throw new Error(`dropTable failed: ${r.status}`);
}
async query<T = any>(sql: string, args: any[] = []): Promise<{ rows: T[] }> {
const r = await this.http(`${this.baseUrl}/v1/db/query`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ sql, args })
});
if (!r.ok) throw new Error(`query failed: ${r.status}`);
return r.json();
}
async transaction(statements: string[]): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/db/transaction`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ statements })
});
if (!r.ok) throw new Error(`transaction failed: ${r.status}`);
}
async schema(): Promise<any> {
const r = await this.http(`${this.baseUrl}/v1/db/schema`, { headers: this.headers(false) });
if (!r.ok) throw new Error(`schema failed: ${r.status}`);
return r.json();
}
// Storage
async put(key: string, value: Uint8Array | string): Promise<void> {
const body = typeof value === 'string' ? new TextEncoder().encode(value) : value;
const r = await this.http(`${this.baseUrl}/v1/storage/put?key=${encodeURIComponent(key)}`, {
method: 'POST', headers: { 'X-API-Key': this.apiKey }, body
});
if (!r.ok) throw new Error(`put failed: ${r.status}`);
}
async get(key: string): Promise<Uint8Array> {
const r = await this.http(`${this.baseUrl}/v1/storage/get?key=${encodeURIComponent(key)}`, {
headers: { 'X-API-Key': this.apiKey }
});
if (!r.ok) throw new Error(`get failed: ${r.status}`);
const buf = new Uint8Array(await r.arrayBuffer());
return buf;
}
async exists(key: string): Promise<boolean> {
const r = await this.http(`${this.baseUrl}/v1/storage/exists?key=${encodeURIComponent(key)}`, {
headers: this.headers(false)
});
if (!r.ok) throw new Error(`exists failed: ${r.status}`);
const j = await r.json();
return !!j.exists;
}
async list(prefix = ""): Promise<string[]> {
const r = await this.http(`${this.baseUrl}/v1/storage/list?prefix=${encodeURIComponent(prefix)}`, {
headers: this.headers(false)
});
if (!r.ok) throw new Error(`list failed: ${r.status}`);
const j = await r.json();
return j.keys || [];
}
async delete(key: string): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/storage/delete`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ key })
});
if (!r.ok) throw new Error(`delete failed: ${r.status}`);
}
// PubSub (minimal)
subscribe(topic: string, onMessage: (data: Uint8Array) => void): { close: () => void } {
const url = new URL(`${this.baseUrl.replace(/^http/, 'ws')}/v1/pubsub/ws`);
url.searchParams.set('topic', topic);
const ws = new WebSocket(url.toString(), { headers: { 'X-API-Key': this.apiKey } } as any);
ws.binaryType = 'arraybuffer';
ws.onmessage = (ev: any) => {
const data = ev.data instanceof ArrayBuffer ? new Uint8Array(ev.data) : new TextEncoder().encode(String(ev.data));
onMessage(data);
};
return { close: () => ws.close() };
}
async publish(topic: string, data: Uint8Array | string): Promise<void> {
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
const b64 = Buffer.from(bytes).toString('base64');
const r = await this.http(`${this.baseUrl}/v1/pubsub/publish`, {
method: 'POST', headers: this.headers(), body: JSON.stringify({ topic, data_base64: b64 })
});
if (!r.ok) throw new Error(`publish failed: ${r.status}`);
}
}

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"moduleResolution": "Node"
},
"include": ["src/**/*"]
}

249
openapi/gateway.yaml Normal file
View File

@ -0,0 +1,249 @@
openapi: 3.0.3
info:
title: DeBros Gateway API
version: 1.0.0
description: REST API over the DeBros Network client for storage, database, and pubsub.
servers:
- url: http://localhost:8080
security:
- ApiKeyAuth: []
- BearerAuth: []
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
BearerAuth:
type: http
scheme: bearer
schemas:
Error:
type: object
properties:
error:
type: string
QueryRequest:
type: object
required: [sql]
properties:
sql:
type: string
args:
type: array
items: {}
QueryResponse:
type: object
properties:
columns:
type: array
items:
type: string
rows:
type: array
items:
type: array
items: {}
count:
type: integer
format: int64
TransactionRequest:
type: object
required: [statements]
properties:
statements:
type: array
items:
type: string
CreateTableRequest:
type: object
required: [schema]
properties:
schema:
type: string
DropTableRequest:
type: object
required: [table]
properties:
table:
type: string
TopicsResponse:
type: object
properties:
topics:
type: array
items:
type: string
paths:
/v1/health:
get:
summary: Gateway health
responses:
'200': { description: OK }
/v1/storage/put:
post:
summary: Store a value by key
parameters:
- in: query
name: key
schema: { type: string }
required: true
requestBody:
required: true
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
'201': { description: Created }
'400': { description: Bad Request, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'401': { description: Unauthorized }
'500': { description: Error, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
/v1/storage/get:
get:
summary: Get a value by key
parameters:
- in: query
name: key
schema: { type: string }
required: true
responses:
'200':
description: OK
content:
application/octet-stream:
schema:
type: string
format: binary
'404': { description: Not Found, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
/v1/storage/exists:
get:
summary: Check key existence
parameters:
- in: query
name: key
schema: { type: string }
required: true
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
exists:
type: boolean
/v1/storage/list:
get:
summary: List keys by prefix
parameters:
- in: query
name: prefix
schema: { type: string }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
keys:
type: array
items:
type: string
/v1/storage/delete:
post:
summary: Delete a key
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [key]
properties:
key: { type: string }
responses:
'200': { description: OK }
/v1/db/create-table:
post:
summary: Create tables via SQL DDL
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateTableRequest' }
responses:
'201': { description: Created }
'400': { description: Bad Request, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'500': { description: Error, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
/v1/db/drop-table:
post:
summary: Drop a table
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/DropTableRequest' }
responses:
'200': { description: OK }
/v1/db/query:
post:
summary: Execute a single SQL query
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/QueryRequest' }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/QueryResponse' }
'400': { description: Bad Request, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'500': { description: Error, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
/v1/db/transaction:
post:
summary: Execute multiple SQL statements atomically
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/TransactionRequest' }
responses:
'200': { description: OK }
'400': { description: Bad Request, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'500': { description: Error, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
/v1/db/schema:
get:
summary: Get current database schema
responses:
'200': { description: OK }
/v1/pubsub/publish:
post:
summary: Publish to a topic
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [topic, data_base64]
properties:
topic: { type: string }
data_base64: { type: string }
responses:
'200': { description: OK }
/v1/pubsub/topics:
get:
summary: List topics in caller namespace
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/TopicsResponse' }