remove demo app md
This commit is contained in:
@@ -1,267 +0,0 @@
|
|||||||
# 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>"
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user