@graphorin/server 0.5.0 → 0.6.1

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +9 -9
  3. package/dist/app.d.ts +5 -5
  4. package/dist/app.d.ts.map +1 -1
  5. package/dist/app.js +11 -10
  6. package/dist/app.js.map +1 -1
  7. package/dist/commentary/audit-bridge.d.ts +2 -2
  8. package/dist/commentary/audit-bridge.js +3 -3
  9. package/dist/commentary/audit-bridge.js.map +1 -1
  10. package/dist/commentary/built-in-patterns.js +1 -1
  11. package/dist/commentary/built-in-patterns.js.map +1 -1
  12. package/dist/commentary/sanitizer.js +2 -2
  13. package/dist/commentary/sanitizer.js.map +1 -1
  14. package/dist/commentary/types.d.ts +5 -5
  15. package/dist/consolidator/daemon.d.ts +1 -1
  16. package/dist/consolidator/daemon.js.map +1 -1
  17. package/dist/health/checks.js.map +1 -1
  18. package/dist/health/routes.d.ts +2 -2
  19. package/dist/health/routes.js.map +1 -1
  20. package/dist/index.d.ts +7 -8
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -8
  23. package/dist/index.js.map +1 -1
  24. package/dist/internal/ids.js +1 -1
  25. package/dist/internal/ids.js.map +1 -1
  26. package/dist/lifecycle/pre-bind.js.map +1 -1
  27. package/dist/metrics/catalog.js +1 -1
  28. package/dist/metrics/catalog.js.map +1 -1
  29. package/dist/metrics/registry.d.ts +2 -2
  30. package/dist/metrics/registry.js +1 -1
  31. package/dist/metrics/registry.js.map +1 -1
  32. package/dist/middleware/auth.d.ts +1 -1
  33. package/dist/middleware/auth.js +1 -1
  34. package/dist/middleware/auth.js.map +1 -1
  35. package/dist/middleware/cors.js.map +1 -1
  36. package/dist/middleware/csrf.js.map +1 -1
  37. package/dist/middleware/idempotency.d.ts +1 -1
  38. package/dist/middleware/idempotency.js +36 -23
  39. package/dist/middleware/idempotency.js.map +1 -1
  40. package/dist/middleware/rate-limit.js.map +1 -1
  41. package/dist/middleware/request-state.js.map +1 -1
  42. package/dist/middleware/scope.js.map +1 -1
  43. package/dist/package.js +6 -0
  44. package/dist/package.js.map +1 -0
  45. package/dist/registry/index.d.ts +2 -2
  46. package/dist/registry/index.js +1 -1
  47. package/dist/registry/index.js.map +1 -1
  48. package/dist/replay/routes.js +1 -1
  49. package/dist/replay/routes.js.map +1 -1
  50. package/dist/routes/agents.js +1 -1
  51. package/dist/routes/agents.js.map +1 -1
  52. package/dist/routes/audit.d.ts +1 -1
  53. package/dist/routes/audit.js.map +1 -1
  54. package/dist/routes/auth.js.map +1 -1
  55. package/dist/routes/health.js +1 -1
  56. package/dist/routes/health.js.map +1 -1
  57. package/dist/routes/sessions.js.map +1 -1
  58. package/dist/routes/tokens.js.map +1 -1
  59. package/dist/routes/workflows.js +42 -8
  60. package/dist/routes/workflows.js.map +1 -1
  61. package/dist/runtime/run-state.d.ts +3 -3
  62. package/dist/runtime/run-state.js +3 -3
  63. package/dist/runtime/run-state.js.map +1 -1
  64. package/dist/sse/events.js +1 -1
  65. package/dist/sse/events.js.map +1 -1
  66. package/dist/triggers/daemon.js.map +1 -1
  67. package/dist/triggers/routes.js.map +1 -1
  68. package/dist/ws/dispatcher.js.map +1 -1
  69. package/dist/ws/subjects.d.ts +1 -1
  70. package/dist/ws/subjects.d.ts.map +1 -1
  71. package/dist/ws/subjects.js +3 -3
  72. package/dist/ws/subjects.js.map +1 -1
  73. package/dist/ws/ticket.d.ts +1 -1
  74. package/dist/ws/ticket.js +1 -1
  75. package/dist/ws/ticket.js.map +1 -1
  76. package/dist/ws/upgrade.d.ts.map +1 -1
  77. package/dist/ws/upgrade.js +7 -5
  78. package/dist/ws/upgrade.js.map +1 -1
  79. package/package.json +16 -16
