268 lines
5.3 KiB
Markdown
268 lines
5.3 KiB
Markdown
# Demo App Transport Migration
|
|
|
|
This guide describes the client-facing protocol for the route-agnostic transport
|
|
architecture.
|
|
|
|
## Connection Model
|
|
|
|
HTTP, SSE, and WebSocket do not authenticate at the transport layer. Do not send
|
|
request-auth headers such as `X-PublicKey`, `X-Signature`, or `X-Timestamp`
|
|
for normal route dispatch.
|
|
|
|
Authentication is scoped to each resource operation. Every written resource
|
|
carries its own `publicKey`, `timestamp`, and `signature`.
|
|
|
|
All route input belongs in the request body. Paths are exact route names; there
|
|
are no path parameters, query parameters, or alternate HTTP methods.
|
|
|
|
## Request Size Limit
|
|
|
|
The server accepts at most 1048576 encoded bytes per HTTP request body or
|
|
complete WebSocket message by default. Deployments can change this with
|
|
`SERVER_MAX_REQUEST_BODY_BYTES`. The count is over the Extended JSON wire text,
|
|
so hexadecimal `Uint8Array` values use approximately twice their decoded byte
|
|
length.
|
|
|
|
HTTP limits use Hono's built-in `bodyLimit` middleware and return `413`.
|
|
WebSocket messages do not pass through HTTP middleware; Hono's Node adapter uses
|
|
`ws`, whose native `maxPayload` option rejects oversized frames before the
|
|
application message handler. Response bodies are not limited.
|
|
|
|
## HTTP Requests
|
|
|
|
All application HTTP routes use POST.
|
|
|
|
Read:
|
|
|
|
```http
|
|
POST /data/get
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"resourceId": ["resource-a", "resource-b"]
|
|
}
|
|
```
|
|
|
|
Write:
|
|
|
|
```http
|
|
POST /data/write
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"resources": [
|
|
{
|
|
"id": "resource-a",
|
|
"publicKey": "...hex...",
|
|
"timestamp": 1730000000000,
|
|
"signature": "...der-hex...",
|
|
"value": "<Uint8Array: deadbeef>"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Write response:
|
|
|
|
```json
|
|
{
|
|
"resources": [
|
|
{
|
|
"id": "resource-a",
|
|
"publicKey": "...hex...",
|
|
"blob": "<Uint8Array: deadbeef>",
|
|
"timestamp": 1730000001000
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
If any item has invalid authorization, the whole batch fails.
|
|
|
|
## SSE Subscriptions
|
|
|
|
The client explicitly requests SSE using `Accept: text/event-stream`:
|
|
|
|
```http
|
|
POST /data/subscribe
|
|
Accept: text/event-stream
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"resourceId": ["resource-a"]
|
|
}
|
|
```
|
|
|
|
A subscription without this Accept header receives a `406` JSON error.
|
|
|
|
The server keeps the SSE request open while its resource topics remain
|
|
subscribed. Subscribing sends no current state; it only enables future
|
|
publications. Request current state separately through `/data/get` when needed.
|
|
|
|
SSE publication:
|
|
|
|
```text
|
|
id: 1730000001000
|
|
event: instance-changed
|
|
data: {"resourceId":"resource-a"}
|
|
```
|
|
|
|
After SSE begins, failures arrive as error events and the server closes that SSE
|
|
connection:
|
|
|
|
```text
|
|
event: error
|
|
data: {"statusCode":500,"error":"Internal Server Error"}
|
|
```
|
|
|
|
To change an SSE subscription, abort the existing request and open a new
|
|
`/data/subscribe` request with the complete desired `resourceId` list.
|
|
|
|
## WebSocket
|
|
|
|
Connect without a connection-auth message:
|
|
|
|
```ts
|
|
const ws = new WebSocket("ws://host:port/ws");
|
|
```
|
|
|
|
Every client message uses this strict envelope. Messages may execute
|
|
concurrently; the server binds `id` and `body` to each individual dispatch so
|
|
responses remain correctly correlated even when they finish out of order:
|
|
|
|
```ts
|
|
type WsRequest = {
|
|
id?: string;
|
|
path: string;
|
|
body?: unknown;
|
|
};
|
|
```
|
|
|
|
Old fields such as `event`, `type`, `url`, `method`, `headers`,
|
|
`params`, and `data` are rejected.
|
|
|
|
Read:
|
|
|
|
```json
|
|
{
|
|
"id": "read-1",
|
|
"path": "/data/get",
|
|
"body": {
|
|
"resourceId": ["resource-a", "resource-b"]
|
|
}
|
|
}
|
|
```
|
|
|
|
Write:
|
|
|
|
```json
|
|
{
|
|
"id": "write-1",
|
|
"path": "/data/write",
|
|
"body": {
|
|
"resources": [
|
|
{
|
|
"id": "resource-a",
|
|
"publicKey": "...hex...",
|
|
"timestamp": 1730000000000,
|
|
"signature": "...der-hex...",
|
|
"value": "<Uint8Array: deadbeef>"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Subscribe:
|
|
|
|
```json
|
|
{
|
|
"id": "sub-1",
|
|
"path": "/data/subscribe",
|
|
"body": {
|
|
"resourceId": ["resource-a"]
|
|
}
|
|
}
|
|
```
|
|
|
|
The subscribe request sends no immediate message. Its dispatch remains active
|
|
until those topics are removed or the connection closes. Request current state
|
|
separately through `/data/get` when needed.
|
|
|
|
The client should consolidate its local subscribers and send only the topic set
|
|
needed by the shared WebSocket.
|
|
|
|
Unsubscribe selected resources without closing the socket:
|
|
|
|
```json
|
|
{
|
|
"id": "unsub-1",
|
|
"path": "/data/unsubscribe",
|
|
"body": {
|
|
"resourceId": ["resource-a"]
|
|
}
|
|
}
|
|
```
|
|
|
|
Unsubscribe is idempotent. The response is a normal correlated response:
|
|
|
|
```json
|
|
{
|
|
"id": "unsub-1",
|
|
"type": "response",
|
|
"statusCode": 200,
|
|
"body": {}
|
|
}
|
|
```
|
|
|
|
Normal response:
|
|
|
|
```json
|
|
{
|
|
"id": "write-1",
|
|
"type": "response",
|
|
"statusCode": 200,
|
|
"body": {}
|
|
}
|
|
```
|
|
|
|
Error response:
|
|
|
|
```json
|
|
{
|
|
"id": "write-1",
|
|
"type": "error",
|
|
"statusCode": 400,
|
|
"error": "Validation Error",
|
|
"details": []
|
|
}
|
|
```
|
|
|
|
Message-level errors do not close the WebSocket. The client decides whether to
|
|
retry, alter its subscription, or reconnect.
|
|
|
|
Published events retain the shared stream shape:
|
|
|
|
```json
|
|
{
|
|
"id": "server-event-id",
|
|
"type": "instance-changed",
|
|
"data": {}
|
|
}
|
|
```
|
|
|
|
## Signing
|
|
|
|
For each written resource, sign this canonical payload:
|
|
|
|
```ts
|
|
`${timestamp}${resourceId}${canonicalBody(value)}`;
|
|
```
|
|
|
|
`value` is the `Uint8Array` payload. `canonicalBody(value)` uses the same
|
|
Extended JSON rules as the server:
|
|
|
|
```json
|
|
"<Uint8Array: deadbeef>"
|
|
```
|