@graphorin/server 0.6.1 → 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.
Files changed (181) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +3 -3
  3. package/dist/app-audit-binding.d.ts +15 -0
  4. package/dist/app-audit-binding.d.ts.map +1 -0
  5. package/dist/app-audit-binding.js +136 -0
  6. package/dist/app-audit-binding.js.map +1 -0
  7. package/dist/app-daemons.d.ts +29 -0
  8. package/dist/app-daemons.d.ts.map +1 -0
  9. package/dist/app-daemons.js +27 -0
  10. package/dist/app-daemons.js.map +1 -0
  11. package/dist/app-lifecycle.js +272 -0
  12. package/dist/app-lifecycle.js.map +1 -0
  13. package/dist/app-metrics.js +79 -0
  14. package/dist/app-metrics.js.map +1 -0
  15. package/dist/app-middleware.js +92 -0
  16. package/dist/app-middleware.js.map +1 -0
  17. package/dist/app-routes.js +131 -0
  18. package/dist/app-routes.js.map +1 -0
  19. package/dist/app-ws.js +65 -0
  20. package/dist/app-ws.js.map +1 -0
  21. package/dist/app.d.ts +16 -16
  22. package/dist/app.d.ts.map +1 -1
  23. package/dist/app.js +35 -607
  24. package/dist/app.js.map +1 -1
  25. package/dist/commentary/built-in-patterns.d.ts +1 -1
  26. package/dist/commentary/built-in-patterns.js +1 -1
  27. package/dist/commentary/built-in-patterns.js.map +1 -1
  28. package/dist/config.d.ts +100 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +30 -2
  31. package/dist/config.js.map +1 -1
  32. package/dist/health/checks.d.ts +15 -1
  33. package/dist/health/checks.d.ts.map +1 -1
  34. package/dist/health/checks.js +21 -0
  35. package/dist/health/checks.js.map +1 -1
  36. package/dist/health/routes.js +1 -1
  37. package/dist/index.d.ts +8 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +19 -24
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal/context.d.ts +2 -2
  42. package/dist/internal/wire-error.js +20 -0
  43. package/dist/internal/wire-error.js.map +1 -0
  44. package/dist/lifecycle/pre-bind.d.ts +1 -1
  45. package/dist/metrics/catalog.d.ts.map +1 -1
  46. package/dist/metrics/catalog.js.map +1 -1
  47. package/dist/metrics/tools-bridge.d.ts +15 -0
  48. package/dist/metrics/tools-bridge.d.ts.map +1 -0
  49. package/dist/metrics/tools-bridge.js +103 -0
  50. package/dist/metrics/tools-bridge.js.map +1 -0
  51. package/dist/middleware/audit.d.ts +1 -1
  52. package/dist/middleware/auth.js +1 -1
  53. package/dist/middleware/csrf.js +1 -1
  54. package/dist/middleware/index.js +1 -1
  55. package/dist/middleware/scope.d.ts.map +1 -1
  56. package/dist/middleware/scope.js +44 -4
  57. package/dist/middleware/scope.js.map +1 -1
  58. package/dist/package.js +1 -1
  59. package/dist/package.js.map +1 -1
  60. package/dist/registry/index.d.ts +33 -0
  61. package/dist/registry/index.d.ts.map +1 -1
  62. package/dist/registry/index.js.map +1 -1
  63. package/dist/replay/routes.d.ts +1 -1
  64. package/dist/replay/routes.js +2 -2
  65. package/dist/routes/agents.d.ts.map +1 -1
  66. package/dist/routes/agents.js +64 -18
  67. package/dist/routes/agents.js.map +1 -1
  68. package/dist/routes/auth.js +2 -2
  69. package/dist/routes/auth.js.map +1 -1
  70. package/dist/routes/sessions.js +5 -5
  71. package/dist/routes/sessions.js.map +1 -1
  72. package/dist/routes/tokens.d.ts +1 -1
  73. package/dist/routes/tokens.d.ts.map +1 -1
  74. package/dist/routes/tokens.js +21 -1
  75. package/dist/routes/tokens.js.map +1 -1
  76. package/dist/routes/workflows.d.ts.map +1 -1
  77. package/dist/routes/workflows.js +165 -19
  78. package/dist/routes/workflows.js.map +1 -1
  79. package/dist/runtime/retention.d.ts +105 -0
  80. package/dist/runtime/retention.d.ts.map +1 -0
  81. package/dist/runtime/retention.js +154 -0
  82. package/dist/runtime/retention.js.map +1 -0
  83. package/dist/runtime/run-state.d.ts +2 -0
  84. package/dist/runtime/run-state.d.ts.map +1 -1
  85. package/dist/runtime/run-state.js +34 -2
  86. package/dist/runtime/run-state.js.map +1 -1
  87. package/dist/sse/events.js +3 -4
  88. package/dist/sse/events.js.map +1 -1
  89. package/dist/tools-audit-bridge.d.ts +29 -0
  90. package/dist/tools-audit-bridge.d.ts.map +1 -0
  91. package/dist/tools-audit-bridge.js +103 -0
  92. package/dist/tools-audit-bridge.js.map +1 -0
  93. package/dist/workflows/timer-daemon.d.ts +69 -0
  94. package/dist/workflows/timer-daemon.d.ts.map +1 -0
  95. package/dist/workflows/timer-daemon.js +37 -0
  96. package/dist/workflows/timer-daemon.js.map +1 -0
  97. package/dist/ws/dispatcher.d.ts +1 -1
  98. package/dist/ws/dispatcher.js +19 -12
  99. package/dist/ws/dispatcher.js.map +1 -1
  100. package/dist/ws/index.d.ts +2 -2
  101. package/dist/ws/index.js +3 -3
  102. package/dist/ws/replay-buffer.d.ts +35 -1
  103. package/dist/ws/replay-buffer.d.ts.map +1 -1
  104. package/dist/ws/replay-buffer.js +35 -3
  105. package/dist/ws/replay-buffer.js.map +1 -1
  106. package/dist/ws/upgrade.d.ts.map +1 -1
  107. package/dist/ws/upgrade.js +24 -18
  108. package/dist/ws/upgrade.js.map +1 -1
  109. package/package.json +30 -29
  110. package/src/app-audit-binding.ts +227 -0
  111. package/src/app-daemons.ts +73 -0
  112. package/src/app-lifecycle.ts +476 -0
  113. package/src/app-metrics.ts +144 -0
  114. package/src/app-middleware.ts +149 -0
  115. package/src/app-routes.ts +305 -0
  116. package/src/app-ws.ts +111 -0
  117. package/src/app.ts +317 -0
  118. package/src/commentary/audit-bridge.ts +135 -0
  119. package/src/commentary/built-in-patterns.ts +79 -0
  120. package/src/commentary/index.ts +32 -0
  121. package/src/commentary/sanitizer.ts +238 -0
  122. package/src/commentary/types.ts +130 -0
  123. package/src/config.ts +472 -0
  124. package/src/consolidator/daemon.ts +141 -0
  125. package/src/consolidator/index.ts +14 -0
  126. package/src/errors/index.ts +247 -0
  127. package/src/health/checks.ts +322 -0
  128. package/src/health/index.ts +34 -0
  129. package/src/health/routes.ts +154 -0
  130. package/src/index.ts +243 -0
  131. package/src/internal/context.ts +77 -0
  132. package/src/internal/ids.ts +17 -0
  133. package/src/internal/json.ts +47 -0
  134. package/src/internal/wire-error.ts +44 -0
  135. package/src/lifecycle/hooks.ts +66 -0
  136. package/src/lifecycle/index.ts +16 -0
  137. package/src/lifecycle/pre-bind.ts +138 -0
  138. package/src/metrics/catalog.ts +112 -0
  139. package/src/metrics/index.ts +12 -0
  140. package/src/metrics/registry.ts +337 -0
  141. package/src/metrics/tools-bridge.ts +124 -0
  142. package/src/middleware/audit.ts +114 -0
  143. package/src/middleware/auth.ts +195 -0
  144. package/src/middleware/cors.ts +72 -0
  145. package/src/middleware/csrf.ts +98 -0
  146. package/src/middleware/idempotency.ts +336 -0
  147. package/src/middleware/index.ts +38 -0
  148. package/src/middleware/rate-limit.ts +71 -0
  149. package/src/middleware/request-state.ts +79 -0
  150. package/src/middleware/scope.ts +161 -0
  151. package/src/registry/index.ts +278 -0
  152. package/src/replay/index.ts +14 -0
  153. package/src/replay/routes.ts +255 -0
  154. package/src/routes/agents.ts +363 -0
  155. package/src/routes/audit.ts +207 -0
  156. package/src/routes/auth.ts +80 -0
  157. package/src/routes/health.ts +42 -0
  158. package/src/routes/index.ts +16 -0
  159. package/src/routes/mcp.ts +97 -0
  160. package/src/routes/memory.ts +152 -0
  161. package/src/routes/sessions.ts +250 -0
  162. package/src/routes/skills.ts +91 -0
  163. package/src/routes/tokens.ts +166 -0
  164. package/src/routes/workflows.ts +616 -0
  165. package/src/runtime/retention.ts +284 -0
  166. package/src/runtime/run-state.ts +422 -0
  167. package/src/sse/events.ts +303 -0
  168. package/src/sse/index.ts +7 -0
  169. package/src/tools-audit-bridge.ts +120 -0
  170. package/src/triggers/daemon.ts +198 -0
  171. package/src/triggers/index.ts +16 -0
  172. package/src/triggers/routes.ts +139 -0
  173. package/src/workflows/timer-daemon.ts +102 -0
  174. package/src/ws/dispatcher.ts +537 -0
  175. package/src/ws/index.ts +45 -0
  176. package/src/ws/replay-buffer.ts +209 -0
  177. package/src/ws/subjects.ts +162 -0
  178. package/src/ws/ticket.ts +174 -0
  179. package/src/ws/upgrade.ts +507 -0
  180. package/dist/lifecycle/index.js +0 -3
  181. package/dist/routes/index.js +0 -12
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"upgrade.js","names":["unregister: (() => void) | undefined","payload: unknown","pkg.version","frame: ServerMessage","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 } 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 { checkScope } from '../middleware/scope.js';\nimport { type RunStateTracker, requiredRunScope } 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 / W-107: only an initialized connection whose grant covers\n // the run's OWNING resource may cancel it. A notification carries\n // no id, so an unauthorised one is silently ignored (there is no\n // error channel) rather than aborting an arbitrary run.\n if (initialized) {\n const snapshot = options.runs?.snapshot(message.params.requestId);\n if (\n snapshot !== undefined &&\n checkScope(\n { kind: 'token', grantedScopes: auth.grantedScopes } as never,\n requiredRunScope(snapshot, 'control'),\n )\n ) {\n options.runs?.abort(message.params.requestId, 'mcp-cancel');\n }\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 / W-107: mirror the REST `POST /runs/:runId/abort` gate at\n // the same per-resource granularity - snapshot first (unknown run\n // answers RUN_NOT_FOUND, matching REST's 404-before-403 order),\n // then require the OWNING agent's/workflow's control scope.\n const runId = message.params.runId;\n const snapshot = options.runs?.snapshot(runId);\n if (snapshot === undefined) {\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 const required = requiredRunScope(snapshot, 'control');\n if (!checkScope({ kind: 'token', grantedScopes: auth.grantedScopes } as never, required)) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SCOPE_DENIED,\n `Token lacks required scope '${required}'.`,\n );\n return;\n }\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-13: the anonymous trusted-loopback principal (auth disabled) holds\n * `admin:*` so every per-subject and per-run check passes uniformly.\n */\nconst ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope> = [parseScope('admin:*')];\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.6.1",
3
+ "version": "0.7.0",
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",
@@ -33,8 +33,9 @@
33
33
  "graceful-shutdown"
34
34
  ],
