@graphorin/server 0.5.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 (204) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -0
  4. package/dist/app.d.ts +174 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +684 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/commentary/audit-bridge.d.ts +57 -0
  9. package/dist/commentary/audit-bridge.d.ts.map +1 -0
  10. package/dist/commentary/audit-bridge.js +92 -0
  11. package/dist/commentary/audit-bridge.js.map +1 -0
  12. package/dist/commentary/built-in-patterns.d.ts +24 -0
  13. package/dist/commentary/built-in-patterns.d.ts.map +1 -0
  14. package/dist/commentary/built-in-patterns.js +62 -0
  15. package/dist/commentary/built-in-patterns.js.map +1 -0
  16. package/dist/commentary/index.d.ts +5 -0
  17. package/dist/commentary/index.js +5 -0
  18. package/dist/commentary/sanitizer.d.ts +37 -0
  19. package/dist/commentary/sanitizer.d.ts.map +1 -0
  20. package/dist/commentary/sanitizer.js +182 -0
  21. package/dist/commentary/sanitizer.js.map +1 -0
  22. package/dist/commentary/types.d.ts +120 -0
  23. package/dist/commentary/types.d.ts.map +1 -0
  24. package/dist/config.d.ts +760 -0
  25. package/dist/config.d.ts.map +1 -0
  26. package/dist/config.js +217 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/consolidator/daemon.d.ts +88 -0
  29. package/dist/consolidator/daemon.d.ts.map +1 -0
  30. package/dist/consolidator/daemon.js +54 -0
  31. package/dist/consolidator/daemon.js.map +1 -0
  32. package/dist/consolidator/index.d.ts +2 -0
  33. package/dist/consolidator/index.js +3 -0
  34. package/dist/errors/index.d.ts +104 -0
  35. package/dist/errors/index.d.ts.map +1 -0
  36. package/dist/errors/index.js +131 -0
  37. package/dist/errors/index.js.map +1 -0
  38. package/dist/health/checks.d.ts +120 -0
  39. package/dist/health/checks.d.ts.map +1 -0
  40. package/dist/health/checks.js +127 -0
  41. package/dist/health/checks.js.map +1 -0
  42. package/dist/health/index.d.ts +3 -0
  43. package/dist/health/index.js +4 -0
  44. package/dist/health/routes.d.ts +66 -0
  45. package/dist/health/routes.d.ts.map +1 -0
  46. package/dist/health/routes.js +96 -0
  47. package/dist/health/routes.js.map +1 -0
  48. package/dist/index.d.ts +88 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +86 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/internal/context.d.ts +66 -0
  53. package/dist/internal/context.d.ts.map +1 -0
  54. package/dist/internal/ids.js +21 -0
  55. package/dist/internal/ids.js.map +1 -0
  56. package/dist/internal/json.js +46 -0
  57. package/dist/internal/json.js.map +1 -0
  58. package/dist/lifecycle/hooks.d.ts +57 -0
  59. package/dist/lifecycle/hooks.d.ts.map +1 -0
  60. package/dist/lifecycle/index.d.ts +2 -0
  61. package/dist/lifecycle/index.js +3 -0
  62. package/dist/lifecycle/pre-bind.d.ts +38 -0
  63. package/dist/lifecycle/pre-bind.d.ts.map +1 -0
  64. package/dist/lifecycle/pre-bind.js +62 -0
  65. package/dist/lifecycle/pre-bind.js.map +1 -0
  66. package/dist/metrics/catalog.d.ts +34 -0
  67. package/dist/metrics/catalog.d.ts.map +1 -0
  68. package/dist/metrics/catalog.js +61 -0
  69. package/dist/metrics/catalog.js.map +1 -0
  70. package/dist/metrics/index.d.ts +3 -0
  71. package/dist/metrics/index.js +4 -0
  72. package/dist/metrics/registry.d.ts +63 -0
  73. package/dist/metrics/registry.d.ts.map +1 -0
  74. package/dist/metrics/registry.js +222 -0
  75. package/dist/metrics/registry.js.map +1 -0
  76. package/dist/middleware/audit.d.ts +47 -0
  77. package/dist/middleware/audit.d.ts.map +1 -0
  78. package/dist/middleware/audit.js +71 -0
  79. package/dist/middleware/audit.js.map +1 -0
  80. package/dist/middleware/auth.d.ts +50 -0
  81. package/dist/middleware/auth.d.ts.map +1 -0
  82. package/dist/middleware/auth.js +164 -0
  83. package/dist/middleware/auth.js.map +1 -0
  84. package/dist/middleware/cors.d.ts +15 -0
  85. package/dist/middleware/cors.d.ts.map +1 -0
  86. package/dist/middleware/cors.js +45 -0
  87. package/dist/middleware/cors.js.map +1 -0
  88. package/dist/middleware/csrf.d.ts +15 -0
  89. package/dist/middleware/csrf.d.ts.map +1 -0
  90. package/dist/middleware/csrf.js +77 -0
  91. package/dist/middleware/csrf.js.map +1 -0
  92. package/dist/middleware/idempotency.d.ts +41 -0
  93. package/dist/middleware/idempotency.d.ts.map +1 -0
  94. package/dist/middleware/idempotency.js +198 -0
  95. package/dist/middleware/idempotency.js.map +1 -0
  96. package/dist/middleware/index.d.ts +9 -0
  97. package/dist/middleware/index.js +10 -0
  98. package/dist/middleware/rate-limit.d.ts +17 -0
  99. package/dist/middleware/rate-limit.d.ts.map +1 -0
  100. package/dist/middleware/rate-limit.js +47 -0
  101. package/dist/middleware/rate-limit.js.map +1 -0
  102. package/dist/middleware/request-state.d.ts +27 -0
  103. package/dist/middleware/request-state.d.ts.map +1 -0
  104. package/dist/middleware/request-state.js +42 -0
  105. package/dist/middleware/request-state.js.map +1 -0
  106. package/dist/middleware/scope.d.ts +24 -0
  107. package/dist/middleware/scope.d.ts.map +1 -0
  108. package/dist/middleware/scope.js +50 -0
  109. package/dist/middleware/scope.js.map +1 -0
  110. package/dist/registry/index.d.ts +135 -0
  111. package/dist/registry/index.d.ts.map +1 -0
  112. package/dist/registry/index.js +96 -0
  113. package/dist/registry/index.js.map +1 -0
  114. package/dist/replay/index.d.ts +2 -0
  115. package/dist/replay/index.js +3 -0
  116. package/dist/replay/routes.d.ts +46 -0
  117. package/dist/replay/routes.d.ts.map +1 -0
  118. package/dist/replay/routes.js +189 -0
  119. package/dist/replay/routes.js.map +1 -0
  120. package/dist/routes/agents.d.ts +41 -0
  121. package/dist/routes/agents.d.ts.map +1 -0
  122. package/dist/routes/agents.js +213 -0
  123. package/dist/routes/agents.js.map +1 -0
  124. package/dist/routes/audit.d.ts +57 -0
  125. package/dist/routes/audit.d.ts.map +1 -0
  126. package/dist/routes/audit.js +116 -0
  127. package/dist/routes/audit.js.map +1 -0
  128. package/dist/routes/auth.d.ts +26 -0
  129. package/dist/routes/auth.d.ts.map +1 -0
  130. package/dist/routes/auth.js +54 -0
  131. package/dist/routes/auth.js.map +1 -0
  132. package/dist/routes/health.d.ts +22 -0
  133. package/dist/routes/health.d.ts.map +1 -0
  134. package/dist/routes/health.js +33 -0
  135. package/dist/routes/health.js.map +1 -0
  136. package/dist/routes/index.d.ts +10 -0
  137. package/dist/routes/index.js +12 -0
  138. package/dist/routes/mcp.d.ts +32 -0
  139. package/dist/routes/mcp.d.ts.map +1 -0
  140. package/dist/routes/mcp.js +61 -0
  141. package/dist/routes/mcp.js.map +1 -0
  142. package/dist/routes/memory.d.ts +141 -0
  143. package/dist/routes/memory.d.ts.map +1 -0
  144. package/dist/routes/memory.js +102 -0
  145. package/dist/routes/memory.js.map +1 -0
  146. package/dist/routes/sessions.d.ts +54 -0
  147. package/dist/routes/sessions.d.ts.map +1 -0
  148. package/dist/routes/sessions.js +135 -0
  149. package/dist/routes/sessions.js.map +1 -0
  150. package/dist/routes/skills.d.ts +34 -0
  151. package/dist/routes/skills.d.ts.map +1 -0
  152. package/dist/routes/skills.js +54 -0
  153. package/dist/routes/skills.js.map +1 -0
  154. package/dist/routes/tokens.d.ts +25 -0
  155. package/dist/routes/tokens.d.ts.map +1 -0
  156. package/dist/routes/tokens.js +87 -0
  157. package/dist/routes/tokens.js.map +1 -0
  158. package/dist/routes/workflows.d.ts +27 -0
  159. package/dist/routes/workflows.d.ts.map +1 -0
  160. package/dist/routes/workflows.js +220 -0
  161. package/dist/routes/workflows.js.map +1 -0
  162. package/dist/runtime/run-state.d.ts +158 -0
  163. package/dist/runtime/run-state.d.ts.map +1 -0
  164. package/dist/runtime/run-state.js +182 -0
  165. package/dist/runtime/run-state.js.map +1 -0
  166. package/dist/sse/events.d.ts +44 -0
  167. package/dist/sse/events.d.ts.map +1 -0
  168. package/dist/sse/events.js +200 -0
  169. package/dist/sse/events.js.map +1 -0
  170. package/dist/sse/index.d.ts +2 -0
  171. package/dist/sse/index.js +3 -0
  172. package/dist/triggers/daemon.d.ts +74 -0
  173. package/dist/triggers/daemon.d.ts.map +1 -0
  174. package/dist/triggers/daemon.js +114 -0
  175. package/dist/triggers/daemon.js.map +1 -0
  176. package/dist/triggers/index.d.ts +3 -0
  177. package/dist/triggers/index.js +4 -0
  178. package/dist/triggers/routes.d.ts +21 -0
  179. package/dist/triggers/routes.d.ts.map +1 -0
  180. package/dist/triggers/routes.js +117 -0
  181. package/dist/triggers/routes.js.map +1 -0
  182. package/dist/ws/dispatcher.d.ts +169 -0
  183. package/dist/ws/dispatcher.d.ts.map +1 -0
  184. package/dist/ws/dispatcher.js +303 -0
  185. package/dist/ws/dispatcher.js.map +1 -0
  186. package/dist/ws/index.d.ts +6 -0
  187. package/dist/ws/index.js +7 -0
  188. package/dist/ws/replay-buffer.d.ts +47 -0
  189. package/dist/ws/replay-buffer.d.ts.map +1 -0
  190. package/dist/ws/replay-buffer.js +88 -0
  191. package/dist/ws/replay-buffer.js.map +1 -0
  192. package/dist/ws/subjects.d.ts +71 -0
  193. package/dist/ws/subjects.d.ts.map +1 -0
  194. package/dist/ws/subjects.js +112 -0
  195. package/dist/ws/subjects.js.map +1 -0
  196. package/dist/ws/ticket.d.ts +74 -0
  197. package/dist/ws/ticket.d.ts.map +1 -0
  198. package/dist/ws/ticket.js +112 -0
  199. package/dist/ws/ticket.js.map +1 -0
  200. package/dist/ws/upgrade.d.ts +63 -0
  201. package/dist/ws/upgrade.d.ts.map +1 -0
  202. package/dist/ws/upgrade.js +324 -0
  203. package/dist/ws/upgrade.js.map +1 -0
  204. package/package.json +156 -0
