@graphorin/server 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +6 -6
  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 +40 -611
  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 +9 -4
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +22 -26
  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 +6 -0
  59. package/dist/package.js.map +1 -0
  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.d.ts.map +1 -1
  99. package/dist/ws/dispatcher.js +19 -12
  100. package/dist/ws/dispatcher.js.map +1 -1
  101. package/dist/ws/index.d.ts +2 -2
  102. package/dist/ws/index.js +3 -3
  103. package/dist/ws/replay-buffer.d.ts +35 -1
  104. package/dist/ws/replay-buffer.d.ts.map +1 -1
  105. package/dist/ws/replay-buffer.js +35 -3
  106. package/dist/ws/replay-buffer.js.map +1 -1
  107. package/dist/ws/upgrade.d.ts.map +1 -1
  108. package/dist/ws/upgrade.js +26 -19
  109. package/dist/ws/upgrade.js.map +1 -1
  110. package/package.json +31 -30
  111. package/src/app-audit-binding.ts +227 -0
  112. package/src/app-daemons.ts +73 -0
  113. package/src/app-lifecycle.ts +476 -0
  114. package/src/app-metrics.ts +144 -0
  115. package/src/app-middleware.ts +149 -0
  116. package/src/app-routes.ts +305 -0
  117. package/src/app-ws.ts +111 -0
  118. package/src/app.ts +317 -0
  119. package/src/commentary/audit-bridge.ts +135 -0
  120. package/src/commentary/built-in-patterns.ts +79 -0
  121. package/src/commentary/index.ts +32 -0
  122. package/src/commentary/sanitizer.ts +238 -0
  123. package/src/commentary/types.ts +130 -0
  124. package/src/config.ts +472 -0
  125. package/src/consolidator/daemon.ts +141 -0
  126. package/src/consolidator/index.ts +14 -0
  127. package/src/errors/index.ts +247 -0
  128. package/src/health/checks.ts +322 -0
  129. package/src/health/index.ts +34 -0
  130. package/src/health/routes.ts +154 -0
  131. package/src/index.ts +243 -0
  132. package/src/internal/context.ts +77 -0
  133. package/src/internal/ids.ts +17 -0
  134. package/src/internal/json.ts +47 -0
  135. package/src/internal/wire-error.ts +44 -0
  136. package/src/lifecycle/hooks.ts +66 -0
  137. package/src/lifecycle/index.ts +16 -0
  138. package/src/lifecycle/pre-bind.ts +138 -0
  139. package/src/metrics/catalog.ts +112 -0
  140. package/src/metrics/index.ts +12 -0
  141. package/src/metrics/registry.ts +337 -0
  142. package/src/metrics/tools-bridge.ts +124 -0
  143. package/src/middleware/audit.ts +114 -0
  144. package/src/middleware/auth.ts +195 -0
  145. package/src/middleware/cors.ts +72 -0
  146. package/src/middleware/csrf.ts +98 -0
  147. package/src/middleware/idempotency.ts +336 -0
  148. package/src/middleware/index.ts +38 -0
  149. package/src/middleware/rate-limit.ts +71 -0
  150. package/src/middleware/request-state.ts +79 -0
  151. package/src/middleware/scope.ts +161 -0
  152. package/src/registry/index.ts +278 -0
  153. package/src/replay/index.ts +14 -0
  154. package/src/replay/routes.ts +255 -0
  155. package/src/routes/agents.ts +363 -0
  156. package/src/routes/audit.ts +207 -0
  157. package/src/routes/auth.ts +80 -0
  158. package/src/routes/health.ts +42 -0
  159. package/src/routes/index.ts +16 -0
  160. package/src/routes/mcp.ts +97 -0
  161. package/src/routes/memory.ts +152 -0
  162. package/src/routes/sessions.ts +250 -0
  163. package/src/routes/skills.ts +91 -0
  164. package/src/routes/tokens.ts +166 -0
  165. package/src/routes/workflows.ts +616 -0
  166. package/src/runtime/retention.ts +284 -0
  167. package/src/runtime/run-state.ts +422 -0
  168. package/src/sse/events.ts +303 -0
  169. package/src/sse/index.ts +7 -0
  170. package/src/tools-audit-bridge.ts +120 -0
  171. package/src/triggers/daemon.ts +198 -0
  172. package/src/triggers/index.ts +16 -0
  173. package/src/triggers/routes.ts +139 -0
  174. package/src/workflows/timer-daemon.ts +102 -0
  175. package/src/ws/dispatcher.ts +537 -0
  176. package/src/ws/index.ts +45 -0
  177. package/src/ws/replay-buffer.ts +209 -0
  178. package/src/ws/subjects.ts +162 -0
  179. package/src/ws/ticket.ts +174 -0
  180. package/src/ws/upgrade.ts +507 -0
  181. package/dist/lifecycle/index.js +0 -3
  182. package/dist/routes/index.js +0 -12