@@ -1 +1 @@
1
- {"version":3,"file":"upgrade.js","names":["unregister: (() => void) | undefined","payload: unknown","frame: ServerMessage","INVOKE_SCOPE: ParsedScope","ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope>"],"sources":["../../src/ws/upgrade.ts"],"sourcesContent":["/**\n * Hono WebSocket upgrade handler. Wires the `@hono/node-ws` adapter\n * to the dispatcher: validates the subprotocol, runs auth (bearer\n * via `requireAuth` middleware OR ticket via the in-memory store),\n * and bridges the per-connection `WSContext` callbacks to the\n * dispatcher's RPC handler.\n *\n * ## Subprotocol + browser ticket flow\n *\n * The negotiated subprotocol is always {@link SUBPROTOCOL_NAME}\n * (`graphorin.protocol.v1`) — the canonical wire contract lives in\n * `@graphorin/protocol`'s `subprotocol.ts`. Two auth paths exist:\n *\n * - **Bearer (non-browser):** the client sends `Authorization:\n * Bearer <token>` and a single `Sec-WebSocket-Protocol:\n * graphorin.protocol.v1` token.\n * - **Ticket (browser):** the `WebSocket` constructor cannot set\n * headers, so the browser client offers two subprotocol tokens —\n * `graphorin.protocol.v1` and `ticket.<value>`. The server echoes\n * back only the canonical name (in `app.ts`'s `handleProtocols`),\n * and {@link resolveUpgradeAuth} extracts the ticket via\n * `parseTicketSubprotocol` and redeems it against the single-use\n * {@link WsTicketStore}. Tickets are short-lived and one-shot to\n * bound replay.\n *\n * @packageDocumentation\n */\n\nimport {\n ClientMessageSchema,\n closeCodeFor,\n isCancelledNotification,\n isInitializeRequest,\n isPingRequest,\n isRunCancelRequest,\n isSubscribeRequest,\n isUnsubscribeRequest,\n negotiateSubprotocol,\n parseTicketSubprotocol,\n RPC_ERROR_CODES,\n type ServerMessage,\n SUBPROTOCOL_NAME,\n} from '@graphorin/protocol';\nimport type { ParsedScope, TokenVerifier } from '@graphorin/security/auth';\nimport { parseScope, scopeMatches } from '@graphorin/security/auth';\nimport type { Context } from 'hono';\nimport type { WSContext, WSEvents } from 'hono/ws';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\nimport type { WsDispatcher } from './dispatcher.js';\nimport type { WsTicket, WsTicketStore } from './ticket.js';\n\n/**\n * Public configuration accepted by {@link createWsUpgradeEvents}.\n *\n * @stable\n */\nexport interface WsUpgradeOptions {\n readonly dispatcher: WsDispatcher;\n readonly tickets: WsTicketStore;\n /**\n * Token verifier for bearer / ticket upgrades. Optional only in the\n * IP-13 no-auth loopback mode (`anonymous: true`), where there is no\n * verifier to construct and every upgrade is accepted.\n */\n readonly verifier?: TokenVerifier;\n /**\n * IP-13: authentication is disabled server-wide (`auth.kind='none'`).\n * Accept the upgrade unconditionally with a full (`admin:*`) scope grant\n * instead of silently refusing to mount the WS route. Trusted-loopback\n * / single-operator mode only.\n */\n readonly anonymous?: boolean;\n readonly runs?: RunStateTracker;\n readonly now?: () => number;\n /**\n * Subprotocol the server advertises. Defaults to\n * {@link SUBPROTOCOL_NAME}; tests can override to exercise the\n * mismatch path.\n */\n readonly serverSubprotocol?: string;\n readonly newSubscriptionId?: () => string;\n readonly newSubscriberId?: () => string;\n}\n\n/**\n * Build the `WSEvents` callback bag the Hono helper consumes. The\n * function takes the request `Context` so the upgrade can read the\n * `Authorization` header / `Sec-WebSocket-Protocol` ticket directly.\n *\n * Production wiring on `@hono/node-ws`:\n *\n * ```ts\n * const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });\n * app.get('/v1/ws', upgradeWebSocket((c) => createWsUpgradeEvents(c, deps)));\n * injectWebSocket(serve(...));\n * ```\n *\n * @stable\n */\nexport async function createWsUpgradeEvents(\n c: Context<{ Variables: ServerVariables }>,\n options: WsUpgradeOptions,\n): Promise<WSEvents> {\n const negotiated = negotiateSubprotocol(c.req.header('sec-websocket-protocol') ?? '');\n if (negotiated === null) {\n return rejectImmediately(closeCodeFor('protocol.violation'), 'protocol.violation');\n }\n const auth = await resolveUpgradeAuth(c, options);\n if (auth.kind === 'denied') {\n return rejectImmediately(closeCodeFor(auth.reason), auth.reason);\n }\n\n const subscriberId = options.newSubscriberId?.() ?? defaultSubscriberId();\n const newSubscriptionId =\n options.newSubscriptionId ?? (() => defaultSubscriptionId(subscriberId));\n\n let unregister: (() => void) | undefined;\n let initialized = false;\n\n return {\n onOpen: (_event, ws) => {\n const handle = {\n id: subscriberId,\n tokenId: auth.tokenId,\n grantedScopes: auth.grantedScopes,\n send: (frame: ServerMessage) => {\n ws.send(JSON.stringify(frame));\n },\n close: (code: number, reason: string) => {\n ws.close(code, reason);\n },\n // IP-9: surface the socket's real backlog so the dispatcher's\n // backpressure threshold measures something — the synchronous\n // outstanding-events counter never accumulates.\n bufferedAmount: () => {\n const raw = (ws as { raw?: { bufferedAmount?: number } }).raw;\n return raw?.bufferedAmount ?? 0;\n },\n };\n const reg = options.dispatcher.registerSubscriber(handle);\n unregister = reg.unregister;\n },\n onMessage: (event, ws) => {\n const raw = typeof event.data === 'string' ? event.data : '';\n let payload: unknown;\n try {\n payload = JSON.parse(raw);\n } catch {\n sendRpcError(ws, undefined, RPC_ERROR_CODES.PARSE_ERROR, 'Frame is not valid JSON.');\n ws.close(closeCodeFor('protocol.violation'), 'protocol.violation');\n return;\n }\n const parsed = ClientMessageSchema.safeParse(payload);\n if (!parsed.success) {\n sendRpcError(\n ws,\n (payload as { id?: string | number } | null)?.id,\n RPC_ERROR_CODES.INVALID_REQUEST,\n 'Frame failed schema validation.',\n );\n ws.close(closeCodeFor('protocol.violation'), 'protocol.violation');\n return;\n }\n const message = parsed.data;\n if (isCancelledNotification(message)) {\n // IP-8: only an initialized connection holding `agents:invoke` may\n // cancel a run. A notification carries no id, so an unauthorised one\n // is silently ignored (there is no error channel) rather than aborting\n // an arbitrary run.\n if (initialized && grantsInvokeScope(auth.grantedScopes)) {\n options.runs?.abort(message.params.requestId, 'mcp-cancel');\n }\n return;\n }\n if (!initialized && !isInitializeRequest(message) && !isPingRequest(message)) {\n // IP-21: a frame before `initialize` is a protocol-sequencing error,\n // not an auth failure — the connection is already authenticated.\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.PROTOCOL_VIOLATION,\n 'Send `initialize` before any other RPC.',\n );\n return;\n }\n if (isInitializeRequest(message)) {\n initialized = true;\n sendRpcSuccess(ws, message.id, {\n serverInfo: {\n name: 'graphorin-server',\n version: '0.5.0',\n },\n capabilities: {\n subscriptions: true,\n replayBuffer: true,\n sseFallback: true,\n },\n });\n return;\n }\n if (isPingRequest(message)) {\n const nonce = message.params?.nonce;\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'pong',\n ...(nonce !== undefined ? { nonce } : {}),\n } satisfies ServerMessage),\n );\n sendRpcSuccess(ws, message.id, { ok: true });\n return;\n }\n if (isSubscribeRequest(message)) {\n const subscriptionId = newSubscriptionId();\n const result = options.dispatcher.subscribe({\n subscriberId,\n subject: message.params.subject,\n subscriptionId,\n ...(message.params.sinceEventId !== undefined\n ? { sinceEventId: message.params.sinceEventId }\n : {}),\n });\n if (!result.ok) {\n sendRpcError(\n ws,\n message.id,\n mapSubscribeErrorCode(result.reason),\n mapSubscribeErrorMessage(result.reason, message.params.subject),\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n subscriptionId,\n subject: message.params.subject,\n snapshotEventId: result.snapshotEventId,\n replayedCount: result.replayedCount,\n });\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'subscribed',\n subscriptionId,\n subject: message.params.subject,\n ...(result.snapshotEventId !== undefined\n ? { snapshotEventId: result.snapshotEventId }\n : {}),\n } satisfies ServerMessage),\n );\n return;\n }\n if (isUnsubscribeRequest(message)) {\n const removed = options.dispatcher.unsubscribe(message.params.subscriptionId);\n if (!removed) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,\n `Subscription '${message.params.subscriptionId}' not active.`,\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n subscriptionId: message.params.subscriptionId,\n unsubscribed: true,\n });\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'unsubscribed',\n subscriptionId: message.params.subscriptionId,\n } satisfies ServerMessage),\n );\n return;\n }\n if (isRunCancelRequest(message)) {\n // IP-8: mirror the REST `POST /runs/:runId/abort` scope gate so a\n // bearer token cannot abort an arbitrary run without `agents:invoke`.\n if (!grantsInvokeScope(auth.grantedScopes)) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SCOPE_DENIED,\n \"Token lacks required scope 'agents:invoke'.\",\n );\n return;\n }\n const runId = message.params.runId;\n const aborted = options.runs?.abort(runId, message.params.reason ?? 'rpc-cancel') === true;\n if (!aborted) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.RUN_NOT_FOUND,\n `Run '${runId}' is not active.`,\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n runId,\n cancelled: true,\n partialStateAvailable: true,\n });\n return;\n }\n },\n onClose: () => {\n try {\n unregister?.();\n } finally {\n unregister = undefined;\n }\n },\n onError: () => {\n try {\n unregister?.();\n } finally {\n unregister = undefined;\n }\n },\n };\n}\n\nfunction sendRpcSuccess(ws: WSContext, id: string | number, result: unknown): void {\n const frame: ServerMessage = { v: '1', jsonrpc: '2.0', id, result };\n ws.send(JSON.stringify(frame));\n}\n\nfunction sendRpcError(\n ws: WSContext,\n id: string | number | undefined,\n code: number,\n message: string,\n): void {\n const frame: ServerMessage = {\n v: '1',\n jsonrpc: '2.0',\n id: id ?? 0,\n error: { code, message },\n };\n ws.send(JSON.stringify(frame));\n}\n\nfunction rejectImmediately(code: number, reason: string): WSEvents {\n return {\n onOpen: (_event, ws) => {\n ws.close(code, reason);\n },\n };\n}\n\ninterface ResolvedUpgradeAuthOk {\n readonly kind: 'ok';\n readonly tokenId: string;\n readonly grantedScopes: ReadonlyArray<ParsedScope>;\n}\n\ninterface ResolvedUpgradeAuthDenied {\n readonly kind: 'denied';\n readonly reason: 'auth.required' | 'auth.invalid' | 'auth.scope_denied';\n}\n\ntype ResolvedUpgradeAuth = ResolvedUpgradeAuthOk | ResolvedUpgradeAuthDenied;\n\nasync function resolveUpgradeAuth(\n c: Context<{ Variables: ServerVariables }>,\n options: WsUpgradeOptions,\n): Promise<ResolvedUpgradeAuth> {\n // Phase 14a's HTTP middleware may have already verified the\n // bearer token (`requireAuth` runs before the upgrade route on\n // the authenticated subtree); if so, reuse the result.\n const state = c.var.state;\n if (state?.auth?.kind === 'token') {\n return {\n kind: 'ok',\n tokenId: state.auth.token.tokenId,\n grantedScopes: state.auth.grantedScopes,\n };\n }\n // IP-13: no-auth loopback mode — accept the upgrade with a full scope grant\n // instead of silently failing to mount. Checked before ticket/bearer so the\n // verifier may legitimately be absent.\n if (options.anonymous === true) {\n return { kind: 'ok', tokenId: 'anonymous', grantedScopes: ANONYMOUS_WS_SCOPES };\n }\n const ticketValue = parseTicketSubprotocol(c.req.header('sec-websocket-protocol') ?? '');\n if (ticketValue !== undefined) {\n const consumed = options.tickets.consume(ticketValue);\n if (!consumed.ok) {\n return { kind: 'denied', reason: 'auth.invalid' };\n }\n return acceptTicket(consumed.ticket);\n }\n const header = c.req.header('authorization') ?? c.req.header('Authorization');\n if (header?.toLowerCase().startsWith('bearer ') === true) {\n if (options.verifier === undefined) return { kind: 'denied', reason: 'auth.required' };\n const token = header.slice(7).trim();\n const verified = await options.verifier.verify(token);\n if (!verified.ok) return { kind: 'denied', reason: 'auth.invalid' };\n return {\n kind: 'ok',\n tokenId: verified.token.tokenId,\n grantedScopes: verified.token.scopes,\n };\n }\n return { kind: 'denied', reason: 'auth.required' };\n}\n\n/**\n * IP-8: the `agents:invoke` scope required to cancel a run over the WS\n * transport — the same requirement the REST `POST /runs/:runId/abort` route\n * enforces. `scopeMatches` honours wildcards (`agents:*`, `admin:*`).\n */\nconst INVOKE_SCOPE: ParsedScope = parseScope('agents:invoke');\n\n/**\n * IP-13: full scope grant handed to a no-auth (`auth.kind='none'`) upgrade.\n * `admin:*` matches every required scope, including the {@link INVOKE_SCOPE}\n * gate on run cancellation.\n */\nconst ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope> = [parseScope('admin:*')];\n\nfunction grantsInvokeScope(granted: ReadonlyArray<ParsedScope>): boolean {\n for (const scope of granted) {\n if (scopeMatches(scope, INVOKE_SCOPE)) return true;\n }\n return false;\n}\n\nfunction acceptTicket(ticket: WsTicket): ResolvedUpgradeAuth {\n return {\n kind: 'ok',\n tokenId: ticket.tokenId,\n grantedScopes: ticket.scopes,\n };\n}\n\nfunction mapSubscribeErrorCode(\n reason: 'subject-malformed' | 'subject-unknown' | 'subject-wildcard' | 'scope-denied',\n): number {\n switch (reason) {\n case 'scope-denied':\n return RPC_ERROR_CODES.SCOPE_DENIED;\n case 'subject-wildcard':\n return RPC_ERROR_CODES.INVALID_PARAMS;\n case 'subject-unknown':\n return RPC_ERROR_CODES.METHOD_NOT_FOUND;\n case 'subject-malformed':\n return RPC_ERROR_CODES.INVALID_PARAMS;\n }\n}\n\nfunction mapSubscribeErrorMessage(\n reason: 'subject-malformed' | 'subject-unknown' | 'subject-wildcard' | 'scope-denied',\n subject: string,\n): string {\n switch (reason) {\n case 'scope-denied':\n return `Token lacks required scope for subject '${subject}'.`;\n case 'subject-wildcard':\n return `Wildcard subjects are not supported in this version. Subject: '${subject}'.`;\n case 'subject-unknown':\n return `Unknown subject grammar: '${subject}'.`;\n case 'subject-malformed':\n return `Subject '${subject}' is malformed.`;\n }\n}\n\nfunction defaultSubscriberId(): string {\n return `sub-conn-${randomToken(8)}`;\n}\n\nfunction defaultSubscriptionId(subscriberId: string): string {\n return `${subscriberId}-${randomToken(6)}`;\n}\n\nfunction randomToken(length: number): string {\n const bytes = new Uint8Array(length);\n if (\n typeof globalThis.crypto !== 'undefined' &&\n typeof globalThis.crypto.getRandomValues === 'function'\n ) {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < length; i += 1) {\n bytes[i] = Math.floor(Math.random() * 256);\n }\n }\n return Buffer.from(bytes).toString('hex');\n}\n\nvoid SUBPROTOCOL_NAME;\n"],"mappings":"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"}
1
+ {"version":3,"file":"upgrade.js","names":["unregister: (() => void) | undefined","payload: unknown","pkg.version","frame: ServerMessage","INVOKE_SCOPE: ParsedScope","ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope>"],"sources":["../../src/ws/upgrade.ts"],"sourcesContent":["import pkg from '../../package.json' with { type: 'json' };\n/**\n * Hono WebSocket upgrade handler. Wires the `@hono/node-ws` adapter\n * to the dispatcher: validates the subprotocol, runs auth (bearer\n * via `requireAuth` middleware OR ticket via the in-memory store),\n * and bridges the per-connection `WSContext` callbacks to the\n * dispatcher's RPC handler.\n *\n * ## Subprotocol + browser ticket flow\n *\n * The negotiated subprotocol is always {@link SUBPROTOCOL_NAME}\n * (`graphorin.protocol.v1`) - the canonical wire contract lives in\n * `@graphorin/protocol`'s `subprotocol.ts`. Two auth paths exist:\n *\n * - **Bearer (non-browser):** the client sends `Authorization:\n * Bearer <token>` and a single `Sec-WebSocket-Protocol:\n * graphorin.protocol.v1` token.\n * - **Ticket (browser):** the `WebSocket` constructor cannot set\n * headers, so the browser client offers two subprotocol tokens -\n * `graphorin.protocol.v1` and `ticket.<value>`. The server echoes\n * back only the canonical name (in `app.ts`'s `handleProtocols`),\n * and {@link resolveUpgradeAuth} extracts the ticket via\n * `parseTicketSubprotocol` and redeems it against the single-use\n * {@link WsTicketStore}. Tickets are short-lived and one-shot to\n * bound replay.\n *\n * @packageDocumentation\n */\n\nimport {\n ClientMessageSchema,\n closeCodeFor,\n isCancelledNotification,\n isInitializeRequest,\n isPingRequest,\n isRunCancelRequest,\n isSubscribeRequest,\n isUnsubscribeRequest,\n negotiateSubprotocol,\n parseTicketSubprotocol,\n RPC_ERROR_CODES,\n type ServerMessage,\n SUBPROTOCOL_NAME,\n} from '@graphorin/protocol';\nimport type { ParsedScope, TokenVerifier } from '@graphorin/security/auth';\nimport { parseScope, scopeMatches } from '@graphorin/security/auth';\nimport type { Context } from 'hono';\nimport type { WSContext, WSEvents } from 'hono/ws';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\nimport type { WsDispatcher } from './dispatcher.js';\nimport type { WsTicket, WsTicketStore } from './ticket.js';\n\n/**\n * Public configuration accepted by {@link createWsUpgradeEvents}.\n *\n * @stable\n */\nexport interface WsUpgradeOptions {\n readonly dispatcher: WsDispatcher;\n readonly tickets: WsTicketStore;\n /**\n * Token verifier for bearer / ticket upgrades. Optional only in the\n * IP-13 no-auth loopback mode (`anonymous: true`), where there is no\n * verifier to construct and every upgrade is accepted.\n */\n readonly verifier?: TokenVerifier;\n /**\n * IP-13: authentication is disabled server-wide (`auth.kind='none'`).\n * Accept the upgrade unconditionally with a full (`admin:*`) scope grant\n * instead of silently refusing to mount the WS route. Trusted-loopback\n * / single-operator mode only.\n */\n readonly anonymous?: boolean;\n readonly runs?: RunStateTracker;\n readonly now?: () => number;\n /**\n * Subprotocol the server advertises. Defaults to\n * {@link SUBPROTOCOL_NAME}; tests can override to exercise the\n * mismatch path.\n */\n readonly serverSubprotocol?: string;\n readonly newSubscriptionId?: () => string;\n readonly newSubscriberId?: () => string;\n}\n\n/**\n * Build the `WSEvents` callback bag the Hono helper consumes. The\n * function takes the request `Context` so the upgrade can read the\n * `Authorization` header / `Sec-WebSocket-Protocol` ticket directly.\n *\n * Production wiring on `@hono/node-ws`:\n *\n * ```ts\n * const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });\n * app.get('/v1/ws', upgradeWebSocket((c) => createWsUpgradeEvents(c, deps)));\n * injectWebSocket(serve(...));\n * ```\n *\n * @stable\n */\nexport async function createWsUpgradeEvents(\n c: Context<{ Variables: ServerVariables }>,\n options: WsUpgradeOptions,\n): Promise<WSEvents> {\n const negotiated = negotiateSubprotocol(c.req.header('sec-websocket-protocol') ?? '');\n if (negotiated === null) {\n return rejectImmediately(closeCodeFor('protocol.violation'), 'protocol.violation');\n }\n const auth = await resolveUpgradeAuth(c, options);\n if (auth.kind === 'denied') {\n return rejectImmediately(closeCodeFor(auth.reason), auth.reason);\n }\n\n const subscriberId = options.newSubscriberId?.() ?? defaultSubscriberId();\n const newSubscriptionId =\n options.newSubscriptionId ?? (() => defaultSubscriptionId(subscriberId));\n\n let unregister: (() => void) | undefined;\n let initialized = false;\n\n return {\n onOpen: (_event, ws) => {\n const handle = {\n id: subscriberId,\n tokenId: auth.tokenId,\n grantedScopes: auth.grantedScopes,\n send: (frame: ServerMessage) => {\n ws.send(JSON.stringify(frame));\n },\n close: (code: number, reason: string) => {\n ws.close(code, reason);\n },\n // IP-9: surface the socket's real backlog so the dispatcher's\n // backpressure threshold measures something - the synchronous\n // outstanding-events counter never accumulates.\n bufferedAmount: () => {\n const raw = (ws as { raw?: { bufferedAmount?: number } }).raw;\n return raw?.bufferedAmount ?? 0;\n },\n };\n const reg = options.dispatcher.registerSubscriber(handle);\n unregister = reg.unregister;\n },\n onMessage: (event, ws) => {\n const raw = typeof event.data === 'string' ? event.data : '';\n let payload: unknown;\n try {\n payload = JSON.parse(raw);\n } catch {\n sendRpcError(ws, undefined, RPC_ERROR_CODES.PARSE_ERROR, 'Frame is not valid JSON.');\n ws.close(closeCodeFor('protocol.violation'), 'protocol.violation');\n return;\n }\n const parsed = ClientMessageSchema.safeParse(payload);\n if (!parsed.success) {\n sendRpcError(\n ws,\n (payload as { id?: string | number } | null)?.id,\n RPC_ERROR_CODES.INVALID_REQUEST,\n 'Frame failed schema validation.',\n );\n ws.close(closeCodeFor('protocol.violation'), 'protocol.violation');\n return;\n }\n const message = parsed.data;\n if (isCancelledNotification(message)) {\n // IP-8: only an initialized connection holding `agents:invoke` may\n // cancel a run. A notification carries no id, so an unauthorised one\n // is silently ignored (there is no error channel) rather than aborting\n // an arbitrary run.\n if (initialized && grantsInvokeScope(auth.grantedScopes)) {\n options.runs?.abort(message.params.requestId, 'mcp-cancel');\n }\n return;\n }\n if (!initialized && !isInitializeRequest(message) && !isPingRequest(message)) {\n // IP-21: a frame before `initialize` is a protocol-sequencing error,\n // not an auth failure - the connection is already authenticated.\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.PROTOCOL_VIOLATION,\n 'Send `initialize` before any other RPC.',\n );\n return;\n }\n if (isInitializeRequest(message)) {\n initialized = true;\n sendRpcSuccess(ws, message.id, {\n serverInfo: {\n name: 'graphorin-server',\n version: pkg.version,\n },\n capabilities: {\n subscriptions: true,\n replayBuffer: true,\n sseFallback: true,\n },\n });\n return;\n }\n if (isPingRequest(message)) {\n const nonce = message.params?.nonce;\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'pong',\n ...(nonce !== undefined ? { nonce } : {}),\n } satisfies ServerMessage),\n );\n sendRpcSuccess(ws, message.id, { ok: true });\n return;\n }\n if (isSubscribeRequest(message)) {\n const subscriptionId = newSubscriptionId();\n const result = options.dispatcher.subscribe({\n subscriberId,\n subject: message.params.subject,\n subscriptionId,\n ...(message.params.sinceEventId !== undefined\n ? { sinceEventId: message.params.sinceEventId }\n : {}),\n });\n if (!result.ok) {\n sendRpcError(\n ws,\n message.id,\n mapSubscribeErrorCode(result.reason),\n mapSubscribeErrorMessage(result.reason, message.params.subject),\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n subscriptionId,\n subject: message.params.subject,\n snapshotEventId: result.snapshotEventId,\n replayedCount: result.replayedCount,\n });\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'subscribed',\n subscriptionId,\n subject: message.params.subject,\n ...(result.snapshotEventId !== undefined\n ? { snapshotEventId: result.snapshotEventId }\n : {}),\n } satisfies ServerMessage),\n );\n return;\n }\n if (isUnsubscribeRequest(message)) {\n const removed = options.dispatcher.unsubscribe(message.params.subscriptionId);\n if (!removed) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,\n `Subscription '${message.params.subscriptionId}' not active.`,\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n subscriptionId: message.params.subscriptionId,\n unsubscribed: true,\n });\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'unsubscribed',\n subscriptionId: message.params.subscriptionId,\n } satisfies ServerMessage),\n );\n return;\n }\n if (isRunCancelRequest(message)) {\n // IP-8: mirror the REST `POST /runs/:runId/abort` scope gate so a\n // bearer token cannot abort an arbitrary run without `agents:invoke`.\n if (!grantsInvokeScope(auth.grantedScopes)) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SCOPE_DENIED,\n \"Token lacks required scope 'agents:invoke'.\",\n );\n return;\n }\n const runId = message.params.runId;\n const aborted = options.runs?.abort(runId, message.params.reason ?? 'rpc-cancel') === true;\n if (!aborted) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.RUN_NOT_FOUND,\n `Run '${runId}' is not active.`,\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n runId,\n cancelled: true,\n partialStateAvailable: true,\n });\n return;\n }\n },\n onClose: () => {\n try {\n unregister?.();\n } finally {\n unregister = undefined;\n }\n },\n onError: () => {\n try {\n unregister?.();\n } finally {\n unregister = undefined;\n }\n },\n };\n}\n\nfunction sendRpcSuccess(ws: WSContext, id: string | number, result: unknown): void {\n const frame: ServerMessage = { v: '1', jsonrpc: '2.0', id, result };\n ws.send(JSON.stringify(frame));\n}\n\nfunction sendRpcError(\n ws: WSContext,\n id: string | number | undefined,\n code: number,\n message: string,\n): void {\n const frame: ServerMessage = {\n v: '1',\n jsonrpc: '2.0',\n id: id ?? 0,\n error: { code, message },\n };\n ws.send(JSON.stringify(frame));\n}\n\nfunction rejectImmediately(code: number, reason: string): WSEvents {\n return {\n onOpen: (_event, ws) => {\n ws.close(code, reason);\n },\n };\n}\n\ninterface ResolvedUpgradeAuthOk {\n readonly kind: 'ok';\n readonly tokenId: string;\n readonly grantedScopes: ReadonlyArray<ParsedScope>;\n}\n\ninterface ResolvedUpgradeAuthDenied {\n readonly kind: 'denied';\n readonly reason: 'auth.required' | 'auth.invalid' | 'auth.scope_denied';\n}\n\ntype ResolvedUpgradeAuth = ResolvedUpgradeAuthOk | ResolvedUpgradeAuthDenied;\n\nasync function resolveUpgradeAuth(\n c: Context<{ Variables: ServerVariables }>,\n options: WsUpgradeOptions,\n): Promise<ResolvedUpgradeAuth> {\n // Phase 14a's HTTP middleware may have already verified the\n // bearer token (`requireAuth` runs before the upgrade route on\n // the authenticated subtree); if so, reuse the result.\n const state = c.var.state;\n if (state?.auth?.kind === 'token') {\n return {\n kind: 'ok',\n tokenId: state.auth.token.tokenId,\n grantedScopes: state.auth.grantedScopes,\n };\n }\n // IP-13: no-auth loopback mode - accept the upgrade with a full scope grant\n // instead of silently failing to mount. Checked before ticket/bearer so the\n // verifier may legitimately be absent.\n if (options.anonymous === true) {\n return { kind: 'ok', tokenId: 'anonymous', grantedScopes: ANONYMOUS_WS_SCOPES };\n }\n const ticketValue = parseTicketSubprotocol(c.req.header('sec-websocket-protocol') ?? '');\n if (ticketValue !== undefined) {\n const consumed = options.tickets.consume(ticketValue);\n if (!consumed.ok) {\n return { kind: 'denied', reason: 'auth.invalid' };\n }\n return acceptTicket(consumed.ticket);\n }\n const header = c.req.header('authorization') ?? c.req.header('Authorization');\n if (header?.toLowerCase().startsWith('bearer ') === true) {\n if (options.verifier === undefined) return { kind: 'denied', reason: 'auth.required' };\n const token = header.slice(7).trim();\n // P-05: pass the client IP like the HTTP auth middleware does, so\n // the verifier's per-IP failure threshold / lockout engages for\n // upgrade attempts too - without it `GET /v1/ws` was a\n // lockout-free brute-force surface.\n const ip = c.get('state')?.clientIp;\n const verified = await options.verifier.verify(token, ip !== undefined ? { ip } : {});\n if (!verified.ok) return { kind: 'denied', reason: 'auth.invalid' };\n return {\n kind: 'ok',\n tokenId: verified.token.tokenId,\n grantedScopes: verified.token.scopes,\n };\n }\n return { kind: 'denied', reason: 'auth.required' };\n}\n\n/**\n * IP-8: the `agents:invoke` scope required to cancel a run over the WS\n * transport - the same requirement the REST `POST /runs/:runId/abort` route\n * enforces. `scopeMatches` honours wildcards (`agents:*`, `admin:*`).\n */\nconst INVOKE_SCOPE: ParsedScope = parseScope('agents:invoke');\n\n/**\n * IP-13: full scope grant handed to a no-auth (`auth.kind='none'`) upgrade.\n * `admin:*` matches every required scope, including the {@link INVOKE_SCOPE}\n * gate on run cancellation.\n */\nconst ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope> = [parseScope('admin:*')];\n\nfunction grantsInvokeScope(granted: ReadonlyArray<ParsedScope>): boolean {\n for (const scope of granted) {\n if (scopeMatches(scope, INVOKE_SCOPE)) return true;\n }\n return false;\n}\n\nfunction acceptTicket(ticket: WsTicket): ResolvedUpgradeAuth {\n return {\n kind: 'ok',\n tokenId: ticket.tokenId,\n grantedScopes: ticket.scopes,\n };\n}\n\nfunction mapSubscribeErrorCode(\n reason: 'subject-malformed' | 'subject-unknown' | 'subject-wildcard' | 'scope-denied',\n): number {\n switch (reason) {\n case 'scope-denied':\n return RPC_ERROR_CODES.SCOPE_DENIED;\n case 'subject-wildcard':\n return RPC_ERROR_CODES.INVALID_PARAMS;\n case 'subject-unknown':\n return RPC_ERROR_CODES.METHOD_NOT_FOUND;\n case 'subject-malformed':\n return RPC_ERROR_CODES.INVALID_PARAMS;\n }\n}\n\nfunction mapSubscribeErrorMessage(\n reason: 'subject-malformed' | 'subject-unknown' | 'subject-wildcard' | 'scope-denied',\n subject: string,\n): string {\n switch (reason) {\n case 'scope-denied':\n return `Token lacks required scope for subject '${subject}'.`;\n case 'subject-wildcard':\n return `Wildcard subjects are not supported in this version. Subject: '${subject}'.`;\n case 'subject-unknown':\n return `Unknown subject grammar: '${subject}'.`;\n case 'subject-malformed':\n return `Subject '${subject}' is malformed.`;\n }\n}\n\nfunction defaultSubscriberId(): string {\n return `sub-conn-${randomToken(8)}`;\n}\n\nfunction defaultSubscriptionId(subscriberId: string): string {\n return `${subscriberId}-${randomToken(6)}`;\n}\n\nfunction randomToken(length: number): string {\n const bytes = new Uint8Array(length);\n if (\n typeof globalThis.crypto !== 'undefined' &&\n typeof globalThis.crypto.getRandomValues === 'function'\n ) {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < length; i += 1) {\n bytes[i] = Math.floor(Math.random() * 256);\n }\n }\n return Buffer.from(bytes).toString('hex');\n}\n\nvoid SUBPROTOCOL_NAME;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphorin/server",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Standalone server runtime for the Graphorin framework: createServer({...}) factory, Hono HTTP app, REST endpoints (agents / workflows / sessions / memory / skills / mcp / tokens / audit / health / health/secrets / metrics / triggers / runs/:runId/replay / sessions/:id/replay / audit/verify), per-request authentication + scope middleware (HMAC-SHA256 + pepper, server pepper required), brute-force lockout, IETF draft-07 Idempotency-Key middleware with SQLite backing + LRU read cache, deny-by-default CORS, double-submit CSRF for browser flows, sliding-window rate limit, audit middleware that streams every authenticated request through the tamper-evident hash chain, programmatic AgentRegistry / WorkflowRegistry, lifecycle hooks (beforeStart / onReady / beforeShutdown), pre-bind secrets resolution + storage migration runner that fails fast on missing pepper / unresolvable SecretRef / missing encryption peer, graceful SIGTERM drain with state preservation, durable triggers daemon (cron / interval / idle / event survive process restart per the catch-up policy contract), consolidator daemon lifecycle integration with hard stop timeout, per-subsystem health rollup (storage / embedder / secrets / encryption / consolidator / triggers / replay-buffer), Prometheus text exposition `/v1/metrics` with the canonical graphorin_* metric inventory, scope-enforced replay endpoints (sanitized by default, raw requires `traces:read:raw` admin scope, every invocation appended to the audit chain), and `POST /v1/audit/verify` chain integrity check. Created and maintained by Oleksiy Stepurenko.",
5
5
  "license": "MIT",
6
6
  "author": "Oleksiy Stepurenko",
@@ -103,20 +103,20 @@
103
103
  "dependencies": {
104
104
  "@hono/node-server": "^1.19.0",
105
105
  "@hono/node-ws": "^1.3.0",
106
- "hono": "^4.12.21",
106
+ "hono": "^4.12.25",
107
107
  "zod": "^3.25.0",
108
- "@graphorin/core": "0.5.0",
109
- "@graphorin/observability": "0.5.0",
110
- "@graphorin/protocol": "0.5.0",
111
- "@graphorin/security": "0.5.0",
112
- "@graphorin/store-sqlite": "0.5.0",
113
- "@graphorin/triggers": "0.5.0"
108
+ "@graphorin/core": "0.6.1",
109
+ "@graphorin/observability": "0.6.1",
110
+ "@graphorin/protocol": "0.6.1",
111
+ "@graphorin/security": "0.6.1",
112
+ "@graphorin/store-sqlite": "0.6.1",
113
+ "@graphorin/triggers": "0.6.1"
114
114
  },
115
115
  "peerDependencies": {
116
- "@graphorin/agent": "0.5.0",
117
- "@graphorin/memory": "0.5.0",
118
- "@graphorin/sessions": "0.5.0",
119
- "@graphorin/workflow": "0.5.0"
116
+ "@graphorin/agent": ">=0.5.0 <1.0.0",
117
+ "@graphorin/memory": ">=0.5.0 <1.0.0",
118
+ "@graphorin/sessions": ">=0.5.0 <1.0.0",
119
+ "@graphorin/workflow": ">=0.5.0 <1.0.0"
120
120
  },
121
121
  "peerDependenciesMeta": {
122
122
  "@graphorin/agent": {
@@ -141,10 +141,10 @@
141
141
  "@types/ws": "^8.18.1",
142
142
  "better-sqlite3": "^12.9.0",
143
143
  "ws": "^8.20.1",
144
- "@graphorin/agent": "0.5.0",
145
- "@graphorin/memory": "0.5.0",
146
- "@graphorin/sessions": "0.5.0",
147
- "@graphorin/workflow": "0.5.0"
144
+ "@graphorin/agent": "0.6.1",
145
+ "@graphorin/memory": "0.6.1",
146
+ "@graphorin/sessions": "0.6.1",
147
+ "@graphorin/workflow": "0.6.1"
148
148
  },
149
149
  "scripts": {
150
150
  "build": "tsdown",