35
35
  "type": "module",
36
+ "sideEffects": false,
36
37
  "engines": {
37
- "node": ">=22.0.0"
38
+ "node": ">=22.12.0"
38
39
  },
39
40
  "main": "./dist/index.js",
40
41
  "module": "./dist/index.js",
@@ -42,60 +43,61 @@
42
43
  "exports": {
43
44
  ".": {
44
45
  "types": "./dist/index.d.ts",
45
- "import": "./dist/index.js"
46
+ "default": "./dist/index.js"
46
47
  },
47
48
  "./config": {
48
49
  "types": "./dist/config.d.ts",
49
- "import": "./dist/config.js"
50
+ "default": "./dist/config.js"
50
51
  },
51
52
  "./middleware": {
52
53
  "types": "./dist/middleware/index.d.ts",
53
- "import": "./dist/middleware/index.js"
54
+ "default": "./dist/middleware/index.js"
54
55
  },
55
56
  "./errors": {
56
57
  "types": "./dist/errors/index.d.ts",
57
- "import": "./dist/errors/index.js"
58
+ "default": "./dist/errors/index.js"
58
59
  },
59
60
  "./registry": {
60
61
  "types": "./dist/registry/index.d.ts",
61
- "import": "./dist/registry/index.js"
62
+ "default": "./dist/registry/index.js"
62
63
  },
63
64
  "./ws": {
64
65
  "types": "./dist/ws/index.d.ts",
65
- "import": "./dist/ws/index.js"
66
+ "default": "./dist/ws/index.js"
66
67
  },
67
68
  "./sse": {
68
69
  "types": "./dist/sse/index.d.ts",
69
- "import": "./dist/sse/index.js"
70
+ "default": "./dist/sse/index.js"
70
71
  },
71
72
  "./commentary": {
72
73
  "types": "./dist/commentary/index.d.ts",
73
- "import": "./dist/commentary/index.js"
74
+ "default": "./dist/commentary/index.js"
74
75
  },
75
76
  "./triggers": {
76
77
  "types": "./dist/triggers/index.d.ts",
77
- "import": "./dist/triggers/index.js"
78
+ "default": "./dist/triggers/index.js"
78
79
  },
79
80
  "./consolidator": {
80
81
  "types": "./dist/consolidator/index.d.ts",
81
- "import": "./dist/consolidator/index.js"
82
+ "default": "./dist/consolidator/index.js"
82
83
  },
83
84
  "./health": {
84
85
  "types": "./dist/health/index.d.ts",
85
- "import": "./dist/health/index.js"
86
+ "default": "./dist/health/index.js"
86
87
  },
87
88
  "./metrics": {
88
89
  "types": "./dist/metrics/index.d.ts",
89
- "import": "./dist/metrics/index.js"
90
+ "default": "./dist/metrics/index.js"
90
91
  },
91
92
  "./replay": {
92
93
  "types": "./dist/replay/index.d.ts",
93
- "import": "./dist/replay/index.js"
94
+ "default": "./dist/replay/index.js"
94
95
  },
95
96
  "./package.json": "./package.json"
96
97
  },
