@graphorin/protocol 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +2 -2
- package/dist/client-message.d.ts +24 -24
- package/dist/client-message.d.ts.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/server-message.d.ts +32 -32
- package/dist/server-message.d.ts.map +1 -1
- package/dist/server-message.js +14 -2
- package/dist/server-message.js.map +1 -1
- package/package.json +8 -7
- package/src/client-message.ts +200 -0
- package/src/close-codes.ts +67 -0
- package/src/index.ts +25 -0
- package/src/server-message.ts +268 -0
- package/src/subprotocol.ts +106 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-message.js","names":[],"sources":["../src/server-message.ts"],"sourcesContent":["/**\n * `ServerMessage` - discriminated union of every frame a Graphorin\n * server may push to a client. Three families share the channel:\n *\n * 1. **RPC responses** (`{ jsonrpc, id, result | error }`) -\n * correlate with a previously-issued client request.\n * 2. **Typed push events** (`{ kind: 'event', subject, type,\n * payload, eventId }`) - the streaming-first data plane;\n * consumers ignore unknown `type` strings per the agent-event\n * extensibility convention.\n * 3. **Asynchronous server frames** (`{ kind: 'lifecycle' | 'error'\n * | 'pong' | 'subscribed' | 'unsubscribed' | 'replay-marker' }`)\n * - server-initiated messages that do not correlate with a\n * single client RPC id.\n *\n * Every frame carries the `v: '1'` literal so future revisions can\n * negotiate forward-compatible additions without a subprotocol bump.\n *\n * @packageDocumentation\n */\n\nimport { z } from 'zod';\n\nconst RpcId = z.union([z.string().min(1), z.number().int()]);\n\nconst RpcError = z\n .object({\n code: z.number().int(),\n message: z.string().min(1),\n data: z.unknown().optional(),\n })\n .strict();\n\nconst RpcSuccess = z\n .object({\n v: z.literal('1'),\n jsonrpc: z.literal('2.0'),\n id: RpcId,\n result: z.unknown(),\n })\n .strict();\n\nconst RpcFailure = z\n .object({\n v: z.literal('1'),\n jsonrpc: z.literal('2.0'),\n id: RpcId,\n error: RpcError,\n })\n .strict();\n\nconst SubscribedFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('subscribed'),\n subscriptionId: z.string().min(1),\n subject: z.string().min(1),\n snapshotEventId: z.string().min(1).optional(),\n })\n .strict();\n\nconst UnsubscribedFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('unsubscribed'),\n subscriptionId: z.string().min(1),\n })\n .strict();\n\nconst EventFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('event'),\n eventId: z.string().min(1),\n subscriptionId: z.string().min(1),\n subject: z.string().min(1),\n type: z.string().min(1),\n payload: z.unknown(),\n })\n .strict();\n\nconst LifecycleFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('lifecycle'),\n subscriptionId: z.string().min(1),\n status: z.enum(['running', 'paused', 'completed', 'aborted', 'failed']),\n reason: z.string().min(1).optional(),\n })\n .strict();\n\nconst ErrorFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('error'),\n code: z.string().min(1),\n message: z.string().min(1),\n fatal: z.boolean().optional(),\n subscriptionId: z.string().min(1).optional(),\n data: z.unknown().optional(),\n })\n .strict();\n\nconst PongFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('pong'),\n nonce: z.string().min(1).optional(),\n })\n .strict();\n\nconst ReplayMarkerFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('replay-marker'),\n subscriptionId: z.string().min(1),\n eventId: z.string().min(1),\n droppedCount: z.number().int().nonnegative().optional(),\n note: z.string().min(1).optional(),\n })\n .strict();\n\nconst KindedFrame = z.discriminatedUnion('kind', [\n SubscribedFrame,\n UnsubscribedFrame,\n EventFrame,\n LifecycleFrame,\n ErrorFrame,\n PongFrame,\n ReplayMarkerFrame,\n]);\n\nconst RpcFrame = z.union([RpcSuccess, RpcFailure]);\n\n/**\n * Zod schema for every legal server → client frame. Validation runs\n * twice in the server pipeline: first when a route handler enqueues\n * the frame onto the dispatcher's send queue (so a malformed frame\n * never escapes the process), then again on the client side to\n * defend against protocol drift.\n *\n * @stable\n */\nexport const ServerMessageSchema = z.union([RpcFrame, KindedFrame]);\n\n/**\n * Inferred TypeScript union for the `ServerMessage` discriminator.\n *\n * @stable\n */\nexport type ServerMessage = z.infer<typeof ServerMessageSchema>;\n\n/**\n * Convenience type aliases for callers that want to reference an\n * individual variant without `z.infer<typeof X>`.\n *\n * @stable\n */\nexport type ServerEventFrame = z.infer<typeof EventFrame>;\n/** @stable */\nexport type ServerLifecycleFrame = z.infer<typeof LifecycleFrame>;\n/** @stable */\nexport type ServerErrorFrame = z.infer<typeof ErrorFrame>;\n/** @stable */\nexport type ServerSubscribedFrame = z.infer<typeof SubscribedFrame>;\n/** @stable */\nexport type ServerUnsubscribedFrame = z.infer<typeof UnsubscribedFrame>;\n/** @stable */\nexport type ServerPongFrame = z.infer<typeof PongFrame>;\n/** @stable */\nexport type ServerReplayMarkerFrame = z.infer<typeof ReplayMarkerFrame>;\n/** @stable */\nexport type ServerRpcSuccess = z.infer<typeof RpcSuccess>;\n/** @stable */\nexport type ServerRpcFailure = z.infer<typeof RpcFailure>;\n\n/**\n * Type guard helpers, one per discriminator. The narrow over the\n * `ServerMessage` union without forcing consumers to memorize the\n * exact field names.\n *\n * @stable\n */\nexport function isEventFrame(message: ServerMessage): message is ServerEventFrame {\n return 'kind' in message && message.kind === 'event';\n}\n\n/** @stable */\nexport function isLifecycleFrame(message: ServerMessage): message is ServerLifecycleFrame {\n return 'kind' in message && message.kind === 'lifecycle';\n}\n\n/** @stable */\nexport function isErrorFrame(message: ServerMessage): message is ServerErrorFrame {\n return 'kind' in message && message.kind === 'error';\n}\n\n/** @stable */\nexport function isSubscribedFrame(message: ServerMessage): message is ServerSubscribedFrame {\n return 'kind' in message && message.kind === 'subscribed';\n}\n\n/** @stable */\nexport function isUnsubscribedFrame(message: ServerMessage): message is ServerUnsubscribedFrame {\n return 'kind' in message && message.kind === 'unsubscribed';\n}\n\n/** @stable */\nexport function isPongFrame(message: ServerMessage): message is ServerPongFrame {\n return 'kind' in message && message.kind === 'pong';\n}\n\n/** @stable */\nexport function isReplayMarkerFrame(message: ServerMessage): message is ServerReplayMarkerFrame {\n return 'kind' in message && message.kind === 'replay-marker';\n}\n\n/** @stable */\nexport function isRpcSuccess(message: ServerMessage): message is ServerRpcSuccess {\n return 'jsonrpc' in message && message.jsonrpc === '2.0' && 'result' in message;\n}\n\n/** @stable */\nexport function isRpcFailure(message: ServerMessage): message is ServerRpcFailure {\n return 'jsonrpc' in message && message.jsonrpc === '2.0' && 'error' in message;\n}\n\n/**\n * Stable JSON-RPC error code catalogue used by the server when\n * surfacing routine failures (per JSON-RPC 2.0 § 5.1 + Graphorin\n * extensions). Application-level errors use codes in the\n * implementation-defined range (`-32000` … `-32099`).\n *\n * @stable\n */\nexport const RPC_ERROR_CODES = Object.freeze({\n PARSE_ERROR: -32700,\n INVALID_REQUEST: -32600,\n METHOD_NOT_FOUND: -32601,\n INVALID_PARAMS: -32602,\n INTERNAL_ERROR: -32603,\n AUTH_REQUIRED: -32001,\n AUTH_INVALID: -32002,\n SCOPE_DENIED: -32003,\n RATE_LIMITED: -32004,\n PROTOCOL_VIOLATION: -32005,\n RUN_NOT_FOUND: -32010,\n SUBSCRIPTION_NOT_FOUND: -32011,\n} as const);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server-message.js","names":[],"sources":["../src/server-message.ts"],"sourcesContent":["/**\n * `ServerMessage` - discriminated union of every frame a Graphorin\n * server may push to a client. Three families share the channel:\n *\n * 1. **RPC responses** (`{ jsonrpc, id, result | error }`) -\n * correlate with a previously-issued client request.\n * 2. **Typed push events** (`{ kind: 'event', subject, type,\n * payload, eventId }`) - the streaming-first data plane;\n * consumers ignore unknown `type` strings per the agent-event\n * extensibility convention.\n * 3. **Asynchronous server frames** (`{ kind: 'lifecycle' | 'error'\n * | 'pong' | 'subscribed' | 'unsubscribed' | 'replay-marker' }`)\n * - server-initiated messages that do not correlate with a\n * single client RPC id.\n *\n * Versioning contract (W-109, honest edition): the frame ENVELOPE -\n * the set of `kind`s, the fields of the control frames, and the\n * `v: '1'` literal - is validated strictly (`.strict()`, literal `v`)\n * on BOTH server and client, and evolves only in lockstep with both\n * sides shipping together: there is NO negotiation, and a frame with\n * `v: '2'` is rejected outright. That is the deployment model of the\n * 0.x line. The additive extension points are deliberate and inside\n * the envelope: the `type` string + `payload: z.unknown()` of event\n * frames (consumers ignore unknown `type`s per the agent-event\n * convention), `result: z.unknown()` of RPC responses, and the\n * `capabilities` record of the `initialize` result (which today the\n * shipped client does not consume). If additive envelope evolution is\n * ever promised publicly, that is a separate negotiation feature -\n * do not loosen `.strict()` for it piecemeal.\n *\n * @packageDocumentation\n */\n\nimport { z } from 'zod';\n\nconst RpcId = z.union([z.string().min(1), z.number().int()]);\n\nconst RpcError = z\n .object({\n code: z.number().int(),\n message: z.string().min(1),\n data: z.unknown().optional(),\n })\n .strict();\n\nconst RpcSuccess = z\n .object({\n v: z.literal('1'),\n jsonrpc: z.literal('2.0'),\n id: RpcId,\n result: z.unknown(),\n })\n .strict();\n\nconst RpcFailure = z\n .object({\n v: z.literal('1'),\n jsonrpc: z.literal('2.0'),\n id: RpcId,\n error: RpcError,\n })\n .strict();\n\nconst SubscribedFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('subscribed'),\n subscriptionId: z.string().min(1),\n subject: z.string().min(1),\n snapshotEventId: z.string().min(1).optional(),\n })\n .strict();\n\nconst UnsubscribedFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('unsubscribed'),\n subscriptionId: z.string().min(1),\n })\n .strict();\n\n// The runtime schema keeps `payload` opaque so this package stays\n// zod-only (no `@graphorin/core` dependency, not even type-only - it\n// would force a resolvable dependency for the d.ts). For agent-run\n// subjects the actual payload shape is the JSON-safe `WireAgentEvent`\n// union documented in `@graphorin/core` (decode with\n// `fromWireAgentEvent`); the cross-package round-trip is pinned by a\n// gate test in core.\nconst EventFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('event'),\n eventId: z.string().min(1),\n subscriptionId: z.string().min(1),\n subject: z.string().min(1),\n type: z.string().min(1),\n payload: z.unknown(),\n })\n .strict();\n\nconst LifecycleFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('lifecycle'),\n subscriptionId: z.string().min(1),\n status: z.enum(['running', 'paused', 'completed', 'aborted', 'failed']),\n reason: z.string().min(1).optional(),\n })\n .strict();\n\nconst ErrorFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('error'),\n code: z.string().min(1),\n message: z.string().min(1),\n fatal: z.boolean().optional(),\n subscriptionId: z.string().min(1).optional(),\n data: z.unknown().optional(),\n })\n .strict();\n\nconst PongFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('pong'),\n nonce: z.string().min(1).optional(),\n })\n .strict();\n\nconst ReplayMarkerFrame = z\n .object({\n v: z.literal('1'),\n kind: z.literal('replay-marker'),\n subscriptionId: z.string().min(1),\n eventId: z.string().min(1),\n droppedCount: z.number().int().nonnegative().optional(),\n note: z.string().min(1).optional(),\n })\n .strict();\n\nconst KindedFrame = z.discriminatedUnion('kind', [\n SubscribedFrame,\n UnsubscribedFrame,\n EventFrame,\n LifecycleFrame,\n ErrorFrame,\n PongFrame,\n ReplayMarkerFrame,\n]);\n\nconst RpcFrame = z.union([RpcSuccess, RpcFailure]);\n\n/**\n * Zod schema for every legal server → client frame. Validation runs\n * twice in the server pipeline: first when a route handler enqueues\n * the frame onto the dispatcher's send queue (so a malformed frame\n * never escapes the process), then again on the client side to\n * defend against protocol drift.\n *\n * @stable\n */\nexport const ServerMessageSchema = z.union([RpcFrame, KindedFrame]);\n\n/**\n * Inferred TypeScript union for the `ServerMessage` discriminator.\n *\n * @stable\n */\nexport type ServerMessage = z.infer<typeof ServerMessageSchema>;\n\n/**\n * Convenience type aliases for callers that want to reference an\n * individual variant without `z.infer<typeof X>`.\n *\n * @stable\n */\nexport type ServerEventFrame = z.infer<typeof EventFrame>;\n/** @stable */\nexport type ServerLifecycleFrame = z.infer<typeof LifecycleFrame>;\n/** @stable */\nexport type ServerErrorFrame = z.infer<typeof ErrorFrame>;\n/** @stable */\nexport type ServerSubscribedFrame = z.infer<typeof SubscribedFrame>;\n/** @stable */\nexport type ServerUnsubscribedFrame = z.infer<typeof UnsubscribedFrame>;\n/** @stable */\nexport type ServerPongFrame = z.infer<typeof PongFrame>;\n/** @stable */\nexport type ServerReplayMarkerFrame = z.infer<typeof ReplayMarkerFrame>;\n/** @stable */\nexport type ServerRpcSuccess = z.infer<typeof RpcSuccess>;\n/** @stable */\nexport type ServerRpcFailure = z.infer<typeof RpcFailure>;\n\n/**\n * Type guard helpers, one per discriminator. The narrow over the\n * `ServerMessage` union without forcing consumers to memorize the\n * exact field names.\n *\n * @stable\n */\nexport function isEventFrame(message: ServerMessage): message is ServerEventFrame {\n return 'kind' in message && message.kind === 'event';\n}\n\n/** @stable */\nexport function isLifecycleFrame(message: ServerMessage): message is ServerLifecycleFrame {\n return 'kind' in message && message.kind === 'lifecycle';\n}\n\n/** @stable */\nexport function isErrorFrame(message: ServerMessage): message is ServerErrorFrame {\n return 'kind' in message && message.kind === 'error';\n}\n\n/** @stable */\nexport function isSubscribedFrame(message: ServerMessage): message is ServerSubscribedFrame {\n return 'kind' in message && message.kind === 'subscribed';\n}\n\n/** @stable */\nexport function isUnsubscribedFrame(message: ServerMessage): message is ServerUnsubscribedFrame {\n return 'kind' in message && message.kind === 'unsubscribed';\n}\n\n/** @stable */\nexport function isPongFrame(message: ServerMessage): message is ServerPongFrame {\n return 'kind' in message && message.kind === 'pong';\n}\n\n/** @stable */\nexport function isReplayMarkerFrame(message: ServerMessage): message is ServerReplayMarkerFrame {\n return 'kind' in message && message.kind === 'replay-marker';\n}\n\n/** @stable */\nexport function isRpcSuccess(message: ServerMessage): message is ServerRpcSuccess {\n return 'jsonrpc' in message && message.jsonrpc === '2.0' && 'result' in message;\n}\n\n/** @stable */\nexport function isRpcFailure(message: ServerMessage): message is ServerRpcFailure {\n return 'jsonrpc' in message && message.jsonrpc === '2.0' && 'error' in message;\n}\n\n/**\n * Stable JSON-RPC error code catalogue used by the server when\n * surfacing routine failures (per JSON-RPC 2.0 § 5.1 + Graphorin\n * extensions). Application-level errors use codes in the\n * implementation-defined range (`-32000` … `-32099`).\n *\n * @stable\n */\nexport const RPC_ERROR_CODES = Object.freeze({\n PARSE_ERROR: -32700,\n INVALID_REQUEST: -32600,\n METHOD_NOT_FOUND: -32601,\n INVALID_PARAMS: -32602,\n INTERNAL_ERROR: -32603,\n AUTH_REQUIRED: -32001,\n AUTH_INVALID: -32002,\n SCOPE_DENIED: -32003,\n RATE_LIMITED: -32004,\n PROTOCOL_VIOLATION: -32005,\n RUN_NOT_FOUND: -32010,\n SUBSCRIPTION_NOT_FOUND: -32011,\n} as const);\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphorin/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Wire protocol contract for the Graphorin framework: Zod schemas + TypeScript types for the WebSocket subprotocol `graphorin.protocol.v1`. Defines the `ClientMessage` and `ServerMessage` discriminated unions, the JSON-RPC-shaped control channel (`initialize` / `subscription.subscribe` / `subscription.unsubscribe` / `run.cancel` / `ping`), the typed event push frames (`{ kind: 'event', subject, type, payload, eventId }`), the asynchronous server-initiated error frames (`{ kind: 'error', code, message }`), the subprotocol negotiation helpers, and the close-code taxonomy (4001 auth.required, 4002 auth.invalid, 4003 auth.revoked, 4004 auth.scope_denied, 4005 rate.limited, 4006 flow.throttled, 4007 server.shutdown, 4008 protocol.violation). Browser-friendly: zero Node-only dependencies; the only runtime dependency is `zod`. Created and maintained by Oleksiy Stepurenko.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Oleksiy Stepurenko",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"type": "module",
|
|
29
29
|
"engines": {
|
|
30
|
-
"node": ">=22.
|
|
30
|
+
"node": ">=22.12.0"
|
|
31
31
|
},
|
|
32
32
|
"main": "./dist/index.js",
|
|
33
33
|
"module": "./dist/index.js",
|
|
@@ -36,28 +36,29 @@
|
|
|
36
36
|
"exports": {
|
|
37
37
|
".": {
|
|
38
38
|
"types": "./dist/index.d.ts",
|
|
39
|
-
"
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
40
|
},
|
|
41
41
|
"./client-message": {
|
|
42
42
|
"types": "./dist/client-message.d.ts",
|
|
43
|
-
"
|
|
43
|
+
"default": "./dist/client-message.js"
|
|
44
44
|
},
|
|
45
45
|
"./server-message": {
|
|
46
46
|
"types": "./dist/server-message.d.ts",
|
|
47
|
-
"
|
|
47
|
+
"default": "./dist/server-message.js"
|
|
48
48
|
},
|
|
49
49
|
"./subprotocol": {
|
|
50
50
|
"types": "./dist/subprotocol.d.ts",
|
|
51
|
-
"
|
|
51
|
+
"default": "./dist/subprotocol.js"
|
|
52
52
|
},
|
|
53
53
|
"./close-codes": {
|
|
54
54
|
"types": "./dist/close-codes.d.ts",
|
|
55
|
-
"
|
|
55
|
+
"default": "./dist/close-codes.js"
|
|
56
56
|
},
|
|
57
57
|
"./package.json": "./package.json"
|
|
58
58
|
},
|
|
59
59
|
"files": [
|
|
60
60
|
"dist",
|
|
61
|
+
"src",
|
|
61
62
|
"README.md",
|
|
62
63
|
"CHANGELOG.md",
|
|
63
64
|
"LICENSE"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ClientMessage` - discriminated union of every frame a Graphorin
|
|
3
|
+
* WebSocket client may send to the server. The wire is hybrid: the
|
|
4
|
+
* control plane uses JSON-RPC-shaped requests / notifications; the
|
|
5
|
+
* data plane uses typed push events emitted exclusively by the server
|
|
6
|
+
* (see `./server-message.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Every frame carries the `v: '1'` literal so future revisions can
|
|
9
|
+
* negotiate forward-compatible additions without a subprotocol bump.
|
|
10
|
+
* The matching subprotocol identifier is `graphorin.protocol.v1`
|
|
11
|
+
* (see `./subprotocol.ts`).
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
|
|
18
|
+
const RpcId = z.union([z.string().min(1), z.number().int()]);
|
|
19
|
+
|
|
20
|
+
const InitializeParams = z
|
|
21
|
+
.object({
|
|
22
|
+
clientInfo: z
|
|
23
|
+
.object({
|
|
24
|
+
name: z.string().min(1),
|
|
25
|
+
version: z.string().min(1),
|
|
26
|
+
})
|
|
27
|
+
.strict(),
|
|
28
|
+
capabilities: z.record(z.string(), z.unknown()).optional(),
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
31
|
+
|
|
32
|
+
const SubscribeParams = z
|
|
33
|
+
.object({
|
|
34
|
+
subject: z.string().min(1),
|
|
35
|
+
// IP-21: `sinceEventId` is the resumption cursor. `lastSequenceId` was a
|
|
36
|
+
// dead wire field - the client never set it and the server never read it.
|
|
37
|
+
sinceEventId: z.string().min(1).optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
|
|
41
|
+
const UnsubscribeParams = z
|
|
42
|
+
.object({
|
|
43
|
+
subscriptionId: z.string().min(1),
|
|
44
|
+
})
|
|
45
|
+
.strict();
|
|
46
|
+
|
|
47
|
+
const RunCancelParams = z
|
|
48
|
+
.object({
|
|
49
|
+
runId: z.string().min(1),
|
|
50
|
+
reason: z.string().min(1).optional(),
|
|
51
|
+
drain: z.boolean().optional(),
|
|
52
|
+
onPendingApprovals: z.enum(['deny', 'preserve']).optional(),
|
|
53
|
+
})
|
|
54
|
+
.strict();
|
|
55
|
+
|
|
56
|
+
const PingParams = z
|
|
57
|
+
.object({
|
|
58
|
+
nonce: z.string().min(1).optional(),
|
|
59
|
+
})
|
|
60
|
+
.strict()
|
|
61
|
+
.optional();
|
|
62
|
+
|
|
63
|
+
const InitializeRequest = z
|
|
64
|
+
.object({
|
|
65
|
+
v: z.literal('1'),
|
|
66
|
+
jsonrpc: z.literal('2.0'),
|
|
67
|
+
id: RpcId,
|
|
68
|
+
method: z.literal('initialize'),
|
|
69
|
+
params: InitializeParams,
|
|
70
|
+
})
|
|
71
|
+
.strict();
|
|
72
|
+
|
|
73
|
+
const SubscribeRequest = z
|
|
74
|
+
.object({
|
|
75
|
+
v: z.literal('1'),
|
|
76
|
+
jsonrpc: z.literal('2.0'),
|
|
77
|
+
id: RpcId,
|
|
78
|
+
method: z.literal('subscription.subscribe'),
|
|
79
|
+
params: SubscribeParams,
|
|
80
|
+
})
|
|
81
|
+
.strict();
|
|
82
|
+
|
|
83
|
+
const UnsubscribeRequest = z
|
|
84
|
+
.object({
|
|
85
|
+
v: z.literal('1'),
|
|
86
|
+
jsonrpc: z.literal('2.0'),
|
|
87
|
+
id: RpcId,
|
|
88
|
+
method: z.literal('subscription.unsubscribe'),
|
|
89
|
+
params: UnsubscribeParams,
|
|
90
|
+
})
|
|
91
|
+
.strict();
|
|
92
|
+
|
|
93
|
+
const RunCancelRequest = z
|
|
94
|
+
.object({
|
|
95
|
+
v: z.literal('1'),
|
|
96
|
+
jsonrpc: z.literal('2.0'),
|
|
97
|
+
id: RpcId,
|
|
98
|
+
method: z.literal('run.cancel'),
|
|
99
|
+
params: RunCancelParams,
|
|
100
|
+
})
|
|
101
|
+
.strict();
|
|
102
|
+
|
|
103
|
+
const PingRequest = z
|
|
104
|
+
.object({
|
|
105
|
+
v: z.literal('1'),
|
|
106
|
+
jsonrpc: z.literal('2.0'),
|
|
107
|
+
id: RpcId,
|
|
108
|
+
method: z.literal('ping'),
|
|
109
|
+
params: PingParams,
|
|
110
|
+
})
|
|
111
|
+
.strict();
|
|
112
|
+
|
|
113
|
+
const CancelledNotification = z
|
|
114
|
+
.object({
|
|
115
|
+
v: z.literal('1'),
|
|
116
|
+
jsonrpc: z.literal('2.0'),
|
|
117
|
+
method: z.literal('notifications/cancelled'),
|
|
118
|
+
params: z.object({ requestId: z.string().min(1) }).strict(),
|
|
119
|
+
})
|
|
120
|
+
.strict();
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Zod schema for every legal client → server frame. Use
|
|
124
|
+
* {@link ClientMessageSchema}.safeParse() inside the server upgrade
|
|
125
|
+
* handler before dispatching to the corresponding subscription /
|
|
126
|
+
* cancel / ping handler.
|
|
127
|
+
*
|
|
128
|
+
* @stable
|
|
129
|
+
*/
|
|
130
|
+
export const ClientMessageSchema = z.discriminatedUnion('method', [
|
|
131
|
+
InitializeRequest,
|
|
132
|
+
SubscribeRequest,
|
|
133
|
+
UnsubscribeRequest,
|
|
134
|
+
RunCancelRequest,
|
|
135
|
+
PingRequest,
|
|
136
|
+
CancelledNotification,
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Inferred TypeScript union for the `ClientMessage` discriminator. A
|
|
141
|
+
* value satisfying this type round-trips through
|
|
142
|
+
* {@link ClientMessageSchema} without throwing.
|
|
143
|
+
*
|
|
144
|
+
* @stable
|
|
145
|
+
*/
|
|
146
|
+
export type ClientMessage = z.infer<typeof ClientMessageSchema>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convenience type for the JSON-RPC `id` slot. Matches the Graphorin
|
|
150
|
+
* subset (string + integer; no `null`, no float).
|
|
151
|
+
*
|
|
152
|
+
* @stable
|
|
153
|
+
*/
|
|
154
|
+
export type ClientMessageId = z.infer<typeof RpcId>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Type guard helpers - one per `method` literal - so consumers can
|
|
158
|
+
* narrow the `ClientMessage` union without re-stringifying the
|
|
159
|
+
* discriminator.
|
|
160
|
+
*
|
|
161
|
+
* @stable
|
|
162
|
+
*/
|
|
163
|
+
export function isInitializeRequest(
|
|
164
|
+
message: ClientMessage,
|
|
165
|
+
): message is z.infer<typeof InitializeRequest> {
|
|
166
|
+
return message.method === 'initialize';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @stable */
|
|
170
|
+
export function isSubscribeRequest(
|
|
171
|
+
message: ClientMessage,
|
|
172
|
+
): message is z.infer<typeof SubscribeRequest> {
|
|
173
|
+
return message.method === 'subscription.subscribe';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @stable */
|
|
177
|
+
export function isUnsubscribeRequest(
|
|
178
|
+
message: ClientMessage,
|
|
179
|
+
): message is z.infer<typeof UnsubscribeRequest> {
|
|
180
|
+
return message.method === 'subscription.unsubscribe';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** @stable */
|
|
184
|
+
export function isRunCancelRequest(
|
|
185
|
+
message: ClientMessage,
|
|
186
|
+
): message is z.infer<typeof RunCancelRequest> {
|
|
187
|
+
return message.method === 'run.cancel';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @stable */
|
|
191
|
+
export function isPingRequest(message: ClientMessage): message is z.infer<typeof PingRequest> {
|
|
192
|
+
return message.method === 'ping';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @stable */
|
|
196
|
+
export function isCancelledNotification(
|
|
197
|
+
message: ClientMessage,
|
|
198
|
+
): message is z.infer<typeof CancelledNotification> {
|
|
199
|
+
return message.method === 'notifications/cancelled';
|
|
200
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom WebSocket close-code taxonomy used by `@graphorin/server`
|
|
3
|
+
* and `@graphorin/client`. The numeric values live in the 4xxx
|
|
4
|
+
* application-private range per RFC 6455 § 7.4.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Discriminator for every Graphorin-defined close code. The
|
|
11
|
+
* matching numeric value is exposed via {@link CLOSE_CODE_VALUES}.
|
|
12
|
+
*
|
|
13
|
+
* @stable
|
|
14
|
+
*/
|
|
15
|
+
export type GraphorinCloseReason =
|
|
16
|
+
| 'auth.required'
|
|
17
|
+
| 'auth.invalid'
|
|
18
|
+
| 'auth.revoked'
|
|
19
|
+
| 'auth.scope_denied'
|
|
20
|
+
| 'rate.limited'
|
|
21
|
+
| 'flow.throttled'
|
|
22
|
+
| 'server.shutdown'
|
|
23
|
+
| 'protocol.violation';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Numeric close-code constants. The pair `(value, reason)` round-trips
|
|
27
|
+
* via {@link closeCodeReason} / {@link closeCodeFor}.
|
|
28
|
+
*
|
|
29
|
+
* @stable
|
|
30
|
+
*/
|
|
31
|
+
export const CLOSE_CODE_VALUES = Object.freeze({
|
|
32
|
+
'auth.required': 4001,
|
|
33
|
+
'auth.invalid': 4002,
|
|
34
|
+
'auth.revoked': 4003,
|
|
35
|
+
'auth.scope_denied': 4004,
|
|
36
|
+
'rate.limited': 4005,
|
|
37
|
+
'flow.throttled': 4006,
|
|
38
|
+
'server.shutdown': 4007,
|
|
39
|
+
'protocol.violation': 4008,
|
|
40
|
+
} as const satisfies Record<GraphorinCloseReason, number>);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Return the numeric close code for a Graphorin reason discriminator.
|
|
44
|
+
*
|
|
45
|
+
* @stable
|
|
46
|
+
*/
|
|
47
|
+
export function closeCodeFor(reason: GraphorinCloseReason): number {
|
|
48
|
+
return CLOSE_CODE_VALUES[reason];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const REVERSE_LOOKUP: ReadonlyMap<number, GraphorinCloseReason> = new Map(
|
|
52
|
+
(Object.entries(CLOSE_CODE_VALUES) as ReadonlyArray<[GraphorinCloseReason, number]>).map(
|
|
53
|
+
([reason, code]) => [code, reason],
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a numeric close code back to its Graphorin reason
|
|
59
|
+
* discriminator. Returns `undefined` for codes outside the
|
|
60
|
+
* Graphorin range so callers can still surface the raw RFC 6455
|
|
61
|
+
* reason for unrelated codes.
|
|
62
|
+
*
|
|
63
|
+
* @stable
|
|
64
|
+
*/
|
|
65
|
+
export function closeCodeReason(code: number): GraphorinCloseReason | undefined {
|
|
66
|
+
return REVERSE_LOOKUP.get(code);
|
|
67
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@graphorin/protocol` - wire-format contract for the Graphorin
|
|
3
|
+
* WebSocket subprotocol.
|
|
4
|
+
*
|
|
5
|
+
* The package is the **single source of truth** for the shape of
|
|
6
|
+
* every frame exchanged over `wss://.../v1/ws`. Both
|
|
7
|
+
* `@graphorin/server` and `@graphorin/client` import their schemas
|
|
8
|
+
* from here so the two implementations cannot drift.
|
|
9
|
+
*
|
|
10
|
+
* Browser-friendly: the only runtime dependency is
|
|
11
|
+
* [`zod`](https://zod.dev). No Node-only imports, no DOM-only
|
|
12
|
+
* imports.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Canonical version constant, derived from `package.json` at build time. */
|
|
18
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
19
|
+
|
|
20
|
+
export const VERSION: string = pkg.version;
|
|
21
|
+
|
|
22
|
+
export * from './client-message.js';
|
|
23
|
+
export * from './close-codes.js';
|
|
24
|
+
export * from './server-message.js';
|
|
25
|
+
export * from './subprotocol.js';
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ServerMessage` - discriminated union of every frame a Graphorin
|
|
3
|
+
* server may push to a client. Three families share the channel:
|
|
4
|
+
*
|
|
5
|
+
* 1. **RPC responses** (`{ jsonrpc, id, result | error }`) -
|
|
6
|
+
* correlate with a previously-issued client request.
|
|
7
|
+
* 2. **Typed push events** (`{ kind: 'event', subject, type,
|
|
8
|
+
* payload, eventId }`) - the streaming-first data plane;
|
|
9
|
+
* consumers ignore unknown `type` strings per the agent-event
|
|
10
|
+
* extensibility convention.
|
|
11
|
+
* 3. **Asynchronous server frames** (`{ kind: 'lifecycle' | 'error'
|
|
12
|
+
* | 'pong' | 'subscribed' | 'unsubscribed' | 'replay-marker' }`)
|
|
13
|
+
* - server-initiated messages that do not correlate with a
|
|
14
|
+
* single client RPC id.
|
|
15
|
+
*
|
|
16
|
+
* Versioning contract (W-109, honest edition): the frame ENVELOPE -
|
|
17
|
+
* the set of `kind`s, the fields of the control frames, and the
|
|
18
|
+
* `v: '1'` literal - is validated strictly (`.strict()`, literal `v`)
|
|
19
|
+
* on BOTH server and client, and evolves only in lockstep with both
|
|
20
|
+
* sides shipping together: there is NO negotiation, and a frame with
|
|
21
|
+
* `v: '2'` is rejected outright. That is the deployment model of the
|
|
22
|
+
* 0.x line. The additive extension points are deliberate and inside
|
|
23
|
+
* the envelope: the `type` string + `payload: z.unknown()` of event
|
|
24
|
+
* frames (consumers ignore unknown `type`s per the agent-event
|
|
25
|
+
* convention), `result: z.unknown()` of RPC responses, and the
|
|
26
|
+
* `capabilities` record of the `initialize` result (which today the
|
|
27
|
+
* shipped client does not consume). If additive envelope evolution is
|
|
28
|
+
* ever promised publicly, that is a separate negotiation feature -
|
|
29
|
+
* do not loosen `.strict()` for it piecemeal.
|
|
30
|
+
*
|
|
31
|
+
* @packageDocumentation
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
|
|
36
|
+
const RpcId = z.union([z.string().min(1), z.number().int()]);
|
|
37
|
+
|
|
38
|
+
const RpcError = z
|
|
39
|
+
.object({
|
|
40
|
+
code: z.number().int(),
|
|
41
|
+
message: z.string().min(1),
|
|
42
|
+
data: z.unknown().optional(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
|
|
46
|
+
const RpcSuccess = z
|
|
47
|
+
.object({
|
|
48
|
+
v: z.literal('1'),
|
|
49
|
+
jsonrpc: z.literal('2.0'),
|
|
50
|
+
id: RpcId,
|
|
51
|
+
result: z.unknown(),
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
54
|
+
|
|
55
|
+
const RpcFailure = z
|
|
56
|
+
.object({
|
|
57
|
+
v: z.literal('1'),
|
|
58
|
+
jsonrpc: z.literal('2.0'),
|
|
59
|
+
id: RpcId,
|
|
60
|
+
error: RpcError,
|
|
61
|
+
})
|
|
62
|
+
.strict();
|
|
63
|
+
|
|
64
|
+
const SubscribedFrame = z
|
|
65
|
+
.object({
|
|
66
|
+
v: z.literal('1'),
|
|
67
|
+
kind: z.literal('subscribed'),
|
|
68
|
+
subscriptionId: z.string().min(1),
|
|
69
|
+
subject: z.string().min(1),
|
|
70
|
+
snapshotEventId: z.string().min(1).optional(),
|
|
71
|
+
})
|
|
72
|
+
.strict();
|
|
73
|
+
|
|
74
|
+
const UnsubscribedFrame = z
|
|
75
|
+
.object({
|
|
76
|
+
v: z.literal('1'),
|
|
77
|
+
kind: z.literal('unsubscribed'),
|
|
78
|
+
subscriptionId: z.string().min(1),
|
|
79
|
+
})
|
|
80
|
+
.strict();
|
|
81
|
+
|
|
82
|
+
// The runtime schema keeps `payload` opaque so this package stays
|
|
83
|
+
// zod-only (no `@graphorin/core` dependency, not even type-only - it
|
|
84
|
+
// would force a resolvable dependency for the d.ts). For agent-run
|
|
85
|
+
// subjects the actual payload shape is the JSON-safe `WireAgentEvent`
|
|
86
|
+
// union documented in `@graphorin/core` (decode with
|
|
87
|
+
// `fromWireAgentEvent`); the cross-package round-trip is pinned by a
|
|
88
|
+
// gate test in core.
|
|
89
|
+
const EventFrame = z
|
|
90
|
+
.object({
|
|
91
|
+
v: z.literal('1'),
|
|
92
|
+
kind: z.literal('event'),
|
|
93
|
+
eventId: z.string().min(1),
|
|
94
|
+
subscriptionId: z.string().min(1),
|
|
95
|
+
subject: z.string().min(1),
|
|
96
|
+
type: z.string().min(1),
|
|
97
|
+
payload: z.unknown(),
|
|
98
|
+
})
|
|
99
|
+
.strict();
|
|
100
|
+
|
|
101
|
+
const LifecycleFrame = z
|
|
102
|
+
.object({
|
|
103
|
+
v: z.literal('1'),
|
|
104
|
+
kind: z.literal('lifecycle'),
|
|
105
|
+
subscriptionId: z.string().min(1),
|
|
106
|
+
status: z.enum(['running', 'paused', 'completed', 'aborted', 'failed']),
|
|
107
|
+
reason: z.string().min(1).optional(),
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
|
|
111
|
+
const ErrorFrame = z
|
|
112
|
+
.object({
|
|
113
|
+
v: z.literal('1'),
|
|
114
|
+
kind: z.literal('error'),
|
|
115
|
+
code: z.string().min(1),
|
|
116
|
+
message: z.string().min(1),
|
|
117
|
+
fatal: z.boolean().optional(),
|
|
118
|
+
subscriptionId: z.string().min(1).optional(),
|
|
119
|
+
data: z.unknown().optional(),
|
|
120
|
+
})
|
|
121
|
+
.strict();
|
|
122
|
+
|
|
123
|
+
const PongFrame = z
|
|
124
|
+
.object({
|
|
125
|
+
v: z.literal('1'),
|
|
126
|
+
kind: z.literal('pong'),
|
|
127
|
+
nonce: z.string().min(1).optional(),
|
|
128
|
+
})
|
|
129
|
+
.strict();
|
|
130
|
+
|
|
131
|
+
const ReplayMarkerFrame = z
|
|
132
|
+
.object({
|
|
133
|
+
v: z.literal('1'),
|
|
134
|
+
kind: z.literal('replay-marker'),
|
|
135
|
+
subscriptionId: z.string().min(1),
|
|
136
|
+
eventId: z.string().min(1),
|
|
137
|
+
droppedCount: z.number().int().nonnegative().optional(),
|
|
138
|
+
note: z.string().min(1).optional(),
|
|
139
|
+
})
|
|
140
|
+
.strict();
|
|
141
|
+
|
|
142
|
+
const KindedFrame = z.discriminatedUnion('kind', [
|
|
143
|
+
SubscribedFrame,
|
|
144
|
+
UnsubscribedFrame,
|
|
145
|
+
EventFrame,
|
|
146
|
+
LifecycleFrame,
|
|
147
|
+
ErrorFrame,
|
|
148
|
+
PongFrame,
|
|
149
|
+
ReplayMarkerFrame,
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
const RpcFrame = z.union([RpcSuccess, RpcFailure]);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Zod schema for every legal server → client frame. Validation runs
|
|
156
|
+
* twice in the server pipeline: first when a route handler enqueues
|
|
157
|
+
* the frame onto the dispatcher's send queue (so a malformed frame
|
|
158
|
+
* never escapes the process), then again on the client side to
|
|
159
|
+
* defend against protocol drift.
|
|
160
|
+
*
|
|
161
|
+
* @stable
|
|
162
|
+
*/
|
|
163
|
+
export const ServerMessageSchema = z.union([RpcFrame, KindedFrame]);
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Inferred TypeScript union for the `ServerMessage` discriminator.
|
|
167
|
+
*
|
|
168
|
+
* @stable
|
|
169
|
+
*/
|
|
170
|
+
export type ServerMessage = z.infer<typeof ServerMessageSchema>;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Convenience type aliases for callers that want to reference an
|
|
174
|
+
* individual variant without `z.infer<typeof X>`.
|
|
175
|
+
*
|
|
176
|
+
* @stable
|
|
177
|
+
*/
|
|
178
|
+
export type ServerEventFrame = z.infer<typeof EventFrame>;
|
|
179
|
+
/** @stable */
|
|
180
|
+
export type ServerLifecycleFrame = z.infer<typeof LifecycleFrame>;
|
|
181
|
+
/** @stable */
|
|
182
|
+
export type ServerErrorFrame = z.infer<typeof ErrorFrame>;
|
|
183
|
+
/** @stable */
|
|
184
|
+
export type ServerSubscribedFrame = z.infer<typeof SubscribedFrame>;
|
|
185
|
+
/** @stable */
|
|
186
|
+
export type ServerUnsubscribedFrame = z.infer<typeof UnsubscribedFrame>;
|
|
187
|
+
/** @stable */
|
|
188
|
+
export type ServerPongFrame = z.infer<typeof PongFrame>;
|
|
189
|
+
/** @stable */
|
|
190
|
+
export type ServerReplayMarkerFrame = z.infer<typeof ReplayMarkerFrame>;
|
|
191
|
+
/** @stable */
|
|
192
|
+
export type ServerRpcSuccess = z.infer<typeof RpcSuccess>;
|
|
193
|
+
/** @stable */
|
|
194
|
+
export type ServerRpcFailure = z.infer<typeof RpcFailure>;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Type guard helpers, one per discriminator. The narrow over the
|
|
198
|
+
* `ServerMessage` union without forcing consumers to memorize the
|
|
199
|
+
* exact field names.
|
|
200
|
+
*
|
|
201
|
+
* @stable
|
|
202
|
+
*/
|
|
203
|
+
export function isEventFrame(message: ServerMessage): message is ServerEventFrame {
|
|
204
|
+
return 'kind' in message && message.kind === 'event';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @stable */
|
|
208
|
+
export function isLifecycleFrame(message: ServerMessage): message is ServerLifecycleFrame {
|
|
209
|
+
return 'kind' in message && message.kind === 'lifecycle';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @stable */
|
|
213
|
+
export function isErrorFrame(message: ServerMessage): message is ServerErrorFrame {
|
|
214
|
+
return 'kind' in message && message.kind === 'error';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** @stable */
|
|
218
|
+
export function isSubscribedFrame(message: ServerMessage): message is ServerSubscribedFrame {
|
|
219
|
+
return 'kind' in message && message.kind === 'subscribed';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** @stable */
|
|
223
|
+
export function isUnsubscribedFrame(message: ServerMessage): message is ServerUnsubscribedFrame {
|
|
224
|
+
return 'kind' in message && message.kind === 'unsubscribed';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** @stable */
|
|
228
|
+
export function isPongFrame(message: ServerMessage): message is ServerPongFrame {
|
|
229
|
+
return 'kind' in message && message.kind === 'pong';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** @stable */
|
|
233
|
+
export function isReplayMarkerFrame(message: ServerMessage): message is ServerReplayMarkerFrame {
|
|
234
|
+
return 'kind' in message && message.kind === 'replay-marker';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** @stable */
|
|
238
|
+
export function isRpcSuccess(message: ServerMessage): message is ServerRpcSuccess {
|
|
239
|
+
return 'jsonrpc' in message && message.jsonrpc === '2.0' && 'result' in message;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @stable */
|
|
243
|
+
export function isRpcFailure(message: ServerMessage): message is ServerRpcFailure {
|
|
244
|
+
return 'jsonrpc' in message && message.jsonrpc === '2.0' && 'error' in message;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Stable JSON-RPC error code catalogue used by the server when
|
|
249
|
+
* surfacing routine failures (per JSON-RPC 2.0 § 5.1 + Graphorin
|
|
250
|
+
* extensions). Application-level errors use codes in the
|
|
251
|
+
* implementation-defined range (`-32000` … `-32099`).
|
|
252
|
+
*
|
|
253
|
+
* @stable
|
|
254
|
+
*/
|
|
255
|
+
export const RPC_ERROR_CODES = Object.freeze({
|
|
256
|
+
PARSE_ERROR: -32700,
|
|
257
|
+
INVALID_REQUEST: -32600,
|
|
258
|
+
METHOD_NOT_FOUND: -32601,
|
|
259
|
+
INVALID_PARAMS: -32602,
|
|
260
|
+
INTERNAL_ERROR: -32603,
|
|
261
|
+
AUTH_REQUIRED: -32001,
|
|
262
|
+
AUTH_INVALID: -32002,
|
|
263
|
+
SCOPE_DENIED: -32003,
|
|
264
|
+
RATE_LIMITED: -32004,
|
|
265
|
+
PROTOCOL_VIOLATION: -32005,
|
|
266
|
+
RUN_NOT_FOUND: -32010,
|
|
267
|
+
SUBSCRIPTION_NOT_FOUND: -32011,
|
|
268
|
+
} as const);
|