@@ -0,0 +1,272 @@
1
+ import { ensureStoreAuditBinding } from "./app-audit-binding.js";
2
+ import { SERVER_METRIC_NAMES } from "./metrics/catalog.js";
3
+ import { LifecycleDoubleStartError, LifecycleNotStartedError, ShutdownTimeoutError } from "./errors/index.js";
4
+ import { scheduleRunPruning } from "./runtime/run-state.js";
5
+ import { bridgeCommentaryToAudit } from "./commentary/audit-bridge.js";
6
+ import { mountRoutes } from "./app-routes.js";
7
+ import { runPreBind } from "./lifecycle/pre-bind.js";
8
+ import { createConsoleRetentionLog, scheduleRetentionSweeps } from "./runtime/retention.js";
9
+ import { bridgeToolAuditToAudit } from "./tools-audit-bridge.js";
10
+ import { scheduleReplayBufferPruning } from "./ws/replay-buffer.js";
11
+ import { readWalSize } from "@graphorin/store-sqlite";
12
+ import { TokenVerifier, applyProcessHardening, generatePepper } from "@graphorin/security";
13
+ import { openAuditDb } from "@graphorin/security/audit";
14
+ import { serve } from "@hono/node-server";
15
+
16
+ //#region src/app-lifecycle.ts
17
+ function createLifecycle(deps) {
18
+ const { options, config, store, app, metricRegistry, runs, agents, workflows, version, startedAt, now, triggersDaemon, consolidatorDaemon, workflowTimerDaemon } = deps;
19
+ const commentaryAuditSink = deps.ws.commentaryAuditSink;
20
+ let serverInstance;
21
+ let listening;
22
+ let started = false;
23
+ let stopped = false;
24
+ let stopRunPruning;
25
+ let stopReplayBufferPruning;
26
+ let stopRetention;
27
+ let auditDb;
28
+ let toolAuditBridge;
29
+ let preBind;
30
+ let verifier;
31
+ let pepperHandle;
32
+ let dispatcher = deps.ws.dispatcher;
33
+ let tickets = deps.ws.tickets;
34
+ let wsAdapter = deps.ws.wsAdapter;
35
+ return {
36
+ get listeningOn() {
37
+ return listening;
38
+ },
39
+ get wsDispatcher() {
40
+ return dispatcher;
41
+ },
42
+ get wsTickets() {
43
+ return tickets;
44
+ },
45
+ async start() {
46
+ if (started) throw new LifecycleDoubleStartError();
47
+ started = true;
48
+ try {
49
+ if (options.hooks?.beforeStart !== void 0) await options.hooks.beforeStart({ config });
50
+ if (options.skipHardening !== true && config.hardening.applyOnStart) applyProcessHardening({
51
+ refuseRoot: config.hardening.refuseRoot,
52
+ umask: config.hardening.umask
53
+ });
54
+ preBind = await runPreBind({
55
+ config,
56
+ store,
57
+ ...options.probeCipherPeer !== void 0 ? { probeCipherPeer: options.probeCipherPeer } : {}
58
+ });
59
+ if (config.audit.enabled && preBind.auditPassphrase !== void 0 && preBind.auditPath !== void 0) {
60
+ ensureStoreAuditBinding();
61
+ auditDb = await openAuditDb({
62
+ path: preBind.auditPath,
63
+ passphrase: preBind.auditPassphrase,
64
+ ...config.audit.cipher !== void 0 ? { cipher: config.audit.cipher } : {}
65
+ });
66
+ commentaryAuditSink?.bind(bridgeCommentaryToAudit(auditDb));
67
+ toolAuditBridge = bridgeToolAuditToAudit(auditDb, { policy: config.audit.toolEvents });
68
+ }
69
+ if (config.auth.kind === "token") {
70
+ const pepper = preBind.pepper ?? await fallbackPepper(options.skipHardening === true);
71
+ pepperHandle = pepper;
72
+ verifier = new TokenVerifier({
73
+ tokenStore: store.authTokens,
74
+ pepper,
75
+ acceptPrefix: config.auth.tokenPrefix,
76
+ acceptEnvironments: config.auth.tokenEnvironments,
77
+ ...config.auth.perIpFailureThreshold !== void 0 ? { perIpFailureThreshold: config.auth.perIpFailureThreshold } : {},
78
+ ...config.auth.perIpLockoutMs !== void 0 ? { perIpLockoutMs: config.auth.perIpLockoutMs } : {},
79
+ now
80
+ });
81
+ }
82
+ mountRoutes(app, config, {
83
+ version,
84
+ startedAt,
85
+ now,
86
+ agents,
87
+ workflows,
88
+ runs,
89
+ store,
90
+ metricRegistry,
91
+ ...options.sessions !== void 0 ? { sessions: options.sessions } : {},
92
+ ...options.memory !== void 0 ? { memory: options.memory } : {},
93
+ ...options.skills !== void 0 ? { skills: options.skills } : {},
94
+ ...options.mcp !== void 0 ? { mcp: options.mcp } : {},
95
+ ...options.audit !== void 0 ? { audit: options.audit } : {},
96
+ ...options.replay !== void 0 ? { replay: options.replay } : {},
97
+ ...options.healthProbes !== void 0 ? { healthProbes: options.healthProbes } : {},
98
+ ...verifier !== void 0 ? { verifier } : {},
99
+ ...auditDb !== void 0 ? { auditDb } : {},
100
+ ...pepperHandle !== void 0 ? { pepper: pepperHandle } : {},
101
+ ...dispatcher !== void 0 ? { wsDispatcher: dispatcher } : {},
102
+ ...tickets !== void 0 ? { wsTickets: tickets } : {},
103
+ ...wsAdapter !== void 0 ? { wsAdapter } : {},
104
+ ...triggersDaemon !== void 0 ? { triggersDaemon } : {},
105
+ ...consolidatorDaemon !== void 0 ? { consolidatorDaemon } : {},
106
+ ...workflowTimerDaemon !== void 0 ? { workflowTimerDaemon } : {}
107
+ });
108
+ if (consolidatorDaemon !== void 0) {
109
+ await consolidatorDaemon.start();
110
+ if (triggersDaemon !== void 0 && consolidatorDaemon.consolidator.registerWithScheduler !== void 0) await consolidatorDaemon.consolidator.registerWithScheduler(triggersDaemon.scheduler);
111
+ }
112
+ if (triggersDaemon !== void 0) await triggersDaemon.start();
113
+ if (workflowTimerDaemon !== void 0) await workflowTimerDaemon.start();
114
+ try {
115
+ const wal = readWalSize(store.connection);
116
+ metricRegistry.set(SERVER_METRIC_NAMES.storageWalSize, wal);
117
+ } catch {}
118
+ metricRegistry.set(SERVER_METRIC_NAMES.serverUptime, Math.max(0, Math.floor((now() - startedAt) / 1e3)));
119
+ metricRegistry.set(SERVER_METRIC_NAMES.inflightRuns, runs.runningCount());
120
+ metricRegistry.set(SERVER_METRIC_NAMES.replayBufferEvents, 0);
121
+ stopRunPruning = scheduleRunPruning(runs, now);
122
+ if (dispatcher !== void 0) stopReplayBufferPruning = scheduleReplayBufferPruning(dispatcher.replayBuffer, { intervalMs: config.server.stream.replayBuffer.pruneIntervalSeconds * 1e3 });
123
+ const retentionLog = createConsoleRetentionLog(config.observability.logger);
124
+ stopRetention = scheduleRetentionSweeps({
125
+ store,
126
+ config: config.retention,
127
+ now,
128
+ ...retentionLog !== void 0 ? { log: retentionLog } : {}
129
+ });
130
+ if (options.skipListen !== true) {
131
+ serverInstance = serve({
132
+ fetch: app.fetch.bind(app),
133
+ hostname: config.server.host,
134
+ port: config.server.port
135
+ });
136
+ if (wsAdapter !== void 0 && serverInstance !== void 0) wsAdapter.injectWebSocket(serverInstance);
137
+ await new Promise((resolve) => {
138
+ const server = serverInstance;
139
+ if ("once" in server && typeof server.once === "function") server.once("listening", () => resolve());
140
+ else setImmediate(() => resolve());
141
+ });
142
+ const address = serverInstance.address();
143
+ if (address !== null && typeof address === "object") listening = {
144
+ host: address.address,
145
+ port: address.port
146
+ };
147
+ else listening = {
148
+ host: config.server.host,
149
+ port: config.server.port
150
+ };
151
+ } else listening = {
152
+ host: config.server.host,
153
+ port: config.server.port
154
+ };
155
+ if (options.hooks?.onReady !== void 0) await options.hooks.onReady({
156
+ config,
157
+ listeningOn: listening
158
+ });
159
+ return listening;
160
+ } catch (err) {
161
+ started = false;
162
+ await emitError(options.hooks, {
163
+ error: err,
164
+ phase: "beforeStart"
165
+ });
166
+ throw err;
167
+ }
168
+ },
169
+ async stop({ force } = {}) {
170
+ if (!started) throw new LifecycleNotStartedError();
171
+ if (stopped) return;
172
+ stopped = true;
173
+ stopRunPruning?.();
174
+ stopRunPruning = void 0;
175
+ stopReplayBufferPruning?.();
176
+ stopReplayBufferPruning = void 0;
177
+ stopRetention?.();
178
+ stopRetention = void 0;
179
+ if (toolAuditBridge !== void 0) {
180
+ toolAuditBridge.stop();
181
+ try {
182
+ await toolAuditBridge.drain();
183
+ } catch {}
184
+ toolAuditBridge = void 0;
185
+ }
186
+ const drainTimeoutMs = force === true ? 0 : config.server.shutdown.drainTimeoutMs;
187
+ try {
188
+ if (options.hooks?.beforeShutdown !== void 0) await options.hooks.beforeShutdown({
189
+ config,
190
+ inflight: runs.inflightCount(),
191
+ drainTimeoutMs
192
+ });
193
+ } catch (err) {
194
+ await emitError(options.hooks, {
195
+ error: err,
196
+ phase: "beforeShutdown"
197
+ });
198
+ }
199
+ if (workflowTimerDaemon !== void 0) try {
200
+ await workflowTimerDaemon.stop();
201
+ } catch (err) {
202
+ await emitError(options.hooks, {
203
+ error: err,
204
+ phase: "beforeShutdown"
205
+ });
206
+ }
207
+ if (triggersDaemon !== void 0) try {
208
+ await triggersDaemon.stop();
209
+ } catch (err) {
210
+ await emitError(options.hooks, {
211
+ error: err,
212
+ phase: "beforeShutdown"
213
+ });
214
+ }
215
+ if (consolidatorDaemon !== void 0) try {
216
+ await consolidatorDaemon.stop();
217
+ } catch (err) {
218
+ await emitError(options.hooks, {
219
+ error: err,
220
+ phase: "beforeShutdown"
221
+ });
222
+ }
223
+ runs.abortPending("server-shutdown");
224
+ if (!await drainInFlight(runs, drainTimeoutMs, now) && drainTimeoutMs > 0) runs.abortAll(new ShutdownTimeoutError(drainTimeoutMs, runs.runningCount()));
225
+ if (dispatcher !== void 0) try {
226
+ dispatcher.shutdown();
227
+ } catch {}
228
+ if (serverInstance !== void 0) {
229
+ await new Promise((resolve) => {
230
+ serverInstance.close(() => resolve());
231
+ });
232
+ serverInstance = void 0;
233
+ }
234
+ if (auditDb !== void 0) {
235
+ await auditDb.close();
236
+ auditDb = void 0;
237
+ }
238
+ await store.close();
239
+ listening = void 0;
240
+ verifier = void 0;
241
+ started = false;
242
+ preBind = void 0;
243
+ dispatcher = void 0;
244
+ tickets = void 0;
245
+ wsAdapter = void 0;
246
+ }
247
+ };
248
+ }
249
+ async function drainInFlight(runs, drainTimeoutMs, now) {
250
+ if (runs.runningCount() === 0) return true;
251
+ if (drainTimeoutMs <= 0) return runs.runningCount() === 0;
252
+ const deadline = now() + drainTimeoutMs;
253
+ while (now() < deadline) {
254
+ if (runs.runningCount() === 0) return true;
255
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - now())));
256
+ }
257
+ return runs.runningCount() === 0;
258
+ }
259
+ async function emitError(hooks, ctx) {
260
+ if (hooks?.onError === void 0) return;
261
+ try {
262
+ await hooks.onError(ctx);
263
+ } catch {}
264
+ }
265
+ async function fallbackPepper(skipHardening) {
266
+ if (!skipHardening) throw new Error("[graphorin/server] missing resolved pepper after pre-bind; auth.pepperRef is required when auth.kind = token.");
267
+ return generatePepper();
268
+ }
269
+
270
+ //#endregion
271
+ export { createLifecycle };
272
+ //# sourceMappingURL=app-lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-lifecycle.js","names":["serverInstance: ServerType | undefined","listening: { readonly host: string; readonly port: number } | undefined","stopRunPruning: (() => void) | undefined","stopReplayBufferPruning: (() => void) | undefined","stopRetention: (() => void) | undefined","auditDb: AuditDb | undefined","toolAuditBridge: ToolAuditBridge | undefined","preBind: Awaited<ReturnType<typeof runPreBind>> | undefined","verifier: TokenVerifier | undefined","pepperHandle: import('@graphorin/security').SecretValue | undefined"],"sources":["../src/app-lifecycle.ts"],"sourcesContent":["/**\n * Lifecycle for `createServer({...})` - the `start()` / `stop()` pair\n * behind the {@link GraphorinServer} handle: beforeStart hook, process\n * hardening, pre-bind validation + migrations, audit-chain open,\n * token-verifier construction, route mounting, daemon start order\n * (consolidator first, scheduler last), listener bind, onReady, and\n * the mirrored shutdown sequence (drain, force-abort, dispatcher\n * shutdown, listener close, audit close, store close).\n *\n * The sequencing in this module is part of the documented contract -\n * pure movement only, do not reorder.\n *\n * @packageDocumentation\n */\n\nimport type { AddressInfo } from 'node:net';\nimport { applyProcessHardening, generatePepper, TokenVerifier } from '@graphorin/security';\nimport { type AuditDb, openAuditDb } from '@graphorin/security/audit';\nimport { type GraphorinSqliteStore, readWalSize } from '@graphorin/store-sqlite';\nimport { type ServerType, serve } from '@hono/node-server';\nimport type { Hono } from 'hono';\nimport { ensureStoreAuditBinding } from './app-audit-binding.js';\nimport { mountRoutes } from './app-routes.js';\nimport type { WsLayer } from './app-ws.js';\nimport { bridgeCommentaryToAudit } from './commentary/index.js';\nimport type { ServerConfigSpec } from './config.js';\nimport type { ConsolidatorDaemon } from './consolidator/daemon.js';\nimport {\n LifecycleDoubleStartError,\n LifecycleNotStartedError,\n ShutdownTimeoutError,\n} from './errors/index.js';\nimport type { HealthCheckOptions } from './health/index.js';\nimport type { ServerVariables } from './internal/context.js';\nimport { type LifecycleHooks, type OnErrorContext, runPreBind } from './lifecycle/index.js';\nimport { SERVER_METRIC_NAMES } from './metrics/catalog.js';\nimport type { MetricRegistry } from './metrics/registry.js';\nimport type { AgentRegistry, WorkflowRegistry } from './registry/index.js';\nimport type { ReplayApi } from './replay/index.js';\nimport type { AuditApi, McpApi, MemoryApi, SessionApi, SkillsApi } from './routes/index.js';\nimport { createConsoleRetentionLog, scheduleRetentionSweeps } from './runtime/retention.js';\nimport { type RunStateTracker, scheduleRunPruning } from './runtime/run-state.js';\nimport { bridgeToolAuditToAudit, type ToolAuditBridge } from './tools-audit-bridge.js';\nimport type { TriggersDaemon } from './triggers/daemon.js';\nimport type { WorkflowTimerDaemon } from './workflows/timer-daemon.js';\nimport type { WsDispatcher, WsTicketStore } from './ws/index.js';\nimport { scheduleReplayBufferPruning } from './ws/replay-buffer.js';\n\n/**\n * Subset of `CreateServerOptions` the lifecycle reads - hooks, test\n * escape hatches, and the domain adapters threaded into the routes.\n */\nexport interface ServerLifecycleOptions {\n readonly sessions?: SessionApi | undefined;\n readonly memory?: MemoryApi | undefined;\n readonly skills?: SkillsApi | undefined;\n readonly mcp?: McpApi | undefined;\n readonly audit?: AuditApi | undefined;\n readonly replay?: ReplayApi | undefined;\n readonly healthProbes?: (() => HealthCheckOptions | Promise<HealthCheckOptions>) | undefined;\n readonly hooks?: LifecycleHooks | undefined;\n readonly probeCipherPeer?: (() => Promise<void>) | undefined;\n readonly skipHardening?: boolean | undefined;\n readonly skipListen?: boolean | undefined;\n}\n\n/** Everything {@link createLifecycle} needs from the composition root. */\nexport interface ServerLifecycleDeps {\n readonly options: ServerLifecycleOptions;\n readonly config: ServerConfigSpec;\n readonly store: GraphorinSqliteStore;\n readonly app: Hono<{ Variables: ServerVariables }>;\n readonly metricRegistry: MetricRegistry;\n readonly runs: RunStateTracker;\n readonly agents: AgentRegistry;\n readonly workflows: WorkflowRegistry;\n readonly version: string;\n readonly startedAt: number;\n readonly now: () => number;\n readonly ws: WsLayer;\n readonly triggersDaemon: TriggersDaemon | undefined;\n readonly consolidatorDaemon: ConsolidatorDaemon | undefined;\n readonly workflowTimerDaemon: WorkflowTimerDaemon | undefined;\n}\n\n/**\n * The stateful `start()` / `stop()` pair plus the live views the\n * {@link GraphorinServer} getters expose (`listeningOn`,\n * `wsDispatcher`, `wsTickets` are cleared during `stop()`).\n */\nexport interface ServerLifecycle {\n readonly listeningOn: { readonly host: string; readonly port: number } | undefined;\n readonly wsDispatcher: WsDispatcher | undefined;\n readonly wsTickets: WsTicketStore | undefined;\n start(): Promise<{ readonly host: string; readonly port: number }>;\n stop(options?: { readonly force?: boolean }): Promise<void>;\n}\n\nexport function createLifecycle(deps: ServerLifecycleDeps): ServerLifecycle {\n const {\n options,\n config,\n store,\n app,\n metricRegistry,\n runs,\n agents,\n workflows,\n version,\n startedAt,\n now,\n triggersDaemon,\n consolidatorDaemon,\n workflowTimerDaemon,\n } = deps;\n const commentaryAuditSink = deps.ws.commentaryAuditSink;\n\n let serverInstance: ServerType | undefined;\n let listening: { readonly host: string; readonly port: number } | undefined;\n let started = false;\n let stopped = false;\n // IP-16: stops the periodic terminal-run prune sweep on shutdown.\n let stopRunPruning: (() => void) | undefined;\n // W-028: stops the periodic WS replay-buffer TTL sweep on shutdown.\n let stopReplayBufferPruning: (() => void) | undefined;\n // W-010: stops the unified retention sweep (spans, consolidator runs,\n // DLQ, idempotency + the opt-in content surfaces) on shutdown.\n let stopRetention: (() => void) | undefined;\n let auditDb: AuditDb | undefined;\n // W-051: tools/MCP audit-bus -> audit-chain bridge (process-global\n // subscription; must unsubscribe on stop()).\n let toolAuditBridge: ToolAuditBridge | undefined;\n let preBind: Awaited<ReturnType<typeof runPreBind>> | undefined;\n let verifier: TokenVerifier | undefined;\n let pepperHandle: import('@graphorin/security').SecretValue | undefined;\n\n let dispatcher = deps.ws.dispatcher;\n let tickets = deps.ws.tickets;\n let wsAdapter = deps.ws.wsAdapter;\n\n return {\n get listeningOn() {\n return listening;\n },\n get wsDispatcher() {\n return dispatcher;\n },\n get wsTickets() {\n return tickets;\n },\n async start(): Promise<{ readonly host: string; readonly port: number }> {\n if (started) throw new LifecycleDoubleStartError();\n started = true;\n try {\n if (options.hooks?.beforeStart !== undefined) {\n await options.hooks.beforeStart({ config });\n }\n if (options.skipHardening !== true && config.hardening.applyOnStart) {\n applyProcessHardening({\n refuseRoot: config.hardening.refuseRoot,\n umask: config.hardening.umask,\n });\n }\n preBind = await runPreBind({\n config,\n store,\n ...(options.probeCipherPeer !== undefined\n ? { probeCipherPeer: options.probeCipherPeer }\n : {}),\n });\n\n if (\n config.audit.enabled &&\n preBind.auditPassphrase !== undefined &&\n preBind.auditPath !== undefined\n ) {\n ensureStoreAuditBinding();\n auditDb = await openAuditDb({\n path: preBind.auditPath,\n passphrase: preBind.auditPassphrase,\n ...(config.audit.cipher !== undefined ? { cipher: config.audit.cipher } : {}),\n });\n // IP-21: now that the audit chain is open, route the WS dispatcher's\n // commentary-sanitizer decisions into it.\n commentaryAuditSink?.bind(bridgeCommentaryToAudit(auditDb));\n // W-051: subscribe the audit chain to the tools/MCP audit bus\n // (shadow-mode dataflow findings, approvals, collisions, ...).\n toolAuditBridge = bridgeToolAuditToAudit(auditDb, {\n policy: config.audit.toolEvents,\n });\n }\n\n if (config.auth.kind === 'token') {\n const pepper = preBind.pepper ?? (await fallbackPepper(options.skipHardening === true));\n pepperHandle = pepper;\n verifier = new TokenVerifier({\n tokenStore: store.authTokens,\n pepper,\n acceptPrefix: config.auth.tokenPrefix,\n acceptEnvironments: config.auth.tokenEnvironments,\n ...(config.auth.perIpFailureThreshold !== undefined\n ? { perIpFailureThreshold: config.auth.perIpFailureThreshold }\n : {}),\n ...(config.auth.perIpLockoutMs !== undefined\n ? { perIpLockoutMs: config.auth.perIpLockoutMs }\n : {}),\n now,\n });\n }\n\n mountRoutes(app, config, {\n version,\n startedAt,\n now,\n agents,\n workflows,\n runs,\n store,\n metricRegistry,\n ...(options.sessions !== undefined ? { sessions: options.sessions } : {}),\n ...(options.memory !== undefined ? { memory: options.memory } : {}),\n ...(options.skills !== undefined ? { skills: options.skills } : {}),\n ...(options.mcp !== undefined ? { mcp: options.mcp } : {}),\n ...(options.audit !== undefined ? { audit: options.audit } : {}),\n ...(options.replay !== undefined ? { replay: options.replay } : {}),\n ...(options.healthProbes !== undefined ? { healthProbes: options.healthProbes } : {}),\n ...(verifier !== undefined ? { verifier } : {}),\n ...(auditDb !== undefined ? { auditDb } : {}),\n ...(pepperHandle !== undefined ? { pepper: pepperHandle } : {}),\n ...(dispatcher !== undefined ? { wsDispatcher: dispatcher } : {}),\n ...(tickets !== undefined ? { wsTickets: tickets } : {}),\n ...(wsAdapter !== undefined ? { wsAdapter } : {}),\n ...(triggersDaemon !== undefined ? { triggersDaemon } : {}),\n ...(consolidatorDaemon !== undefined ? { consolidatorDaemon } : {}),\n ...(workflowTimerDaemon !== undefined ? { workflowTimerDaemon } : {}),\n });\n\n // Start the consolidator first so it is ready to handle fired\n // triggers, bridge its cron / idle triggers onto the scheduler\n // (MCON-4 - without this nothing pipes triggers into the\n // consolidator and background consolidation never runs), then start\n // the scheduler last so it only fires fully-wired triggers.\n if (consolidatorDaemon !== undefined) {\n await consolidatorDaemon.start();\n if (\n triggersDaemon !== undefined &&\n consolidatorDaemon.consolidator.registerWithScheduler !== undefined\n ) {\n await consolidatorDaemon.consolidator.registerWithScheduler(triggersDaemon.scheduler);\n }\n }\n if (triggersDaemon !== undefined) {\n await triggersDaemon.start();\n }\n // W-032: fire due durable timers from server start onward.\n if (workflowTimerDaemon !== undefined) {\n await workflowTimerDaemon.start();\n }\n\n // Sample a couple of gauges immediately so the very first\n // `/v1/metrics` scrape after start carries non-zero data.\n try {\n const wal = readWalSize(store.connection);\n metricRegistry.set(SERVER_METRIC_NAMES.storageWalSize, wal);\n } catch {\n // Best-effort.\n }\n metricRegistry.set(\n SERVER_METRIC_NAMES.serverUptime,\n Math.max(0, Math.floor((now() - startedAt) / 1000)),\n );\n metricRegistry.set(SERVER_METRIC_NAMES.inflightRuns, runs.runningCount());\n metricRegistry.set(SERVER_METRIC_NAMES.replayBufferEvents, 0);\n\n // IP-16: terminal run records (each holding an AbortController) would\n // otherwise accumulate forever; sweep them on a periodic timer.\n stopRunPruning = scheduleRunPruning(runs, now);\n\n // W-028: finished-run replay subjects (fresh runId per run, no\n // further push/replay activity) would otherwise hold up to\n // maxEvents payloads forever; sweep the buffer's TTL\n // periodically. Guarded: the dispatcher is absent on no-WS\n // configurations.\n if (dispatcher !== undefined) {\n stopReplayBufferPruning = scheduleReplayBufferPruning(dispatcher.replayBuffer, {\n intervalMs: config.server.stream.replayBuffer.pruneIntervalSeconds * 1000,\n });\n }\n\n // W-010 / W-008: the unified retention sweep over every SQLite\n // growth surface, including the W-061 idempotency sweep it\n // subsumes. Runs after runPreBind, so migrations have applied;\n // the first sweep fires immediately.\n const retentionLog = createConsoleRetentionLog(config.observability.logger);\n stopRetention = scheduleRetentionSweeps({\n store,\n config: config.retention,\n now,\n ...(retentionLog !== undefined ? { log: retentionLog } : {}),\n });\n\n if (options.skipListen !== true) {\n serverInstance = serve({\n fetch: app.fetch.bind(app),\n hostname: config.server.host,\n port: config.server.port,\n });\n if (wsAdapter !== undefined && serverInstance !== undefined) {\n wsAdapter.injectWebSocket(serverInstance);\n }\n await new Promise<void>((resolve) => {\n const server = serverInstance as unknown as {\n once(event: 'listening', cb: () => void): void;\n };\n if ('once' in server && typeof server.once === 'function') {\n server.once('listening', () => resolve());\n } else {\n // serve(...) returned a Node http.Server already bound.\n setImmediate(() => resolve());\n }\n });\n const address = (\n serverInstance as unknown as { address(): AddressInfo | string | null }\n ).address();\n if (address !== null && typeof address === 'object') {\n listening = { host: address.address, port: address.port };\n } else {\n listening = { host: config.server.host, port: config.server.port };\n }\n } else {\n listening = { host: config.server.host, port: config.server.port };\n }\n\n if (options.hooks?.onReady !== undefined) {\n await options.hooks.onReady({ config, listeningOn: listening });\n }\n return listening;\n } catch (err) {\n started = false;\n await emitError(options.hooks, { error: err, phase: 'beforeStart' });\n throw err;\n }\n },\n async stop({ force }: { readonly force?: boolean } = {}): Promise<void> {\n if (!started) throw new LifecycleNotStartedError();\n if (stopped) return;\n stopped = true;\n // IP-16: halt the prune sweep before draining.\n stopRunPruning?.();\n stopRunPruning = undefined;\n // W-028: halt the replay-buffer TTL sweep symmetrically.\n stopReplayBufferPruning?.();\n stopReplayBufferPruning = undefined;\n // W-010: halt the retention sweep symmetrically.\n stopRetention?.();\n stopRetention = undefined;\n // W-051: unsubscribe from the tools audit bus and let in-flight\n // audit writes settle before the audit DB closes below.\n if (toolAuditBridge !== undefined) {\n toolAuditBridge.stop();\n try {\n await toolAuditBridge.drain();\n } catch {\n // Write errors were already surfaced by the bridge itself.\n }\n toolAuditBridge = undefined;\n }\n const drainTimeoutMs = force === true ? 0 : config.server.shutdown.drainTimeoutMs;\n try {\n if (options.hooks?.beforeShutdown !== undefined) {\n await options.hooks.beforeShutdown({\n config,\n inflight: runs.inflightCount(),\n drainTimeoutMs,\n });\n }\n } catch (err) {\n await emitError(options.hooks, { error: err, phase: 'beforeShutdown' });\n }\n\n if (workflowTimerDaemon !== undefined) {\n try {\n await workflowTimerDaemon.stop();\n } catch (err) {\n await emitError(options.hooks, { error: err, phase: 'beforeShutdown' });\n }\n }\n if (triggersDaemon !== undefined) {\n try {\n await triggersDaemon.stop();\n } catch (err) {\n await emitError(options.hooks, { error: err, phase: 'beforeShutdown' });\n }\n }\n if (consolidatorDaemon !== undefined) {\n try {\n await consolidatorDaemon.stop();\n } catch (err) {\n await emitError(options.hooks, { error: err, phase: 'beforeShutdown' });\n }\n }\n\n // Pending reservations (e.g. awaited WS subscriptions for the\n // streaming endpoints) hold no work in progress; abort them\n // immediately so the drain only waits for live runs.\n runs.abortPending('server-shutdown');\n const drained = await drainInFlight(runs, drainTimeoutMs, now);\n if (!drained && drainTimeoutMs > 0) {\n // Force-abort everything that didn't drain in time.\n runs.abortAll(new ShutdownTimeoutError(drainTimeoutMs, runs.runningCount()));\n }\n\n if (dispatcher !== undefined) {\n try {\n dispatcher.shutdown();\n } catch {\n // Best-effort during stop().\n }\n }\n\n if (serverInstance !== undefined) {\n await new Promise<void>((resolve) => {\n (serverInstance as unknown as { close(cb: () => void): void }).close(() => resolve());\n });\n serverInstance = undefined;\n }\n if (auditDb !== undefined) {\n await auditDb.close();\n auditDb = undefined;\n }\n await store.close();\n listening = undefined;\n verifier = undefined;\n started = false;\n preBind = undefined;\n dispatcher = undefined;\n tickets = undefined;\n wsAdapter = undefined;\n },\n };\n}\n\nasync function drainInFlight(\n runs: RunStateTracker,\n drainTimeoutMs: number,\n now: () => number,\n): Promise<boolean> {\n if (runs.runningCount() === 0) return true;\n if (drainTimeoutMs <= 0) return runs.runningCount() === 0;\n const deadline = now() + drainTimeoutMs;\n while (now() < deadline) {\n if (runs.runningCount() === 0) return true;\n await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - now())));\n }\n return runs.runningCount() === 0;\n}\n\nasync function emitError(hooks: LifecycleHooks | undefined, ctx: OnErrorContext): Promise<void> {\n if (hooks?.onError === undefined) return;\n try {\n await hooks.onError(ctx);\n } catch {\n // onError must never throw further; swallow.\n }\n}\n\nasync function fallbackPepper(skipHardening: boolean) {\n // Test-only path: when `skipHardening` is set we mint an ephemeral\n // pepper so the verifier is constructible without a real keyring.\n if (!skipHardening) {\n throw new Error(\n '[graphorin/server] missing resolved pepper after pre-bind; auth.pepperRef is required when auth.kind = token.',\n );\n }\n return generatePepper();\n}\n"],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import { SERVER_METRIC_NAMES } from "./metrics/catalog.js";
2
+ import { syncToolCounters } from "./metrics/tools-bridge.js";
3
+ import { readWalSize } from "@graphorin/store-sqlite";
4
+
5
+ //#region src/app-metrics.ts
6
+ /**
7
+ * Live-metrics refresh for the `/v1/metrics` exposition - samples the
8
+ * WAL size, uptime, in-flight runs, replay-buffer occupancy, trigger
9
+ * fire counters, and consolidator gauges on every scrape. Wired into
10
+ * the metrics route by `app-routes.ts`.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ async function refreshLiveMetrics(options) {
15
+ const { registry, store, runs, startedAt, now } = options;
16
+ syncToolCounters(registry);
17
+ try {
18
+ const wal = readWalSize(store.connection);
19
+ registry.set(SERVER_METRIC_NAMES.storageWalSize, wal);
20
+ } catch {}
21
+ registry.set(SERVER_METRIC_NAMES.serverUptime, Math.max(0, Math.floor((now() - startedAt) / 1e3)));
22
+ registry.set(SERVER_METRIC_NAMES.inflightRuns, runs.runningCount());
23
+ if (options.wsDispatcher !== void 0) registry.set(SERVER_METRIC_NAMES.replayBufferEvents, options.wsDispatcher.replayBuffer.stats?.().events ?? 0);
24
+ if (options.triggersDaemon !== void 0) {
25
+ const metrics = options.triggersDaemon.metrics();
26
+ for (const [triggerId, counts] of metrics.fires) {
27
+ const sanitized = sanitizeMetricLabelValue(triggerId);
28
+ const successCurrent = readCounter(registry, SERVER_METRIC_NAMES.triggersFiresTotal, {
29
+ trigger_id: sanitized,
30
+ status: "success"
31
+ });
32
+ const errorCurrent = readCounter(registry, SERVER_METRIC_NAMES.triggersFiresTotal, {
33
+ trigger_id: sanitized,
34
+ status: "error"
35
+ });
36
+ const successDelta = counts.success - successCurrent;
37
+ const errorDelta = counts.error - errorCurrent;
38
+ if (successDelta > 0) registry.inc(SERVER_METRIC_NAMES.triggersFiresTotal, {
39
+ trigger_id: sanitized,
40
+ status: "success"
41
+ }, successDelta);
42
+ if (errorDelta > 0) registry.inc(SERVER_METRIC_NAMES.triggersFiresTotal, {
43
+ trigger_id: sanitized,
44
+ status: "error"
45
+ }, errorDelta);
46
+ }
47
+ }
48
+ if (options.consolidatorDaemon !== void 0) try {
49
+ const status = await options.consolidatorDaemon.status();
50
+ registry.set(SERVER_METRIC_NAMES.consolidatorQueueDepth, status.queueDepth, { phase: "aggregate" });
51
+ registry.set(SERVER_METRIC_NAMES.consolidatorDlqSize, status.dlqSize);
52
+ registry.set(SERVER_METRIC_NAMES.consolidatorBudgetRemainingUsd, status.budget.costRemaining);
53
+ registry.set(SERVER_METRIC_NAMES.consolidatorBudgetRemainingTokens, status.budget.tokensRemaining);
54
+ } catch {}
55
+ }
56
+ /**
57
+ * Convert an arbitrary user-supplied identifier (trigger id) into a
58
+ * Prometheus-safe label value. Replaces every character outside the
59
+ * `[A-Za-z0-9_:]` range with `_`. This guarantees the cardinality
60
+ * never explodes on UTF-8 sequences while keeping the value
61
+ * recognizable.
62
+ */
63
+ function sanitizeMetricLabelValue(value) {
64
+ return value.replace(/[^A-Za-z0-9_:.-]/g, "_").slice(0, 200);
65
+ }
66
+ function readCounter(registry, name, labels) {
67
+ const snap = registry.snapshot().counters[name] ?? [];
68
+ for (const entry of snap) if (matchesLabels(entry.labels, labels)) return entry.value;
69
+ return 0;
70
+ }
71
+ function matchesLabels(a, b) {
72
+ for (const k of Object.keys(b)) if (String(a[k]) !== b[k]) return false;
73
+ for (const k of Object.keys(a)) if (!(k in b)) return false;
74
+ return true;
75
+ }
76
+
77
+ //#endregion
78
+ export { refreshLiveMetrics };
79
+ //# sourceMappingURL=app-metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-metrics.js","names":[],"sources":["../src/app-metrics.ts"],"sourcesContent":["/**\n * Live-metrics refresh for the `/v1/metrics` exposition - samples the\n * WAL size, uptime, in-flight runs, replay-buffer occupancy, trigger\n * fire counters, and consolidator gauges on every scrape. Wired into\n * the metrics route by `app-routes.ts`.\n *\n * @packageDocumentation\n */\n\nimport { type GraphorinSqliteStore, readWalSize } from '@graphorin/store-sqlite';\nimport type { ConsolidatorDaemon } from './consolidator/daemon.js';\nimport { SERVER_METRIC_NAMES } from './metrics/catalog.js';\nimport type { MetricRegistry } from './metrics/registry.js';\nimport { syncToolCounters } from './metrics/tools-bridge.js';\nimport type { RunStateTracker } from './runtime/run-state.js';\nimport type { TriggersDaemon } from './triggers/daemon.js';\nimport type { WsDispatcher } from './ws/index.js';\n\nexport interface RefreshLiveMetricsOptions {\n readonly registry: MetricRegistry;\n readonly store: GraphorinSqliteStore;\n readonly runs: RunStateTracker;\n readonly startedAt: number;\n readonly now: () => number;\n readonly triggersDaemon?: TriggersDaemon;\n readonly consolidatorDaemon?: ConsolidatorDaemon;\n readonly wsDispatcher?: WsDispatcher;\n}\n\nexport async function refreshLiveMetrics(options: RefreshLiveMetricsOptions): Promise<void> {\n const { registry, store, runs, startedAt, now } = options;\n\n // W-051: fold the tools/MCP module counters (dataflow shadow signals,\n // executor retries, reconnects, ...) into the scrape.\n syncToolCounters(registry);\n\n try {\n const wal = readWalSize(store.connection);\n registry.set(SERVER_METRIC_NAMES.storageWalSize, wal);\n } catch {\n // Best-effort.\n }\n\n registry.set(\n SERVER_METRIC_NAMES.serverUptime,\n Math.max(0, Math.floor((now() - startedAt) / 1000)),\n );\n registry.set(SERVER_METRIC_NAMES.inflightRuns, runs.runningCount());\n\n if (options.wsDispatcher !== undefined) {\n // W-028: the gauge now reports buffered EVENTS, matching its name -\n // previously it was filled with the subscription count. `stats` is\n // optional on the @stable ReplayBuffer interface, so external\n // implementations without it degrade to 0 instead of throwing.\n registry.set(\n SERVER_METRIC_NAMES.replayBufferEvents,\n options.wsDispatcher.replayBuffer.stats?.().events ?? 0,\n );\n }\n\n if (options.triggersDaemon !== undefined) {\n const metrics = options.triggersDaemon.metrics();\n for (const [triggerId, counts] of metrics.fires) {\n const sanitized = sanitizeMetricLabelValue(triggerId);\n const successCurrent = readCounter(registry, SERVER_METRIC_NAMES.triggersFiresTotal, {\n trigger_id: sanitized,\n status: 'success',\n });\n const errorCurrent = readCounter(registry, SERVER_METRIC_NAMES.triggersFiresTotal, {\n trigger_id: sanitized,\n status: 'error',\n });\n const successDelta = counts.success - successCurrent;\n const errorDelta = counts.error - errorCurrent;\n if (successDelta > 0) {\n registry.inc(\n SERVER_METRIC_NAMES.triggersFiresTotal,\n { trigger_id: sanitized, status: 'success' },\n successDelta,\n );\n }\n if (errorDelta > 0) {\n registry.inc(\n SERVER_METRIC_NAMES.triggersFiresTotal,\n { trigger_id: sanitized, status: 'error' },\n errorDelta,\n );\n }\n }\n }\n\n if (options.consolidatorDaemon !== undefined) {\n try {\n const status = await options.consolidatorDaemon.status();\n registry.set(SERVER_METRIC_NAMES.consolidatorQueueDepth, status.queueDepth, {\n phase: 'aggregate',\n });\n registry.set(SERVER_METRIC_NAMES.consolidatorDlqSize, status.dlqSize);\n registry.set(SERVER_METRIC_NAMES.consolidatorBudgetRemainingUsd, status.budget.costRemaining);\n registry.set(\n SERVER_METRIC_NAMES.consolidatorBudgetRemainingTokens,\n status.budget.tokensRemaining,\n );\n } catch {\n // Best-effort.\n }\n }\n}\n\n/**\n * Convert an arbitrary user-supplied identifier (trigger id) into a\n * Prometheus-safe label value. Replaces every character outside the\n * `[A-Za-z0-9_:]` range with `_`. This guarantees the cardinality\n * never explodes on UTF-8 sequences while keeping the value\n * recognizable.\n */\nfunction sanitizeMetricLabelValue(value: string): string {\n return value.replace(/[^A-Za-z0-9_:.-]/g, '_').slice(0, 200);\n}\n\nfunction readCounter(\n registry: MetricRegistry,\n name: string,\n labels: Record<string, string>,\n): number {\n const snap = registry.snapshot().counters[name] ?? [];\n for (const entry of snap) {\n if (matchesLabels(entry.labels, labels)) return entry.value;\n }\n return 0;\n}\n\nfunction matchesLabels(\n a: Record<string, string | number | boolean>,\n b: Record<string, string>,\n): boolean {\n for (const k of Object.keys(b)) {\n if (String(a[k]) !== b[k]) return false;\n }\n for (const k of Object.keys(a)) {\n if (!(k in b)) return false;\n }\n return true;\n}\n"],"mappings":";;;;;;;;;;;;;AA6BA,eAAsB,mBAAmB,SAAmD;CAC1F,MAAM,EAAE,UAAU,OAAO,MAAM,WAAW,QAAQ;AAIlD,kBAAiB,SAAS;AAE1B,KAAI;EACF,MAAM,MAAM,YAAY,MAAM,WAAW;AACzC,WAAS,IAAI,oBAAoB,gBAAgB,IAAI;SAC/C;AAIR,UAAS,IACP,oBAAoB,cACpB,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,GAAG,aAAa,IAAK,CAAC,CACpD;AACD,UAAS,IAAI,oBAAoB,cAAc,KAAK,cAAc,CAAC;AAEnE,KAAI,QAAQ,iBAAiB,OAK3B,UAAS,IACP,oBAAoB,oBACpB,QAAQ,aAAa,aAAa,SAAS,CAAC,UAAU,EACvD;AAGH,KAAI,QAAQ,mBAAmB,QAAW;EACxC,MAAM,UAAU,QAAQ,eAAe,SAAS;AAChD,OAAK,MAAM,CAAC,WAAW,WAAW,QAAQ,OAAO;GAC/C,MAAM,YAAY,yBAAyB,UAAU;GACrD,MAAM,iBAAiB,YAAY,UAAU,oBAAoB,oBAAoB;IACnF,YAAY;IACZ,QAAQ;IACT,CAAC;GACF,MAAM,eAAe,YAAY,UAAU,oBAAoB,oBAAoB;IACjF,YAAY;IACZ,QAAQ;IACT,CAAC;GACF,MAAM,eAAe,OAAO,UAAU;GACtC,MAAM,aAAa,OAAO,QAAQ;AAClC,OAAI,eAAe,EACjB,UAAS,IACP,oBAAoB,oBACpB;IAAE,YAAY;IAAW,QAAQ;IAAW,EAC5C,aACD;AAEH,OAAI,aAAa,EACf,UAAS,IACP,oBAAoB,oBACpB;IAAE,YAAY;IAAW,QAAQ;IAAS,EAC1C,WACD;;;AAKP,KAAI,QAAQ,uBAAuB,OACjC,KAAI;EACF,MAAM,SAAS,MAAM,QAAQ,mBAAmB,QAAQ;AACxD,WAAS,IAAI,oBAAoB,wBAAwB,OAAO,YAAY,EAC1E,OAAO,aACR,CAAC;AACF,WAAS,IAAI,oBAAoB,qBAAqB,OAAO,QAAQ;AACrE,WAAS,IAAI,oBAAoB,gCAAgC,OAAO,OAAO,cAAc;AAC7F,WAAS,IACP,oBAAoB,mCACpB,OAAO,OAAO,gBACf;SACK;;;;;;;;;AAaZ,SAAS,yBAAyB,OAAuB;AACvD,QAAO,MAAM,QAAQ,qBAAqB,IAAI,CAAC,MAAM,GAAG,IAAI;;AAG9D,SAAS,YACP,UACA,MACA,QACQ;CACR,MAAM,OAAO,SAAS,UAAU,CAAC,SAAS,SAAS,EAAE;AACrD,MAAK,MAAM,SAAS,KAClB,KAAI,cAAc,MAAM,QAAQ,OAAO,CAAE,QAAO,MAAM;AAExD,QAAO;;AAGT,SAAS,cACP,GACA,GACS;AACT,MAAK,MAAM,KAAK,OAAO,KAAK,EAAE,CAC5B,KAAI,OAAO,EAAE,GAAG,KAAK,EAAE,GAAI,QAAO;AAEpC,MAAK,MAAM,KAAK,OAAO,KAAK,EAAE,CAC5B,KAAI,EAAE,KAAK,GAAI,QAAO;AAExB,QAAO"}
@@ -0,0 +1,92 @@
1
+ import { createAuditMiddleware } from "./middleware/audit.js";
2
+ import { createAnonymousAuthMiddleware, createAuthMiddleware } from "./middleware/auth.js";
3
+ import { createCorsMiddleware } from "./middleware/cors.js";
4
+ import { createCsrfMiddleware } from "./middleware/csrf.js";
5
+ import { createIdempotencyMiddleware } from "./middleware/idempotency.js";
6
+ import { createRateLimitMiddleware } from "./middleware/rate-limit.js";
7
+ import { createRequestStateMiddleware } from "./middleware/request-state.js";
8
+
9
+ //#region src/app-middleware.ts
10
+ /**
11
+ * Global middleware in the documented order. Per-route scope +
12
+ * idempotency layers are mounted by the route factories so the
13
+ * composition stays declarative + auditable.
14
+ */
15
+ function attachGlobalMiddleware(app, config, now) {
16
+ app.use("*", createRequestStateMiddleware({
17
+ now,
18
+ ...config.server.trustProxy ? { trustProxy: true } : {}
19
+ }));
20
+ app.use("*", createCorsMiddleware(config.server.cors));
21
+ app.use("*", createCsrfMiddleware(config.server.csrf));
22
+ app.use("*", createRateLimitMiddleware(config.server.rateLimit, { now }));
23
+ }
24
+ /**
25
+ * Authenticated subtree begins here. The health endpoint above is
26
+ * intentionally outside the auth boundary so liveness probes work
27
+ * before the token verifier is wired. The WebSocket upgrade path is
28
+ * also exempt: the upgrade handler in `ws/upgrade.ts` performs its
29
+ * own bearer + ticket validation inline (the HTTP auth middleware
30
+ * would otherwise short-circuit the upgrade with a 401 response
31
+ * before the handler can negotiate the subprotocol).
32
+ */
33
+ function attachProtectedMiddleware(app, config, ctx) {
34
+ const base = config.server.basePath;
35
+ const wsUpgradePath = config.server.ws.enabled ? `${base}${config.server.ws.path}` : void 0;
36
+ const metricsPath = config.metrics.enabled && !config.metrics.requireAuth ? `${base}${config.metrics.path}` : void 0;
37
+ function shouldSkipAuth(path) {
38
+ if (path === `${base}/health` || path === `${base}/health/`) return true;
39
+ if (wsUpgradePath !== void 0) {
40
+ if (path === wsUpgradePath || path === `${wsUpgradePath}/`) return true;
41
+ }
42
+ if (metricsPath !== void 0) {
43
+ if (path === metricsPath || path === `${metricsPath}/`) return true;
44
+ }
45
+ return false;
46
+ }
47
+ const anonymousAuth = ctx.verifier === void 0 && config.auth.kind === "none";
48
+ if (ctx.verifier !== void 0 || anonymousAuth) {
49
+ const authMw = ctx.verifier !== void 0 ? createAuthMiddleware({ verifier: ctx.verifier }) : createAnonymousAuthMiddleware();
50
+ app.use(`${base}/*`, async (c, next) => {
51
+ if (shouldSkipAuth(c.req.path)) {
52
+ await next();
53
+ return;
54
+ }
55
+ return authMw(c, next);
56
+ });
57
+ if (ctx.auditDb !== void 0) {
58
+ const auditMw = createAuditMiddleware({
59
+ auditDb: ctx.auditDb,
60
+ now: ctx.now
61
+ });
62
+ app.use(`${base}/*`, async (c, next) => {
63
+ if (shouldSkipAuth(c.req.path)) {
64
+ await next();
65
+ return;
66
+ }
67
+ return auditMw(c, next);
68
+ });
69
+ }
70
+ }
71
+ if (config.server.idempotency.enabled) {
72
+ const idempotencyMw = createIdempotencyMiddleware({
73
+ store: ctx.store.idempotency,
74
+ config: config.server.idempotency,
75
+ now: ctx.now,
76
+ excludeResponseCachePaths: [`${base}/tokens`],
77
+ metricRegistry: ctx.metricRegistry
78
+ });
79
+ app.use(`${base}/*`, async (c, next) => {
80
+ if (shouldSkipAuth(c.req.path)) {
81
+ await next();
82
+ return;
83
+ }
84
+ return idempotencyMw(c, next);
85
+ });
86
+ }
87
+ return { anonymousAuth };
88
+ }
89
+
90
+ //#endregion
91
+ export { attachGlobalMiddleware, attachProtectedMiddleware };
92
+ //# sourceMappingURL=app-middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-middleware.js","names":[],"sources":["../src/app-middleware.ts"],"sourcesContent":["/**\n * Middleware wiring for `createServer({...})`.\n *\n * Two attachment points, called in this order by the composition:\n *\n * 1. {@link attachGlobalMiddleware} - request-state / CORS / CSRF /\n * rate-limit, mounted on `*` at app construction time.\n * 2. {@link attachProtectedMiddleware} - auth + audit + idempotency\n * for the authenticated subtree, mounted by `mountRoutes` after\n * the unauthenticated health / metrics endpoints.\n *\n * The relative order of these layers is part of the documented\n * contract - do not reorder.\n *\n * @packageDocumentation\n */\n\nimport type { TokenVerifier } from '@graphorin/security';\nimport type { AuditDb } from '@graphorin/security/audit';\nimport type { GraphorinSqliteStore } from '@graphorin/store-sqlite';\nimport type { Hono } from 'hono';\nimport type { ServerConfigSpec } from './config.js';\nimport type { ServerVariables } from './internal/context.js';\nimport type { MetricRegistry } from './metrics/registry.js';\nimport {\n createAnonymousAuthMiddleware,\n createAuditMiddleware,\n createAuthMiddleware,\n createCorsMiddleware,\n createCsrfMiddleware,\n createIdempotencyMiddleware,\n createRateLimitMiddleware,\n createRequestStateMiddleware,\n} from './middleware/index.js';\n\n/**\n * Global middleware in the documented order. Per-route scope +\n * idempotency layers are mounted by the route factories so the\n * composition stays declarative + auditable.\n */\nexport function attachGlobalMiddleware(\n app: Hono<{ Variables: ServerVariables }>,\n config: ServerConfigSpec,\n now: () => number,\n): void {\n app.use(\n '*',\n createRequestStateMiddleware({\n now,\n ...(config.server.trustProxy ? { trustProxy: true } : {}),\n }),\n );\n app.use('*', createCorsMiddleware(config.server.cors));\n app.use('*', createCsrfMiddleware(config.server.csrf));\n app.use('*', createRateLimitMiddleware(config.server.rateLimit, { now }));\n}\n\n/** Dependencies consumed by {@link attachProtectedMiddleware}. */\nexport interface ProtectedMiddlewareContext {\n readonly now: () => number;\n readonly store: GraphorinSqliteStore;\n readonly metricRegistry: MetricRegistry;\n readonly verifier?: TokenVerifier | undefined;\n readonly auditDb?: AuditDb | undefined;\n}\n\n/**\n * Authenticated subtree begins here. The health endpoint above is\n * intentionally outside the auth boundary so liveness probes work\n * before the token verifier is wired. The WebSocket upgrade path is\n * also exempt: the upgrade handler in `ws/upgrade.ts` performs its\n * own bearer + ticket validation inline (the HTTP auth middleware\n * would otherwise short-circuit the upgrade with a 401 response\n * before the handler can negotiate the subprotocol).\n */\nexport function attachProtectedMiddleware(\n app: Hono<{ Variables: ServerVariables }>,\n config: ServerConfigSpec,\n ctx: ProtectedMiddlewareContext,\n): { readonly anonymousAuth: boolean } {\n const base = config.server.basePath;\n const wsUpgradePath = config.server.ws.enabled ? `${base}${config.server.ws.path}` : undefined;\n const metricsPath =\n config.metrics.enabled && !config.metrics.requireAuth\n ? `${base}${config.metrics.path}`\n : undefined;\n function shouldSkipAuth(path: string): boolean {\n if (path === `${base}/health` || path === `${base}/health/`) return true;\n if (wsUpgradePath !== undefined) {\n if (path === wsUpgradePath || path === `${wsUpgradePath}/`) return true;\n }\n if (metricsPath !== undefined) {\n if (path === metricsPath || path === `${metricsPath}/`) return true;\n }\n return false;\n }\n // IP-13: in the no-auth loopback mode (`auth.kind='none'`) there is no\n // verifier, but the authenticated subtree must still be reachable - install\n // an anonymous middleware that stamps a fully-authorized principal so the\n // domain routes serve instead of every one of them returning 401.\n const anonymousAuth = ctx.verifier === undefined && config.auth.kind === 'none';\n if (ctx.verifier !== undefined || anonymousAuth) {\n const authMw =\n ctx.verifier !== undefined\n ? createAuthMiddleware({ verifier: ctx.verifier })\n : createAnonymousAuthMiddleware();\n app.use(`${base}/*`, async (c, next) => {\n if (shouldSkipAuth(c.req.path)) {\n await next();\n return;\n }\n return authMw(c, next);\n });\n if (ctx.auditDb !== undefined) {\n const auditMw = createAuditMiddleware({ auditDb: ctx.auditDb, now: ctx.now });\n app.use(`${base}/*`, async (c, next) => {\n if (shouldSkipAuth(c.req.path)) {\n await next();\n return;\n }\n return auditMw(c, next);\n });\n }\n }\n\n // Idempotency middleware - applied to side-effecting endpoints. We\n // mount it once on the authenticated subtree so handlers don't have\n // to duplicate the configuration per-route.\n if (config.server.idempotency.enabled) {\n const idempotencyMw = createIdempotencyMiddleware({\n store: ctx.store.idempotency,\n config: config.server.idempotency,\n now: ctx.now,\n // IP-6: token minting returns a raw secret - never cache it.\n excludeResponseCachePaths: [`${base}/tokens`],\n // IP-15: publish the live cache hit ratio gauge.\n metricRegistry: ctx.metricRegistry,\n });\n app.use(`${base}/*`, async (c, next) => {\n if (shouldSkipAuth(c.req.path)) {\n await next();\n return;\n }\n return idempotencyMw(c, next);\n });\n }\n\n return { anonymousAuth };\n}\n"],"mappings":";;;;;;;;;;;;;;AAwCA,SAAgB,uBACd,KACA,QACA,KACM;AACN,KAAI,IACF,KACA,6BAA6B;EAC3B;EACA,GAAI,OAAO,OAAO,aAAa,EAAE,YAAY,MAAM,GAAG,EAAE;EACzD,CAAC,CACH;AACD,KAAI,IAAI,KAAK,qBAAqB,OAAO,OAAO,KAAK,CAAC;AACtD,KAAI,IAAI,KAAK,qBAAqB,OAAO,OAAO,KAAK,CAAC;AACtD,KAAI,IAAI,KAAK,0BAA0B,OAAO,OAAO,WAAW,EAAE,KAAK,CAAC,CAAC;;;;;;;;;;;AAqB3E,SAAgB,0BACd,KACA,QACA,KACqC;CACrC,MAAM,OAAO,OAAO,OAAO;CAC3B,MAAM,gBAAgB,OAAO,OAAO,GAAG,UAAU,GAAG,OAAO,OAAO,OAAO,GAAG,SAAS;CACrF,MAAM,cACJ,OAAO,QAAQ,WAAW,CAAC,OAAO,QAAQ,cACtC,GAAG,OAAO,OAAO,QAAQ,SACzB;CACN,SAAS,eAAe,MAAuB;AAC7C,MAAI,SAAS,GAAG,KAAK,YAAY,SAAS,GAAG,KAAK,UAAW,QAAO;AACpE,MAAI,kBAAkB,QACpB;OAAI,SAAS,iBAAiB,SAAS,GAAG,cAAc,GAAI,QAAO;;AAErE,MAAI,gBAAgB,QAClB;OAAI,SAAS,eAAe,SAAS,GAAG,YAAY,GAAI,QAAO;;AAEjE,SAAO;;CAMT,MAAM,gBAAgB,IAAI,aAAa,UAAa,OAAO,KAAK,SAAS;AACzE,KAAI,IAAI,aAAa,UAAa,eAAe;EAC/C,MAAM,SACJ,IAAI,aAAa,SACb,qBAAqB,EAAE,UAAU,IAAI,UAAU,CAAC,GAChD,+BAA+B;AACrC,MAAI,IAAI,GAAG,KAAK,KAAK,OAAO,GAAG,SAAS;AACtC,OAAI,eAAe,EAAE,IAAI,KAAK,EAAE;AAC9B,UAAM,MAAM;AACZ;;AAEF,UAAO,OAAO,GAAG,KAAK;IACtB;AACF,MAAI,IAAI,YAAY,QAAW;GAC7B,MAAM,UAAU,sBAAsB;IAAE,SAAS,IAAI;IAAS,KAAK,IAAI;IAAK,CAAC;AAC7E,OAAI,IAAI,GAAG,KAAK,KAAK,OAAO,GAAG,SAAS;AACtC,QAAI,eAAe,EAAE,IAAI,KAAK,EAAE;AAC9B,WAAM,MAAM;AACZ;;AAEF,WAAO,QAAQ,GAAG,KAAK;KACvB;;;AAON,KAAI,OAAO,OAAO,YAAY,SAAS;EACrC,MAAM,gBAAgB,4BAA4B;GAChD,OAAO,IAAI,MAAM;GACjB,QAAQ,OAAO,OAAO;GACtB,KAAK,IAAI;GAET,2BAA2B,CAAC,GAAG,KAAK,SAAS;GAE7C,gBAAgB,IAAI;GACrB,CAAC;AACF,MAAI,IAAI,GAAG,KAAK,KAAK,OAAO,GAAG,SAAS;AACtC,OAAI,eAAe,EAAE,IAAI,KAAK,EAAE;AAC9B,UAAM,MAAM;AACZ;;AAEF,UAAO,cAAc,GAAG,KAAK;IAC7B;;AAGJ,QAAO,EAAE,eAAe"}