97
98
  "files": [
98
99
  "dist",
100
+ "src",
99
101
  "README.md",
100
102
  "CHANGELOG.md",
101
103
  "LICENSE"
@@ -105,18 +107,18 @@
105
107
  "@hono/node-ws": "^1.3.0",
106
108
  "hono": "^4.12.25",
107
109
  "zod": "^3.25.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"
110
+ "@graphorin/core": "0.7.0",
111
+ "@graphorin/protocol": "0.7.0",
112
+ "@graphorin/security": "0.7.0",
113
+ "@graphorin/store-sqlite": "0.7.0",
114
+ "@graphorin/tools": "0.7.0",
115
+ "@graphorin/triggers": "0.7.0"
114
116
  },
115
117
  "peerDependencies": {
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"
118
+ "@graphorin/agent": ">=0.7.0 <1.0.0",
119
+ "@graphorin/memory": ">=0.7.0 <1.0.0",
120
+ "@graphorin/sessions": ">=0.7.0 <1.0.0",
121
+ "@graphorin/workflow": ">=0.7.0 <1.0.0"
120
122
  },
121
123
  "peerDependenciesMeta": {
122
124
  "@graphorin/agent": {
@@ -141,10 +143,9 @@
141
143
  "@types/ws": "^8.18.1",
142
144
  "better-sqlite3": "^12.9.0",
143
145
  "ws": "^8.20.1",
144
- "@graphorin/agent": "0.6.1",
145
- "@graphorin/memory": "0.6.1",
146
- "@graphorin/sessions": "0.6.1",
147
- "@graphorin/workflow": "0.6.1"
146
+ "@graphorin/agent": "0.7.0",
147
+ "@graphorin/sessions": "0.7.0",
148
+ "@graphorin/workflow": "0.7.0"
148
149
  },
149
150
  "scripts": {
150
151
  "build": "tsdown",
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Default audit-db binding registration for `createServer({...})`.
3
+ *
4
+ * Extracted from `app.ts` so the binding (and its SQLite row mapping)
5
+ * lives next to nothing but itself; the server lifecycle and the CLI
6
+ * both call {@link ensureStoreAuditBinding} before opening the audit
7
+ * chain.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ import { registerAuditDbBinding } from '@graphorin/security/audit';
13
+ import {
14
+ cipherSelectionPragmas,
15
+ type EncryptionCipher,
16
+ loadCipherDriver,
17
+ } from '@graphorin/store-sqlite';
18
+
19
+ /** The ciphers sqlite3mc supports - mirrors `EncryptionCipher`. */
20
+ const KNOWN_CIPHERS: ReadonlySet<string> = new Set([
21
+ 'sqlcipher',
22
+ 'chacha20',
23
+ 'aes256cbc',
24
+ 'aes128cbc',
25
+ 'rc4',
26
+ ]);
27
+
28
+ /**
29
+ * Pre-built audit-db binding shipped from `@graphorin/store-sqlite`.
30
+ * Registered exactly once per process so {@link openAuditDb} can find
31
+ * a default binding without forcing operators to wire it manually.
32
+ *
33
+ * Exported as {@link ensureStoreAuditBinding} so the CLI (Phase 15
34
+ * `graphorin audit verify | prune | export`) can reach into the same
35
+ * binding without booting the HTTP listener.
36
+ *
37
+ * @stable
38
+ */
39
+ let storeAuditBindingRegistered = false;
40
+ export function ensureStoreAuditBinding(): void {
41
+ if (storeAuditBindingRegistered) return;
42
+ registerAuditDbBinding(
43
+ {
44
+ id: 'better-sqlite3-multiple-ciphers',
45
+ description: 'Default audit-db binding shipped by @graphorin/store-sqlite.',
46
+ open: async (opts) => {
47
+ const driver = await loadCipherDriver();
48
+ const passphrase = await opts.passphrase.use((value) => value);
49
+ const Db = driver as unknown as new (
50
+ path: string,
51
+ ) => {
52
+ pragma(s: string): unknown;
53
+ prepare(sql: string): {
54
+ run(...args: unknown[]): unknown;
55
+ get(...args: unknown[]): unknown;
56
+ all(...args: unknown[]): unknown;
57
+ iterate(...args: unknown[]): IterableIterator<unknown>;
58
+ };
59
+ exec(sql: string): unknown;
60
+ close(): void;
61
+ open: boolean;
62
+ };
63
+ // W-110 / CS-7: pin the cipher BEFORE PRAGMA key - `opts.cipher`
64
+ // (config.audit.cipher) was previously ignored, so every audit.db
65
+ // silently came out as sqlite3mc's default. Default 'chacha20':
66
+ // pre-fix files were created without cipher pragmas (= chacha20
67
+ // format), so this pin is byte-compatible with every existing
68
+ // audit.db, while 'sqlcipher' (the MAIN store's ADR-030 default)
69
+ // would brick them. Unknown values fail fast.
70
+ const cipher = opts.cipher ?? 'chacha20';
71
+ if (!KNOWN_CIPHERS.has(cipher)) {
72
+ throw new Error(
73
+ `[graphorin/server] unknown audit.cipher '${cipher}'. Supported: ${[...KNOWN_CIPHERS].join(', ')}.`,
74
+ );
75
+ }
76
+ const db = new Db(opts.path);
77
+ for (const pragma of cipherSelectionPragmas(cipher as EncryptionCipher)) {
78
+ db.pragma(pragma);
79
+ }
80
+ db.pragma(`key = '${passphrase.replace(/'/g, "''")}'`);
81
+ db.pragma('journal_mode = WAL');
82
+ db.pragma('synchronous = NORMAL');
83
+ db.pragma('busy_timeout = 5000');
84
+ db.pragma('foreign_keys = ON');
85
+ db.exec(
86
+ `CREATE TABLE IF NOT EXISTS audit_log (
87
+ seq INTEGER PRIMARY KEY,
88
+ ts INTEGER NOT NULL,
89
+ actor_json TEXT NOT NULL,
90
+ action TEXT NOT NULL,
91
+ target TEXT NOT NULL,
92
+ decision TEXT NOT NULL,
93
+ context_json TEXT,
94
+ metadata_json TEXT,
95
+ prev_hash TEXT NOT NULL,
96
+ hash TEXT NOT NULL UNIQUE
97
+ ) WITHOUT ROWID;`,
98
+ );
99
+ return {
100
+ binding: 'better-sqlite3-multiple-ciphers',
101
+ path: opts.path,
102
+ async insert(entry) {
103
+ db.prepare(
104
+ `INSERT INTO audit_log (
105
+ seq, ts, actor_json, action, target, decision,
106
+ context_json, metadata_json, prev_hash, hash
107
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
108
+ ).run(
109
+ entry.seq,
110
+ entry.ts,
111
+ JSON.stringify(entry.actor),
112
+ entry.action,
113
+ entry.target,
114
+ entry.decision,
115
+ entry.context !== undefined ? JSON.stringify(entry.context) : null,
116
+ entry.metadata !== undefined ? JSON.stringify(entry.metadata) : null,
117
+ entry.prevHash,
118
+ entry.hash,
119
+ );
120
+ return entry;
121
+ },
122
+ async latest() {
123
+ const row = db.prepare('SELECT * FROM audit_log ORDER BY seq DESC LIMIT 1').get() as
124
+ | undefined
125
+ | {
126
+ seq: number;
127
+ ts: number;
128
+ actor_json: string;
129
+ action: string;
130
+ target: string;
131
+ decision: string;
132
+ context_json: string | null;
133
+ metadata_json: string | null;
134
+ prev_hash: string;
135
+ hash: string;
136
+ };
137
+ if (row === undefined) return undefined;
138
+ return rowToEntry(row);
139
+ },
140
+ async *iterate(bounds) {
141
+ const lo = bounds?.fromSeq ?? 1;
142
+ const hi = bounds?.toSeq ?? Number.MAX_SAFE_INTEGER;
143
+ const iter = db
144
+ .prepare('SELECT * FROM audit_log WHERE seq BETWEEN ? AND ? ORDER BY seq ASC')
145
+ .iterate(lo, hi);
146
+ for (const row of iter) yield rowToEntry(row as Parameters<typeof rowToEntry>[0]);
147
+ },
148
+ async count() {
149
+ const row = db.prepare('SELECT COUNT(*) AS n FROM audit_log').get() as { n: number };
150
+ return row.n;
151
+ },
152
+ async deleteUpTo(threshold) {
153
+ const before = db
154
+ .prepare('SELECT COUNT(*) AS n FROM audit_log WHERE seq <= ?')
155
+ .get(threshold) as { n: number };
156
+ db.prepare('DELETE FROM audit_log WHERE seq <= ?').run(threshold);
157
+ return before.n;
158
+ },
159
+ async replaceEntry(entry) {
160
+ db.prepare(`UPDATE audit_log SET prev_hash = ?, hash = ? WHERE seq = ?`).run(
161
+ entry.prevHash,
162
+ entry.hash,
163
+ entry.seq,
164
+ );
165
+ },
166
+ // W-011: cross-process fence. Raw BEGIN IMMEDIATE (not
167
+ // better-sqlite3's .transaction() - that helper rejects async
168
+ // functions, while this binding's methods are async-shaped
169
+ // with synchronous bodies; the in-process WRITE_CHAINS
170
+ // serialisation guarantees no foreign statement interleaves
171
+ // inside the transaction). ROLLBACK strictly in finally so an
172
+ // exception can never leave the handle wedged inside an open
173
+ // transaction.
174
+ async transact(fn) {
175
+ db.exec('BEGIN IMMEDIATE');
176
+ let committed = false;
177
+ try {
178
+ const result = await fn();
179
+ db.exec('COMMIT');
180
+ committed = true;
181
+ return result;
182
+ } finally {
183
+ if (!committed) {
184
+ try {
185
+ db.exec('ROLLBACK');
186
+ } catch {
187
+ // Already rolled back / connection unusable - nothing to release.
188
+ }
189
+ }
190
+ }
191
+ },
192
+ async close() {
193
+ if (db.open) db.close();
194
+ },
195
+ };
196
+ },
197
+ },
198
+ { setAsDefault: true },
199
+ );
200
+ storeAuditBindingRegistered = true;
201
+ }
202
+
203
+ function rowToEntry(row: {
204
+ seq: number;
205
+ ts: number;
206
+ actor_json: string;
207
+ action: string;
208
+ target: string;
209
+ decision: string;
210
+ context_json: string | null;
211
+ metadata_json: string | null;
212
+ prev_hash: string;
213
+ hash: string;
214
+ }) {
215
+ return {
216
+ seq: row.seq,
217
+ ts: row.ts,
218
+ actor: JSON.parse(row.actor_json),
219
+ action: row.action,
220
+ target: row.target,
221
+ decision: row.decision as 'success' | 'denied' | 'error' | 'not-found',
222
+ ...(row.context_json !== null ? { context: JSON.parse(row.context_json) } : {}),
223
+ ...(row.metadata_json !== null ? { metadata: JSON.parse(row.metadata_json) } : {}),
224
+ prevHash: row.prev_hash,
225
+ hash: row.hash,
226
+ };
227
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Daemon construction for `createServer({...})` - wraps the caller's
3
+ * triggers scheduler / consolidator surfaces in their daemon adapters.
4
+ * Start/stop sequencing stays in `app-lifecycle.ts` because the order
5
+ * relative to the listener is part of the lifecycle contract.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { ConsolidatorDaemon, ConsolidatorLike } from './consolidator/daemon.js';
11
+ import { createConsolidatorDaemon } from './consolidator/daemon.js';
12
+ import type { TriggersDaemon } from './triggers/daemon.js';
13
+ import { createTriggersDaemon } from './triggers/daemon.js';
14
+ import type { WorkflowTimerDaemon, WorkflowTimerDriverLike } from './workflows/timer-daemon.js';
15
+ import { createWorkflowTimerDaemon } from './workflows/timer-daemon.js';
16
+
17
+ /**
18
+ * Discriminated union accepted by `CreateServerOptions.triggers`. A
19
+ * caller may either supply a fully-built daemon (e.g. constructed
20
+ * around a custom `Scheduler`) or just the underlying scheduler - the
21
+ * server wraps it with {@link createTriggersDaemon} automatically.
22
+ *
23
+ * @stable
24
+ */
25
+ export type TriggersDaemonInput =
26
+ | { readonly daemon: TriggersDaemon }
27
+ | { readonly scheduler: import('@graphorin/triggers').Scheduler };
28
+
29
+ /** W-032: accepted forms for `createServer({ workflowTimers })`. */
30
+ export type WorkflowTimersInput =
31
+ | { readonly daemon: WorkflowTimerDaemon }
32
+ | { readonly driver: WorkflowTimerDriverLike };
33
+
34
+ /** Subset of `CreateServerOptions` consumed by {@link buildDaemons}. */
35
+ export interface BuildDaemonsOptions {
36
+ readonly triggers?: TriggersDaemonInput | undefined;
37
+ readonly consolidator?: ConsolidatorLike | undefined;
38
+ readonly workflowTimers?: WorkflowTimersInput | undefined;
39
+ }
40
+
41
+ /** Daemon handles constructed (or adopted) by {@link buildDaemons}. */
42
+ export interface BuiltDaemons {
43
+ readonly triggersDaemon: TriggersDaemon | undefined;
44
+ readonly consolidatorDaemon: ConsolidatorDaemon | undefined;
45
+ readonly workflowTimerDaemon: WorkflowTimerDaemon | undefined;
46
+ }
47
+
48
+ /**
49
+ * Wrap the operator-supplied triggers / consolidator surfaces in their
50
+ * daemon adapters. Pure construction - nothing is started here.
51
+ */
52
+ export function buildDaemons(options: BuildDaemonsOptions): BuiltDaemons {
53
+ let triggersDaemon: TriggersDaemon | undefined;
54
+ if (options.triggers !== undefined) {
55
+ if ('daemon' in options.triggers) {
56
+ triggersDaemon = options.triggers.daemon;
57
+ } else {
58
+ triggersDaemon = createTriggersDaemon({ scheduler: options.triggers.scheduler });
59
+ }
60
+ }
61
+ let consolidatorDaemon: ConsolidatorDaemon | undefined;
62
+ if (options.consolidator !== undefined) {
63
+ consolidatorDaemon = createConsolidatorDaemon({ consolidator: options.consolidator });
64
+ }
65
+ let workflowTimerDaemon: WorkflowTimerDaemon | undefined;
66
+ if (options.workflowTimers !== undefined) {
67
+ workflowTimerDaemon =
68
+ 'daemon' in options.workflowTimers
69
+ ? options.workflowTimers.daemon
70
+ : createWorkflowTimerDaemon({ driver: options.workflowTimers.driver });
71
+ }
72
+ return { triggersDaemon, consolidatorDaemon, workflowTimerDaemon };
73
+ }