package/dist/app.js ADDED
@@ -0,0 +1,684 @@
1
+ import { bridgeCommentaryToAudit, createLateBoundCommentarySink } from "./commentary/audit-bridge.js";
2
+ import "./commentary/index.js";
3
+ import { LifecycleDoubleStartError, LifecycleNotStartedError, ShutdownTimeoutError } from "./errors/index.js";
4
+ import { parseServerConfig } from "./config.js";
5
+ import { createConsolidatorDaemon } from "./consolidator/daemon.js";
6
+ import { createExtendedHealthRoutes, createMetricsRoutes, createSecretsHealthRoutes } from "./health/routes.js";
7
+ import "./health/index.js";
8
+ import { runPreBind } from "./lifecycle/pre-bind.js";
9
+ import "./lifecycle/index.js";
10
+ import { SERVER_METRIC_NAMES, createServerMetricRegistry } from "./metrics/catalog.js";
11
+ import { createAuditMiddleware } from "./middleware/audit.js";
12
+ import { createAnonymousAuthMiddleware, createAuthMiddleware } from "./middleware/auth.js";
13
+ import { createCorsMiddleware } from "./middleware/cors.js";
14
+ import { createCsrfMiddleware } from "./middleware/csrf.js";
15
+ import { createIdempotencyMiddleware } from "./middleware/idempotency.js";
16
+ import { createRateLimitMiddleware } from "./middleware/rate-limit.js";
17
+ import { createRequestStateMiddleware } from "./middleware/request-state.js";
18
+ import "./middleware/index.js";
19
+ import { AgentRegistry, WorkflowRegistry } from "./registry/index.js";
20
+ import { createReplayRoutes } from "./replay/routes.js";
21
+ import "./replay/index.js";
22
+ import { createAgentRoutes, createRunRoutes } from "./routes/agents.js";
23
+ import { createAuditRoutes } from "./routes/audit.js";
24
+ import { createAuthRoutes } from "./routes/auth.js";
25
+ import { createMcpRoutes } from "./routes/mcp.js";
26
+ import { createMemoryRoutes } from "./routes/memory.js";
27
+ import { createSessionRoutes } from "./routes/sessions.js";
28
+ import { createSkillsRoutes } from "./routes/skills.js";
29
+ import { createTokensRoutes } from "./routes/tokens.js";
30
+ import { createWorkflowRoutes } from "./routes/workflows.js";
31
+ import "./routes/index.js";
32
+ import { RunStateTracker, scheduleRunPruning } from "./runtime/run-state.js";
33
+ import { createSseRoutes } from "./sse/events.js";
34
+ import "./sse/index.js";
35
+ import { createTriggersDaemon } from "./triggers/daemon.js";
36
+ import { createTriggersRoutes } from "./triggers/routes.js";
37
+ import { createWsDispatcher } from "./ws/dispatcher.js";
38
+ import { createWsTicketStore } from "./ws/ticket.js";
39
+ import { createWsUpgradeEvents } from "./ws/upgrade.js";
40
+ import "./ws/index.js";
41
+ import { SUBPROTOCOL_NAME, negotiateSubprotocol } from "@graphorin/protocol";
42
+ import { TokenVerifier, applyProcessHardening, generatePepper } from "@graphorin/security";
43
+ import { openAuditDb, registerAuditDbBinding } from "@graphorin/security/audit";
44
+ import { createSqliteStore, loadCipherDriver, readWalSize } from "@graphorin/store-sqlite";
45
+ import { serve } from "@hono/node-server";
46
+ import { createNodeWebSocket } from "@hono/node-ws";
47
+ import { Hono } from "hono";
48
+
49
+ //#region src/app.ts
50
+ /**
51
+ * Pre-built audit-db binding shipped from `@graphorin/store-sqlite`.
52
+ * Registered exactly once per process so {@link openAuditDb} can find
53
+ * a default binding without forcing operators to wire it manually.
54
+ *
55
+ * Exported as {@link ensureStoreAuditBinding} so the CLI (Phase 15
56
+ * `graphorin audit verify | prune | export`) can reach into the same
57
+ * binding without booting the HTTP listener.
58
+ *
59
+ * @stable
60
+ */
61
+ let storeAuditBindingRegistered = false;
62
+ function ensureStoreAuditBinding() {
63
+ if (storeAuditBindingRegistered) return;
64
+ registerAuditDbBinding({
65
+ id: "better-sqlite3-multiple-ciphers",
66
+ description: "Default audit-db binding shipped by @graphorin/store-sqlite.",
67
+ open: async (opts) => {
68
+ const driver = await loadCipherDriver();
69
+ const passphrase = await opts.passphrase.use((value) => value);
70
+ const db = new driver(opts.path);
71
+ db.pragma(`key = '${passphrase.replace(/'/g, "''")}'`);
72
+ db.pragma("journal_mode = WAL");
73
+ db.pragma("synchronous = NORMAL");
74
+ db.pragma("busy_timeout = 5000");
75
+ db.pragma("foreign_keys = ON");
76
+ db.exec(`CREATE TABLE IF NOT EXISTS audit_log (
77
+ seq INTEGER PRIMARY KEY,
78
+ ts INTEGER NOT NULL,
79
+ actor_json TEXT NOT NULL,
80
+ action TEXT NOT NULL,
81
+ target TEXT NOT NULL,
82
+ decision TEXT NOT NULL,
83
+ context_json TEXT,
84
+ metadata_json TEXT,
85
+ prev_hash TEXT NOT NULL,
86
+ hash TEXT NOT NULL UNIQUE
87
+ ) WITHOUT ROWID;`);
88
+ return {
89
+ binding: "better-sqlite3-multiple-ciphers",
90
+ path: opts.path,
91
+ async insert(entry) {
92
+ db.prepare(`INSERT INTO audit_log (
93
+ seq, ts, actor_json, action, target, decision,
94
+ context_json, metadata_json, prev_hash, hash
95
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(entry.seq, entry.ts, JSON.stringify(entry.actor), entry.action, entry.target, entry.decision, entry.context !== void 0 ? JSON.stringify(entry.context) : null, entry.metadata !== void 0 ? JSON.stringify(entry.metadata) : null, entry.prevHash, entry.hash);
96
+ return entry;
97
+ },
98
+ async latest() {
99
+ const row = db.prepare("SELECT * FROM audit_log ORDER BY seq DESC LIMIT 1").get();
100
+ if (row === void 0) return void 0;
101
+ return rowToEntry(row);
102
+ },
103
+ async *iterate(bounds) {
104
+ const lo = bounds?.fromSeq ?? 1;
105
+ const hi = bounds?.toSeq ?? Number.MAX_SAFE_INTEGER;
106
+ const iter = db.prepare("SELECT * FROM audit_log WHERE seq BETWEEN ? AND ? ORDER BY seq ASC").iterate(lo, hi);
107
+ for (const row of iter) yield rowToEntry(row);
108
+ },
109
+ async count() {
110
+ return db.prepare("SELECT COUNT(*) AS n FROM audit_log").get().n;
111
+ },
112
+ async deleteUpTo(threshold) {
113
+ const before = db.prepare("SELECT COUNT(*) AS n FROM audit_log WHERE seq <= ?").get(threshold);
114
+ db.prepare("DELETE FROM audit_log WHERE seq <= ?").run(threshold);
115
+ return before.n;
116
+ },
117
+ async replaceEntry(entry) {
118
+ db.prepare(`UPDATE audit_log SET prev_hash = ?, hash = ? WHERE seq = ?`).run(entry.prevHash, entry.hash, entry.seq);
119
+ },
120
+ async close() {
121
+ if (db.open) db.close();
122
+ }
123
+ };
124
+ }
125
+ }, { setAsDefault: true });
126
+ storeAuditBindingRegistered = true;
127
+ }
128
+ function rowToEntry(row) {
129
+ return {
130
+ seq: row.seq,
131
+ ts: row.ts,
132
+ actor: JSON.parse(row.actor_json),
133
+ action: row.action,
134
+ target: row.target,
135
+ decision: row.decision,
136
+ ...row.context_json !== null ? { context: JSON.parse(row.context_json) } : {},
137
+ ...row.metadata_json !== null ? { metadata: JSON.parse(row.metadata_json) } : {},
138
+ prevHash: row.prev_hash,
139
+ hash: row.hash
140
+ };
141
+ }
142
+ /**
143
+ * @stable
144
+ */
145
+ const VERSION = "0.5.0";
146
+ /**
147
+ * Build a fully-wired Graphorin server. The returned handle is
148
+ * inert until `start()` is awaited.
149
+ *
150
+ * @stable
151
+ */
152
+ async function createServer(options = {}) {
153
+ const config = options.validatedConfig ?? parseServerConfig(options.config ?? {});
154
+ const now = options.now ?? Date.now;
155
+ const startedAt = now();
156
+ const version = options.version ?? VERSION;
157
+ let storeEncryption;
158
+ if (options.store === void 0 && config.storage.encryption.enabled) {
159
+ if (config.storage.encryption.passphraseRef === void 0) throw new Error("[graphorin/server] storage.encryption.enabled is true but no passphraseRef is configured.");
160
+ const { resolveSecret } = await import("@graphorin/security/secrets");
161
+ const passphrase = await resolveSecret(config.storage.encryption.passphraseRef);
162
+ storeEncryption = {
163
+ enabled: true,
164
+ passphraseResolver: async () => passphrase.use((v) => v)
165
+ };
166
+ }
167
+ const store = options.store ?? await createSqliteStore({
168
+ path: config.storage.path,
169
+ mode: config.storage.mode,
170
+ ...storeEncryption !== void 0 ? { encryption: {
171
+ enabled: true,
172
+ ...config.storage.encryption.cipher !== void 0 ? { cipher: config.storage.encryption.cipher } : {},
173
+ passphraseResolver: storeEncryption.passphraseResolver
174
+ } } : {}
175
+ });
176
+ const metricRegistry = options.metricRegistry ?? createServerMetricRegistry();
177
+ metricRegistry.set(SERVER_METRIC_NAMES.buildInfo, 1, { version });
178
+ const runs = options.runs ?? new RunStateTracker({
179
+ now,
180
+ onTerminal: (info) => {
181
+ metricRegistry.inc(SERVER_METRIC_NAMES.agentRunsTotal, { status: info.status });
182
+ if (info.durationMs !== void 0) metricRegistry.observe(SERVER_METRIC_NAMES.agentRunDuration, info.durationMs / 1e3);
183
+ }
184
+ });
185
+ const agents = options.agents ?? new AgentRegistry();
186
+ const workflows = options.workflows ?? new WorkflowRegistry();
187
+ const app = new Hono();
188
+ app.use("*", createRequestStateMiddleware({
189
+ now,
190
+ ...config.server.trustProxy ? { trustProxy: true } : {}
191
+ }));
192
+ app.use("*", createCorsMiddleware(config.server.cors));
193
+ app.use("*", createCsrfMiddleware(config.server.csrf));
194
+ app.use("*", createRateLimitMiddleware(config.server.rateLimit, { now }));
195
+ let serverInstance;
196
+ let listening;
197
+ let started = false;
198
+ let stopped = false;
199
+ let stopRunPruning;
200
+ let auditDb;
201
+ let preBind;
202
+ let verifier;
203
+ let pepperHandle;
204
+ let triggersDaemon;
205
+ if (options.triggers !== void 0) if ("daemon" in options.triggers) triggersDaemon = options.triggers.daemon;
206
+ else triggersDaemon = createTriggersDaemon({ scheduler: options.triggers.scheduler });
207
+ let consolidatorDaemon;
208
+ if (options.consolidator !== void 0) consolidatorDaemon = createConsolidatorDaemon({ consolidator: options.consolidator });
209
+ let dispatcher;
210
+ let tickets;
211
+ let wsAdapter;
212
+ let commentaryAuditSink;
213
+ if (config.server.ws.enabled) {
214
+ commentaryAuditSink = createLateBoundCommentarySink();
215
+ dispatcher = createWsDispatcher({
216
+ commentary: {
217
+ policy: config.server.ws.commentarySanitization.policy,
218
+ applyToEvents: config.server.ws.commentarySanitization.applyToEvents,
219
+ sink: commentaryAuditSink
220
+ },
221
+ replayBuffer: {
222
+ maxEvents: config.server.stream.replayBuffer.maxEvents,
223
+ ttlMs: config.server.stream.replayBuffer.ttlSeconds * 1e3
224
+ },
225
+ perConnectionQueueLimit: config.server.stream.perConnectionQueueLimit,
226
+ now
227
+ });
228
+ tickets = createWsTicketStore({
229
+ ttlMs: config.server.ws.ticketTtlMs,
230
+ now
231
+ });
232
+ wsAdapter = createNodeWebSocket({ app });
233
+ const wssOptions = wsAdapter.wss.options;
234
+ wssOptions.handleProtocols = (protocols) => {
235
+ const negotiated = negotiateSubprotocol(Array.from(protocols));
236
+ if (negotiated !== null) return negotiated;
237
+ for (const candidate of protocols) if (candidate === SUBPROTOCOL_NAME) return SUBPROTOCOL_NAME;
238
+ return false;
239
+ };
240
+ }
241
+ return {
242
+ version,
243
+ config,
244
+ app,
245
+ agents,
246
+ workflows,
247
+ runs,
248
+ metrics: metricRegistry,
249
+ get wsDispatcher() {
250
+ return dispatcher;
251
+ },
252
+ get wsTickets() {
253
+ return tickets;
254
+ },
255
+ get triggers() {
256
+ return triggersDaemon;
257
+ },
258
+ get consolidator() {
259
+ return consolidatorDaemon;
260
+ },
261
+ get listeningOn() {
262
+ return listening;
263
+ },
264
+ async start() {
265
+ if (started) throw new LifecycleDoubleStartError();
266
+ started = true;
267
+ try {
268
+ if (options.hooks?.beforeStart !== void 0) await options.hooks.beforeStart({ config });
269
+ if (options.skipHardening !== true && config.hardening.applyOnStart) applyProcessHardening({
270
+ refuseRoot: config.hardening.refuseRoot,
271
+ umask: config.hardening.umask
272
+ });
273
+ preBind = await runPreBind({
274
+ config,
275
+ store,
276
+ ...options.probeCipherPeer !== void 0 ? { probeCipherPeer: options.probeCipherPeer } : {}
277
+ });
278
+ if (config.audit.enabled && preBind.auditPassphrase !== void 0 && preBind.auditPath !== void 0) {
279
+ ensureStoreAuditBinding();
280
+ auditDb = await openAuditDb({
281
+ path: preBind.auditPath,
282
+ passphrase: preBind.auditPassphrase,
283
+ ...config.audit.cipher !== void 0 ? { cipher: config.audit.cipher } : {}
284
+ });
285
+ commentaryAuditSink?.bind(bridgeCommentaryToAudit(auditDb));
286
+ }
287
+ if (config.auth.kind === "token") {
288
+ const pepper = preBind.pepper ?? await fallbackPepper(options.skipHardening === true);
289
+ pepperHandle = pepper;
290
+ verifier = new TokenVerifier({
291
+ tokenStore: store.authTokens,
292
+ pepper,
293
+ acceptPrefix: config.auth.tokenPrefix,
294
+ acceptEnvironments: config.auth.tokenEnvironments,
295
+ ...config.auth.perIpFailureThreshold !== void 0 ? { perIpFailureThreshold: config.auth.perIpFailureThreshold } : {},
296
+ ...config.auth.perIpLockoutMs !== void 0 ? { perIpLockoutMs: config.auth.perIpLockoutMs } : {},
297
+ now
298
+ });
299
+ }
300
+ mountRoutes(app, config, {
301
+ version,
302
+ startedAt,
303
+ now,
304
+ agents,
305
+ workflows,
306
+ runs,
307
+ store,
308
+ metricRegistry,
309
+ ...options.sessions !== void 0 ? { sessions: options.sessions } : {},
310
+ ...options.memory !== void 0 ? { memory: options.memory } : {},
311
+ ...options.skills !== void 0 ? { skills: options.skills } : {},
312
+ ...options.mcp !== void 0 ? { mcp: options.mcp } : {},
313
+ ...options.audit !== void 0 ? { audit: options.audit } : {},
314
+ ...options.replay !== void 0 ? { replay: options.replay } : {},
315
+ ...options.healthProbes !== void 0 ? { healthProbes: options.healthProbes } : {},
316
+ ...verifier !== void 0 ? { verifier } : {},
317
+ ...auditDb !== void 0 ? { auditDb } : {},
318
+ ...pepperHandle !== void 0 ? { pepper: pepperHandle } : {},
319
+ ...dispatcher !== void 0 ? { wsDispatcher: dispatcher } : {},
320
+ ...tickets !== void 0 ? { wsTickets: tickets } : {},
321
+ ...wsAdapter !== void 0 ? { wsAdapter } : {},
322
+ ...triggersDaemon !== void 0 ? { triggersDaemon } : {},
323
+ ...consolidatorDaemon !== void 0 ? { consolidatorDaemon } : {}
324
+ });
325
+ if (consolidatorDaemon !== void 0) {
326
+ await consolidatorDaemon.start();
327
+ if (triggersDaemon !== void 0 && consolidatorDaemon.consolidator.registerWithScheduler !== void 0) await consolidatorDaemon.consolidator.registerWithScheduler(triggersDaemon.scheduler);
328
+ }
329
+ if (triggersDaemon !== void 0) await triggersDaemon.start();
330
+ try {
331
+ const wal = readWalSize(store.connection);
332
+ metricRegistry.set(SERVER_METRIC_NAMES.storageWalSize, wal);
333
+ } catch {}
334
+ metricRegistry.set(SERVER_METRIC_NAMES.serverUptime, Math.max(0, Math.floor((now() - startedAt) / 1e3)));
335
+ metricRegistry.set(SERVER_METRIC_NAMES.inflightRuns, runs.runningCount());
336
+ metricRegistry.set(SERVER_METRIC_NAMES.replayBufferEvents, 0);
337
+ stopRunPruning = scheduleRunPruning(runs, now);
338
+ if (options.skipListen !== true) {
339
+ serverInstance = serve({
340
+ fetch: app.fetch.bind(app),
341
+ hostname: config.server.host,
342
+ port: config.server.port
343
+ });
344
+ if (wsAdapter !== void 0 && serverInstance !== void 0) wsAdapter.injectWebSocket(serverInstance);
345
+ await new Promise((resolve) => {
346
+ const server = serverInstance;
347
+ if ("once" in server && typeof server.once === "function") server.once("listening", () => resolve());
348
+ else setImmediate(() => resolve());
349
+ });
350
+ const address = serverInstance.address();
351
+ if (address !== null && typeof address === "object") listening = {
352
+ host: address.address,
353
+ port: address.port
354
+ };
355
+ else listening = {
356
+ host: config.server.host,
357
+ port: config.server.port
358
+ };
359
+ } else listening = {
360
+ host: config.server.host,
361
+ port: config.server.port
362
+ };
363
+ if (options.hooks?.onReady !== void 0) await options.hooks.onReady({
364
+ config,
365
+ listeningOn: listening
366
+ });
367
+ return listening;
368
+ } catch (err) {
369
+ started = false;
370
+ await emitError(options.hooks, {
371
+ error: err,
372
+ phase: "beforeStart"
373
+ });
374
+ throw err;
375
+ }
376
+ },
377
+ async stop({ force } = {}) {
378
+ if (!started) throw new LifecycleNotStartedError();
379
+ if (stopped) return;
380
+ stopped = true;
381
+ stopRunPruning?.();
382
+ stopRunPruning = void 0;
383
+ const drainTimeoutMs = force === true ? 0 : config.server.shutdown.drainTimeoutMs;
384
+ try {
385
+ if (options.hooks?.beforeShutdown !== void 0) await options.hooks.beforeShutdown({
386
+ config,
387
+ inflight: runs.inflightCount(),
388
+ drainTimeoutMs
389
+ });
390
+ } catch (err) {
391
+ await emitError(options.hooks, {
392
+ error: err,
393
+ phase: "beforeShutdown"
394
+ });
395
+ }
396
+ if (triggersDaemon !== void 0) try {
397
+ await triggersDaemon.stop();
398
+ } catch (err) {
399
+ await emitError(options.hooks, {
400
+ error: err,
401
+ phase: "beforeShutdown"
402
+ });
403
+ }
404
+ if (consolidatorDaemon !== void 0) try {
405
+ await consolidatorDaemon.stop();
406
+ } catch (err) {
407
+ await emitError(options.hooks, {
408
+ error: err,
409
+ phase: "beforeShutdown"
410
+ });
411
+ }
412
+ runs.abortPending("server-shutdown");
413
+ if (!await drainInFlight(runs, drainTimeoutMs, now) && drainTimeoutMs > 0) runs.abortAll(new ShutdownTimeoutError(drainTimeoutMs, runs.runningCount()));
414
+ if (dispatcher !== void 0) try {
415
+ dispatcher.shutdown();
416
+ } catch {}
417
+ if (serverInstance !== void 0) {
418
+ await new Promise((resolve) => {
419
+ serverInstance.close(() => resolve());
420
+ });
421
+ serverInstance = void 0;
422
+ }
423
+ if (auditDb !== void 0) {
424
+ await auditDb.close();
425
+ auditDb = void 0;
426
+ }
427
+ await store.close();
428
+ listening = void 0;
429
+ verifier = void 0;
430
+ started = false;
431
+ preBind = void 0;
432
+ dispatcher = void 0;
433
+ tickets = void 0;
434
+ wsAdapter = void 0;
435
+ }
436
+ };
437
+ }
438
+ /** IP-23: is `host` a loopback interface (so an open /metrics is not exposed)? */
439
+ function isLoopbackHost(host) {
440
+ const h = host.trim().toLowerCase();
441
+ return h === "127.0.0.1" || h === "::1" || h === "[::1]" || h === "localhost";
442
+ }
443
+ function mountRoutes(app, config, ctx) {
444
+ const base = config.server.basePath;
445
+ const probes = ctx.healthProbes ?? (() => buildDefaultHealthProbes(ctx.store, ctx.triggersDaemon, ctx.consolidatorDaemon, ctx.wsDispatcher, config));
446
+ const health = createExtendedHealthRoutes({
447
+ version: ctx.version,
448
+ startedAt: ctx.startedAt,
449
+ now: ctx.now,
450
+ probes
451
+ });
452
+ app.route(`${base}/health`, health);
453
+ if (config.auth.kind === "none" && !isLoopbackHost(config.server.host)) console.warn(`[graphorin/server] WARN: auth.kind='none' disables authentication on every endpoint, but the server binds the non-loopback host '${config.server.host}'. Anyone who can reach it has full admin access — use auth.kind='token' for non-loopback deployments or bind a loopback host.`);
454
+ if (config.metrics.enabled) {
455
+ if (!config.metrics.requireAuth && !isLoopbackHost(config.server.host)) console.warn(`[graphorin/server] WARN: /metrics is unauthenticated (metrics.requireAuth=false) and the server binds the non-loopback host '${config.server.host}'. The exposition leaks operational detail to anyone who can reach it — set metrics.requireAuth=true or bind a loopback host.`);
456
+ const metricsRoute = createMetricsRoutes({
457
+ registry: ctx.metricRegistry,
458
+ requireAuth: config.metrics.requireAuth,
459
+ refresh: () => refreshLiveMetrics({
460
+ registry: ctx.metricRegistry,
461
+ store: ctx.store,
462
+ runs: ctx.runs,
463
+ startedAt: ctx.startedAt,
464
+ now: ctx.now,
465
+ ...ctx.triggersDaemon !== void 0 ? { triggersDaemon: ctx.triggersDaemon } : {},
466
+ ...ctx.consolidatorDaemon !== void 0 ? { consolidatorDaemon: ctx.consolidatorDaemon } : {},
467
+ ...ctx.wsDispatcher !== void 0 ? { wsDispatcher: ctx.wsDispatcher } : {}
468
+ })
469
+ });
470
+ app.route(`${base}${config.metrics.path}`, metricsRoute);
471
+ }
472
+ const wsUpgradePath = config.server.ws.enabled ? `${base}${config.server.ws.path}` : void 0;
473
+ const metricsPath = config.metrics.enabled && !config.metrics.requireAuth ? `${base}${config.metrics.path}` : void 0;
474
+ function shouldSkipAuth(path) {
475
+ if (path === `${base}/health` || path === `${base}/health/`) return true;
476
+ if (wsUpgradePath !== void 0) {
477
+ if (path === wsUpgradePath || path === `${wsUpgradePath}/`) return true;
478
+ }
479
+ if (metricsPath !== void 0) {
480
+ if (path === metricsPath || path === `${metricsPath}/`) return true;
481
+ }
482
+ return false;
483
+ }
484
+ const anonymousAuth = ctx.verifier === void 0 && config.auth.kind === "none";
485
+ if (ctx.verifier !== void 0 || anonymousAuth) {
486
+ const authMw = ctx.verifier !== void 0 ? createAuthMiddleware({ verifier: ctx.verifier }) : createAnonymousAuthMiddleware();
487
+ app.use(`${base}/*`, async (c, next) => {
488
+ if (shouldSkipAuth(c.req.path)) {
489
+ await next();
490
+ return;
491
+ }
492
+ return authMw(c, next);
493
+ });
494
+ if (ctx.auditDb !== void 0) {
495
+ const auditMw = createAuditMiddleware({
496
+ auditDb: ctx.auditDb,
497
+ now: ctx.now
498
+ });
499
+ app.use(`${base}/*`, async (c, next) => {
500
+ if (shouldSkipAuth(c.req.path)) {
501
+ await next();
502
+ return;
503
+ }
504
+ return auditMw(c, next);
505
+ });
506
+ }
507
+ }
508
+ if (config.server.idempotency.enabled) {
509
+ const idempotencyMw = createIdempotencyMiddleware({
510
+ store: ctx.store.idempotency,
511
+ config: config.server.idempotency,
512
+ now: ctx.now,
513
+ excludeResponseCachePaths: [`${base}/tokens`],
514
+ metricRegistry: ctx.metricRegistry
515
+ });
516
+ app.use(`${base}/*`, async (c, next) => {
517
+ if (shouldSkipAuth(c.req.path)) {
518
+ await next();
519
+ return;
520
+ }
521
+ return idempotencyMw(c, next);
522
+ });
523
+ }
524
+ app.route(`${base}/health/secrets`, createSecretsHealthRoutes());
525
+ app.route(`${base}/agents`, createAgentRoutes({
526
+ agents: ctx.agents,
527
+ runs: ctx.runs,
528
+ ...ctx.wsDispatcher !== void 0 ? { dispatcher: ctx.wsDispatcher } : {}
529
+ }));
530
+ app.route(`${base}/runs`, createRunRoutes({
531
+ agents: ctx.agents,
532
+ runs: ctx.runs
533
+ }));
534
+ app.route(`${base}/workflows`, createWorkflowRoutes({
535
+ workflows: ctx.workflows,
536
+ runs: ctx.runs,
537
+ ...ctx.wsDispatcher !== void 0 ? { dispatcher: ctx.wsDispatcher } : {}
538
+ }));
539
+ if (ctx.sessions !== void 0) app.route(`${base}/sessions`, createSessionRoutes({ sessions: ctx.sessions }));
540
+ if (ctx.memory !== void 0) app.route(`${base}/memory`, createMemoryRoutes({ memory: ctx.memory }));
541
+ if (ctx.skills !== void 0) app.route(`${base}/skills`, createSkillsRoutes({ skills: ctx.skills }));
542
+ if (ctx.mcp !== void 0) app.route(`${base}/mcp`, createMcpRoutes({ mcp: ctx.mcp }));
543
+ if (ctx.audit !== void 0) app.route(`${base}/audit`, createAuditRoutes({ audit: ctx.audit }));
544
+ if (ctx.triggersDaemon !== void 0) app.route(`${base}/triggers`, createTriggersRoutes({ daemon: ctx.triggersDaemon }));
545
+ if (ctx.replay !== void 0) app.route(`${base}`, createReplayRoutes({
546
+ replay: ctx.replay,
547
+ ...ctx.auditDb !== void 0 ? { auditDb: ctx.auditDb } : {},
548
+ now: ctx.now
549
+ }));
550
+ if (config.auth.kind === "token" && ctx.pepper !== void 0) app.route(`${base}/tokens`, createTokensRoutes({
551
+ tokenStore: ctx.store.authTokens,
552
+ pepper: ctx.pepper,
553
+ defaultEnv: "live",
554
+ allowedEnvs: config.auth.tokenEnvironments
555
+ }));
556
+ if (ctx.wsTickets !== void 0) app.route(`${base}`, createAuthRoutes({ tickets: ctx.wsTickets }));
557
+ if (ctx.wsDispatcher !== void 0 && ctx.wsTickets !== void 0 && ctx.wsAdapter !== void 0 && (ctx.verifier !== void 0 || anonymousAuth) && config.server.ws.enabled) {
558
+ const dispatcher = ctx.wsDispatcher;
559
+ const tickets = ctx.wsTickets;
560
+ const verifier = ctx.verifier;
561
+ const runs = ctx.runs;
562
+ app.get(`${base}${config.server.ws.path}`, ctx.wsAdapter.upgradeWebSocket((c) => createWsUpgradeEvents(c, {
563
+ dispatcher,
564
+ tickets,
565
+ ...verifier !== void 0 ? { verifier } : {},
566
+ anonymous: anonymousAuth,
567
+ runs,
568
+ now: ctx.now
569
+ })));
570
+ }
571
+ if (ctx.wsDispatcher !== void 0 && config.server.sse.enabled) app.route(`${base}${config.server.sse.path}`, createSseRoutes({
572
+ dispatcher: ctx.wsDispatcher,
573
+ keepAliveMs: config.server.sse.keepAliveMs,
574
+ perConnectionQueueLimit: config.server.stream.perConnectionQueueLimit,
575
+ now: ctx.now
576
+ }));
577
+ }
578
+ async function drainInFlight(runs, drainTimeoutMs, now) {
579
+ if (runs.runningCount() === 0) return true;
580
+ if (drainTimeoutMs <= 0) return runs.runningCount() === 0;
581
+ const deadline = now() + drainTimeoutMs;
582
+ while (now() < deadline) {
583
+ if (runs.runningCount() === 0) return true;
584
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - now())));
585
+ }
586
+ return runs.runningCount() === 0;
587
+ }
588
+ async function emitError(hooks, ctx) {
589
+ if (hooks?.onError === void 0) return;
590
+ try {
591
+ await hooks.onError(ctx);
592
+ } catch {}
593
+ }
594
+ async function refreshLiveMetrics(options) {
595
+ const { registry, store, runs, startedAt, now } = options;
596
+ try {
597
+ const wal = readWalSize(store.connection);
598
+ registry.set(SERVER_METRIC_NAMES.storageWalSize, wal);
599
+ } catch {}
600
+ registry.set(SERVER_METRIC_NAMES.serverUptime, Math.max(0, Math.floor((now() - startedAt) / 1e3)));
601
+ registry.set(SERVER_METRIC_NAMES.inflightRuns, runs.runningCount());
602
+ if (options.wsDispatcher !== void 0) {
603
+ const sizes = options.wsDispatcher.size();
604
+ registry.set(SERVER_METRIC_NAMES.replayBufferEvents, sizes.subscriptions);
605
+ }
606
+ if (options.triggersDaemon !== void 0) {
607
+ const metrics = options.triggersDaemon.metrics();
608
+ for (const [triggerId, counts] of metrics.fires) {
609
+ const sanitized = sanitizeMetricLabelValue(triggerId);
610
+ const successCurrent = readCounter(registry, SERVER_METRIC_NAMES.triggersFiresTotal, {
611
+ trigger_id: sanitized,
612
+ status: "success"
613
+ });
614
+ const errorCurrent = readCounter(registry, SERVER_METRIC_NAMES.triggersFiresTotal, {
615
+ trigger_id: sanitized,
616
+ status: "error"
617
+ });
618
+ const successDelta = counts.success - successCurrent;
619
+ const errorDelta = counts.error - errorCurrent;
620
+ if (successDelta > 0) registry.inc(SERVER_METRIC_NAMES.triggersFiresTotal, {
621
+ trigger_id: sanitized,
622
+ status: "success"
623
+ }, successDelta);
624
+ if (errorDelta > 0) registry.inc(SERVER_METRIC_NAMES.triggersFiresTotal, {
625
+ trigger_id: sanitized,
626
+ status: "error"
627
+ }, errorDelta);
628
+ }
629
+ }
630
+ if (options.consolidatorDaemon !== void 0) try {
631
+ const status = await options.consolidatorDaemon.status();
632
+ registry.set(SERVER_METRIC_NAMES.consolidatorQueueDepth, status.queueDepth, { phase: "aggregate" });
633
+ registry.set(SERVER_METRIC_NAMES.consolidatorDlqSize, status.dlqSize);
634
+ registry.set(SERVER_METRIC_NAMES.consolidatorBudgetRemainingUsd, status.budget.costRemaining);
635
+ registry.set(SERVER_METRIC_NAMES.consolidatorBudgetRemainingTokens, status.budget.tokensRemaining);
636
+ } catch {}
637
+ }
638
+ /**
639
+ * Convert an arbitrary user-supplied identifier (trigger id) into a
640
+ * Prometheus-safe label value. Replaces every character outside the
641
+ * `[A-Za-z0-9_:]` range with `_`. This guarantees the cardinality
642
+ * never explodes on UTF-8 sequences while keeping the value
643
+ * recognizable.
644
+ */
645
+ function sanitizeMetricLabelValue(value) {
646
+ return value.replace(/[^A-Za-z0-9_:.-]/g, "_").slice(0, 200);
647
+ }
648
+ function readCounter(registry, name, labels) {
649
+ const snap = registry.snapshot().counters[name] ?? [];
650
+ for (const entry of snap) if (matchesLabels(entry.labels, labels)) return entry.value;
651
+ return 0;
652
+ }
653
+ function matchesLabels(a, b) {
654
+ for (const k of Object.keys(b)) if (String(a[k]) !== b[k]) return false;
655
+ for (const k of Object.keys(a)) if (!(k in b)) return false;
656
+ return true;
657
+ }
658
+ function buildDefaultHealthProbes(store, triggersDaemon, consolidatorDaemon, dispatcher, config) {
659
+ const out = {
660
+ store,
661
+ walWarnThresholdBytes: config.health.walWarnThresholdBytes,
662
+ encryptionEnabled: config.storage.encryption.enabled,
663
+ ...config.storage.encryption.enabled ? { cipherPeerInstalled: true } : {}
664
+ };
665
+ if (triggersDaemon !== void 0) out.triggers = triggersDaemon;
666
+ if (consolidatorDaemon !== void 0) out.consolidator = consolidatorDaemon;
667
+ if (dispatcher !== void 0) {
668
+ const sizes = dispatcher.size();
669
+ out.replayBuffer = {
670
+ eventsBuffered: sizes.subscriptions,
671
+ subscribers: sizes.subscribers,
672
+ subscriptions: sizes.subscriptions
673
+ };
674
+ }
675
+ return Object.freeze(out);
676
+ }
677
+ async function fallbackPepper(skipHardening) {
678
+ if (!skipHardening) throw new Error("[graphorin/server] missing resolved pepper after pre-bind; auth.pepperRef is required when auth.kind = token.");
679
+ return generatePepper();
680
+ }
681
+
682
+ //#endregion
683
+ export { createServer, ensureStoreAuditBinding };
684
+ //# sourceMappingURL=app.js.map