@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.
- package/CHANGELOG.md +85 -0
- package/README.md +6 -6
- package/dist/app-audit-binding.d.ts +15 -0
- package/dist/app-audit-binding.d.ts.map +1 -0
- package/dist/app-audit-binding.js +136 -0
- package/dist/app-audit-binding.js.map +1 -0
- package/dist/app-daemons.d.ts +29 -0
- package/dist/app-daemons.d.ts.map +1 -0
- package/dist/app-daemons.js +27 -0
- package/dist/app-daemons.js.map +1 -0
- package/dist/app-lifecycle.js +272 -0
- package/dist/app-lifecycle.js.map +1 -0
- package/dist/app-metrics.js +79 -0
- package/dist/app-metrics.js.map +1 -0
- package/dist/app-middleware.js +92 -0
- package/dist/app-middleware.js.map +1 -0
- package/dist/app-routes.js +131 -0
- package/dist/app-routes.js.map +1 -0
- package/dist/app-ws.js +65 -0
- package/dist/app-ws.js.map +1 -0
- package/dist/app.d.ts +16 -16
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +40 -611
- package/dist/app.js.map +1 -1
- package/dist/commentary/built-in-patterns.d.ts +1 -1
- package/dist/commentary/built-in-patterns.js +1 -1
- package/dist/commentary/built-in-patterns.js.map +1 -1
- package/dist/config.d.ts +100 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +30 -2
- package/dist/config.js.map +1 -1
- package/dist/health/checks.d.ts +15 -1
- package/dist/health/checks.d.ts.map +1 -1
- package/dist/health/checks.js +21 -0
- package/dist/health/checks.js.map +1 -1
- package/dist/health/routes.js +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -26
- package/dist/index.js.map +1 -1
- package/dist/internal/context.d.ts +2 -2
- package/dist/internal/wire-error.js +20 -0
- package/dist/internal/wire-error.js.map +1 -0
- package/dist/lifecycle/pre-bind.d.ts +1 -1
- package/dist/metrics/catalog.d.ts.map +1 -1
- package/dist/metrics/catalog.js.map +1 -1
- package/dist/metrics/tools-bridge.d.ts +15 -0
- package/dist/metrics/tools-bridge.d.ts.map +1 -0
- package/dist/metrics/tools-bridge.js +103 -0
- package/dist/metrics/tools-bridge.js.map +1 -0
- package/dist/middleware/audit.d.ts +1 -1
- package/dist/middleware/auth.js +1 -1
- package/dist/middleware/csrf.js +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/scope.d.ts.map +1 -1
- package/dist/middleware/scope.js +44 -4
- package/dist/middleware/scope.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/registry/index.d.ts +33 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js.map +1 -1
- package/dist/replay/routes.d.ts +1 -1
- package/dist/replay/routes.js +2 -2
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +64 -18
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/auth.js +2 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/sessions.js +5 -5
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/tokens.d.ts +1 -1
- package/dist/routes/tokens.d.ts.map +1 -1
- package/dist/routes/tokens.js +21 -1
- package/dist/routes/tokens.js.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/routes/workflows.js +165 -19
- package/dist/routes/workflows.js.map +1 -1
- package/dist/runtime/retention.d.ts +105 -0
- package/dist/runtime/retention.d.ts.map +1 -0
- package/dist/runtime/retention.js +154 -0
- package/dist/runtime/retention.js.map +1 -0
- package/dist/runtime/run-state.d.ts +2 -0
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +34 -2
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/sse/events.js +3 -4
- package/dist/sse/events.js.map +1 -1
- package/dist/tools-audit-bridge.d.ts +29 -0
- package/dist/tools-audit-bridge.d.ts.map +1 -0
- package/dist/tools-audit-bridge.js +103 -0
- package/dist/tools-audit-bridge.js.map +1 -0
- package/dist/workflows/timer-daemon.d.ts +69 -0
- package/dist/workflows/timer-daemon.d.ts.map +1 -0
- package/dist/workflows/timer-daemon.js +37 -0
- package/dist/workflows/timer-daemon.js.map +1 -0
- package/dist/ws/dispatcher.d.ts +1 -1
- package/dist/ws/dispatcher.d.ts.map +1 -1
- package/dist/ws/dispatcher.js +19 -12
- package/dist/ws/dispatcher.js.map +1 -1
- package/dist/ws/index.d.ts +2 -2
- package/dist/ws/index.js +3 -3
- package/dist/ws/replay-buffer.d.ts +35 -1
- package/dist/ws/replay-buffer.d.ts.map +1 -1
- package/dist/ws/replay-buffer.js +35 -3
- package/dist/ws/replay-buffer.js.map +1 -1
- package/dist/ws/upgrade.d.ts.map +1 -1
- package/dist/ws/upgrade.js +26 -19
- package/dist/ws/upgrade.js.map +1 -1
- package/package.json +31 -30
- package/src/app-audit-binding.ts +227 -0
- package/src/app-daemons.ts +73 -0
- package/src/app-lifecycle.ts +476 -0
- package/src/app-metrics.ts +144 -0
- package/src/app-middleware.ts +149 -0
- package/src/app-routes.ts +305 -0
- package/src/app-ws.ts +111 -0
- package/src/app.ts +317 -0
- package/src/commentary/audit-bridge.ts +135 -0
- package/src/commentary/built-in-patterns.ts +79 -0
- package/src/commentary/index.ts +32 -0
- package/src/commentary/sanitizer.ts +238 -0
- package/src/commentary/types.ts +130 -0
- package/src/config.ts +472 -0
- package/src/consolidator/daemon.ts +141 -0
- package/src/consolidator/index.ts +14 -0
- package/src/errors/index.ts +247 -0
- package/src/health/checks.ts +322 -0
- package/src/health/index.ts +34 -0
- package/src/health/routes.ts +154 -0
- package/src/index.ts +243 -0
- package/src/internal/context.ts +77 -0
- package/src/internal/ids.ts +17 -0
- package/src/internal/json.ts +47 -0
- package/src/internal/wire-error.ts +44 -0
- package/src/lifecycle/hooks.ts +66 -0
- package/src/lifecycle/index.ts +16 -0
- package/src/lifecycle/pre-bind.ts +138 -0
- package/src/metrics/catalog.ts +112 -0
- package/src/metrics/index.ts +12 -0
- package/src/metrics/registry.ts +337 -0
- package/src/metrics/tools-bridge.ts +124 -0
- package/src/middleware/audit.ts +114 -0
- package/src/middleware/auth.ts +195 -0
- package/src/middleware/cors.ts +72 -0
- package/src/middleware/csrf.ts +98 -0
- package/src/middleware/idempotency.ts +336 -0
- package/src/middleware/index.ts +38 -0
- package/src/middleware/rate-limit.ts +71 -0
- package/src/middleware/request-state.ts +79 -0
- package/src/middleware/scope.ts +161 -0
- package/src/registry/index.ts +278 -0
- package/src/replay/index.ts +14 -0
- package/src/replay/routes.ts +255 -0
- package/src/routes/agents.ts +363 -0
- package/src/routes/audit.ts +207 -0
- package/src/routes/auth.ts +80 -0
- package/src/routes/health.ts +42 -0
- package/src/routes/index.ts +16 -0
- package/src/routes/mcp.ts +97 -0
- package/src/routes/memory.ts +152 -0
- package/src/routes/sessions.ts +250 -0
- package/src/routes/skills.ts +91 -0
- package/src/routes/tokens.ts +166 -0
- package/src/routes/workflows.ts +616 -0
- package/src/runtime/retention.ts +284 -0
- package/src/runtime/run-state.ts +422 -0
- package/src/sse/events.ts +303 -0
- package/src/sse/index.ts +7 -0
- package/src/tools-audit-bridge.ts +120 -0
- package/src/triggers/daemon.ts +198 -0
- package/src/triggers/index.ts +16 -0
- package/src/triggers/routes.ts +139 -0
- package/src/workflows/timer-daemon.ts +102 -0
- package/src/ws/dispatcher.ts +537 -0
- package/src/ws/index.ts +45 -0
- package/src/ws/replay-buffer.ts +209 -0
- package/src/ws/subjects.ts +162 -0
- package/src/ws/ticket.ts +174 -0
- package/src/ws/upgrade.ts +507 -0
- package/dist/lifecycle/index.js +0 -3
- package/dist/routes/index.js +0 -12
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error surface for `@graphorin/server`. Every server-side
|
|
3
|
+
* configuration / lifecycle / runtime failure that an operator must
|
|
4
|
+
* be able to reason about flows through one of the classes in this
|
|
5
|
+
* module so it carries a stable `kind` discriminator + `hint` field
|
|
6
|
+
* pointing the operator at the next remediation step.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Stable string discriminator for {@link GraphorinServerError}. Each
|
|
13
|
+
* value maps to a single failure scenario; never reuse a value for a
|
|
14
|
+
* different cause.
|
|
15
|
+
*
|
|
16
|
+
* @stable
|
|
17
|
+
*/
|
|
18
|
+
export type GraphorinServerErrorCode =
|
|
19
|
+
| 'config-invalid'
|
|
20
|
+
| 'pre-bind-pepper-missing'
|
|
21
|
+
| 'pre-bind-secret-unresolvable'
|
|
22
|
+
| 'pre-bind-encryption-peer-missing'
|
|
23
|
+
| 'pre-bind-encryption-required'
|
|
24
|
+
| 'migration-failed'
|
|
25
|
+
| 'shutdown-timeout'
|
|
26
|
+
| 'idempotency-conflict'
|
|
27
|
+
| 'idempotency-key-required'
|
|
28
|
+
| 'auth-required'
|
|
29
|
+
| 'auth-invalid'
|
|
30
|
+
| 'auth-revoked'
|
|
31
|
+
| 'auth-expired'
|
|
32
|
+
| 'auth-locked-out'
|
|
33
|
+
| 'auth-overloaded'
|
|
34
|
+
| 'scope-denied'
|
|
35
|
+
| 'csrf-denied'
|
|
36
|
+
| 'cors-denied'
|
|
37
|
+
| 'rate-limit-exceeded'
|
|
38
|
+
| 'route-handler-missing'
|
|
39
|
+
| 'agent-not-found'
|
|
40
|
+
| 'workflow-not-found'
|
|
41
|
+
| 'session-not-found'
|
|
42
|
+
| 'run-not-found'
|
|
43
|
+
| 'run-aborted'
|
|
44
|
+
| 'lifecycle-double-start'
|
|
45
|
+
| 'lifecycle-not-started';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Base error class. Every server-emitted typed error inherits from
|
|
49
|
+
* here so middleware can pattern-match a single union.
|
|
50
|
+
*
|
|
51
|
+
* @stable
|
|
52
|
+
*/
|
|
53
|
+
export class GraphorinServerError extends Error {
|
|
54
|
+
readonly kind: GraphorinServerErrorCode;
|
|
55
|
+
readonly hint?: string;
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
kind: GraphorinServerErrorCode,
|
|
59
|
+
message: string,
|
|
60
|
+
options?: { readonly cause?: unknown; readonly hint?: string },
|
|
61
|
+
) {
|
|
62
|
+
super(
|
|
63
|
+
message,
|
|
64
|
+
options !== undefined && options.cause !== undefined ? { cause: options.cause } : undefined,
|
|
65
|
+
);
|
|
66
|
+
this.name = this.constructor.name;
|
|
67
|
+
this.kind = kind;
|
|
68
|
+
if (options?.hint !== undefined) this.hint = options.hint;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @stable */
|
|
73
|
+
export class ConfigInvalidError extends GraphorinServerError {
|
|
74
|
+
readonly issues: ReadonlyArray<{
|
|
75
|
+
readonly path: ReadonlyArray<string | number>;
|
|
76
|
+
readonly message: string;
|
|
77
|
+
}>;
|
|
78
|
+
|
|
79
|
+
constructor(
|
|
80
|
+
issues: ReadonlyArray<{
|
|
81
|
+
readonly path: ReadonlyArray<string | number>;
|
|
82
|
+
readonly message: string;
|
|
83
|
+
}>,
|
|
84
|
+
cause?: unknown,
|
|
85
|
+
) {
|
|
86
|
+
super('config-invalid', `graphorin.config invalid: ${issues.length} issue(s)`, {
|
|
87
|
+
hint: 'Inspect the issues array; each entry pinpoints the offending path.',
|
|
88
|
+
...(cause !== undefined ? { cause } : {}),
|
|
89
|
+
});
|
|
90
|
+
this.issues = Object.freeze(issues.slice());
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @stable */
|
|
95
|
+
export class PrebindPepperMissingError extends GraphorinServerError {
|
|
96
|
+
constructor() {
|
|
97
|
+
super(
|
|
98
|
+
'pre-bind-pepper-missing',
|
|
99
|
+
'auth.pepper SecretRef did not resolve to a non-empty pepper.',
|
|
100
|
+
{
|
|
101
|
+
hint: 'Run `graphorin doctor --check-secrets` (Phase 15) and verify the configured SecretRef.',
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @stable */
|
|
108
|
+
export class PrebindSecretUnresolvableError extends GraphorinServerError {
|
|
109
|
+
readonly path: ReadonlyArray<string | number>;
|
|
110
|
+
readonly raw: string;
|
|
111
|
+
|
|
112
|
+
constructor(path: ReadonlyArray<string | number>, raw: string, cause?: unknown) {
|
|
113
|
+
super(
|
|
114
|
+
'pre-bind-secret-unresolvable',
|
|
115
|
+
`Could not resolve SecretRef '${raw}' at config path ${path.join('.')}`,
|
|
116
|
+
{
|
|
117
|
+
hint: 'Run `graphorin doctor --check-secrets` (Phase 15) and verify the configured SecretRef.',
|
|
118
|
+
...(cause !== undefined ? { cause } : {}),
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
this.path = Object.freeze(path.slice());
|
|
122
|
+
this.raw = raw;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @stable */
|
|
127
|
+
export class PrebindEncryptionPeerMissingError extends GraphorinServerError {
|
|
128
|
+
constructor(cause?: unknown) {
|
|
129
|
+
super(
|
|
130
|
+
'pre-bind-encryption-peer-missing',
|
|
131
|
+
'storage.encryption.enabled = true but the cipher peer is not installed.',
|
|
132
|
+
{
|
|
133
|
+
hint: "Install the 'better-sqlite3-multiple-ciphers' peer (or set storage.encryption.enabled = false).",
|
|
134
|
+
...(cause !== undefined ? { cause } : {}),
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @stable */
|
|
141
|
+
export class PrebindEncryptionRequiredError extends GraphorinServerError {
|
|
142
|
+
constructor(reason: string) {
|
|
143
|
+
super('pre-bind-encryption-required', reason, {
|
|
144
|
+
hint: 'Audit logs are mandatory-encrypted (DEC-124); enable storage.encryption or supply audit.encryption explicitly.',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @stable */
|
|
150
|
+
export class MigrationFailedError extends GraphorinServerError {
|
|
151
|
+
constructor(message: string, cause?: unknown) {
|
|
152
|
+
super('migration-failed', message, {
|
|
153
|
+
hint: 'Inspect the underlying SQLite error and re-run `graphorin migrate`.',
|
|
154
|
+
...(cause !== undefined ? { cause } : {}),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @stable */
|
|
160
|
+
export class ShutdownTimeoutError extends GraphorinServerError {
|
|
161
|
+
readonly drainTimeoutMs: number;
|
|
162
|
+
readonly inflight: number;
|
|
163
|
+
|
|
164
|
+
constructor(drainTimeoutMs: number, inflight: number) {
|
|
165
|
+
super(
|
|
166
|
+
'shutdown-timeout',
|
|
167
|
+
`Drain timed out after ${drainTimeoutMs}ms with ${inflight} request(s) still in flight.`,
|
|
168
|
+
{
|
|
169
|
+
hint: 'Increase server.shutdown.drainTimeoutMs or fix slow handlers.',
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
this.drainTimeoutMs = drainTimeoutMs;
|
|
173
|
+
this.inflight = inflight;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** @stable */
|
|
178
|
+
export class IdempotencyConflictError extends GraphorinServerError {
|
|
179
|
+
constructor(key: string) {
|
|
180
|
+
super(
|
|
181
|
+
'idempotency-conflict',
|
|
182
|
+
`Idempotency-Key '${key}' was previously used with a different request body.`,
|
|
183
|
+
{
|
|
184
|
+
hint: 'Generate a fresh Idempotency-Key for the new payload.',
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @stable */
|
|
191
|
+
export class IdempotencyKeyRequiredError extends GraphorinServerError {
|
|
192
|
+
constructor() {
|
|
193
|
+
super(
|
|
194
|
+
'idempotency-key-required',
|
|
195
|
+
'Side-effecting endpoint requires an Idempotency-Key header.',
|
|
196
|
+
{
|
|
197
|
+
hint: "Set 'Idempotency-Key: <uuid>' on the request, or relax server.idempotency.requireKey to 'warn'.",
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @stable */
|
|
204
|
+
export class LifecycleDoubleStartError extends GraphorinServerError {
|
|
205
|
+
constructor() {
|
|
206
|
+
super('lifecycle-double-start', 'Server.start() invoked while the server is already started.', {
|
|
207
|
+
hint: 'Call stop() before re-starting the server.',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @stable */
|
|
213
|
+
export class LifecycleNotStartedError extends GraphorinServerError {
|
|
214
|
+
constructor() {
|
|
215
|
+
super('lifecycle-not-started', 'Server.stop() invoked before start() resolved.', {
|
|
216
|
+
hint: 'Await start() before calling stop().',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** @stable */
|
|
222
|
+
export class RouteHandlerMissingError extends GraphorinServerError {
|
|
223
|
+
readonly id: string;
|
|
224
|
+
readonly kind_: 'agent' | 'workflow';
|
|
225
|
+
|
|
226
|
+
constructor(kind: 'agent' | 'workflow', id: string) {
|
|
227
|
+
super('route-handler-missing', `No ${kind} registered with id '${id}'.`, {
|
|
228
|
+
hint: `Register the ${kind} via createServer({ ${kind}s: { ... } }) or the AgentRegistry / WorkflowRegistry surface.`,
|
|
229
|
+
});
|
|
230
|
+
this.id = id;
|
|
231
|
+
this.kind_ = kind;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @stable */
|
|
236
|
+
export class AgentNotFoundError extends RouteHandlerMissingError {
|
|
237
|
+
constructor(id: string) {
|
|
238
|
+
super('agent', id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @stable */
|
|
243
|
+
export class WorkflowNotFoundError extends RouteHandlerMissingError {
|
|
244
|
+
constructor(id: string) {
|
|
245
|
+
super('workflow', id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-subsystem health checks aggregated by `/v1/health`. Every check
|
|
3
|
+
* resolves to `'ok' | 'warn' | 'fail'`; the aggregator promotes the
|
|
4
|
+
* worst status to the top-level rollup.
|
|
5
|
+
*
|
|
6
|
+
* The serialized shape is **flat**: each check carries its detail
|
|
7
|
+
* fields directly on the same object as the `status` discriminator
|
|
8
|
+
* - no nested `detail` wrapper - matching the documented response
|
|
9
|
+
* contract exactly.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { GraphorinSqliteStore } from '@graphorin/store-sqlite';
|
|
15
|
+
import { readWalSize } from '@graphorin/store-sqlite';
|
|
16
|
+
import type { ConsolidatorDaemon } from '../consolidator/daemon.js';
|
|
17
|
+
import type { TriggersDaemon } from '../triggers/daemon.js';
|
|
18
|
+
import type { WorkflowTimerDaemon } from '../workflows/timer-daemon.js';
|
|
19
|
+
|
|
20
|
+
/** @stable */
|
|
21
|
+
export interface ReplayBufferProbe {
|
|
22
|
+
/** Total events buffered across all subjects. Aggregated by the caller. */
|
|
23
|
+
readonly eventsBuffered: number;
|
|
24
|
+
readonly subscribers?: number;
|
|
25
|
+
readonly subscriptions?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @stable */
|
|
29
|
+
export type HealthStatus = 'ok' | 'warn' | 'fail';
|
|
30
|
+
|
|
31
|
+
/** @stable */
|
|
32
|
+
export type HealthRollup = 'ok' | 'degraded' | 'failing';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Common discriminator carried on every per-subsystem check entry.
|
|
36
|
+
* Concrete shapes extend this with subsystem-specific fields per the
|
|
37
|
+
* documented contract.
|
|
38
|
+
*
|
|
39
|
+
* @stable
|
|
40
|
+
*/
|
|
41
|
+
export interface BaseHealthCheck {
|
|
42
|
+
readonly status: HealthStatus;
|
|
43
|
+
readonly message?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @stable */
|
|
47
|
+
export interface StorageCheck extends BaseHealthCheck {
|
|
48
|
+
readonly walSizeBytes: number;
|
|
49
|
+
readonly warnThresholdBytes: number;
|
|
50
|
+
readonly lastCheckpointAt?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @stable */
|
|
54
|
+
export interface EmbedderCheck extends BaseHealthCheck {
|
|
55
|
+
readonly modelLoaded: boolean;
|
|
56
|
+
readonly model?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @stable */
|
|
60
|
+
export interface SecretsCheck extends BaseHealthCheck {
|
|
61
|
+
readonly activeStore: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @stable */
|
|
65
|
+
export interface EncryptionCheck extends BaseHealthCheck {
|
|
66
|
+
readonly enabled: boolean;
|
|
67
|
+
readonly peerDepInstalled: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @stable */
|
|
71
|
+
export interface ConsolidatorCheck extends BaseHealthCheck {
|
|
72
|
+
readonly tier: string;
|
|
73
|
+
readonly running: boolean;
|
|
74
|
+
readonly paused: boolean;
|
|
75
|
+
readonly queueDepth: number;
|
|
76
|
+
readonly dlqSize: number;
|
|
77
|
+
readonly budgetRemaining: {
|
|
78
|
+
readonly tokens: number;
|
|
79
|
+
readonly costUsd: number;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @stable */
|
|
84
|
+
export interface TriggersCheck extends BaseHealthCheck {
|
|
85
|
+
readonly running: boolean;
|
|
86
|
+
readonly active: number;
|
|
87
|
+
readonly disabled: number;
|
|
88
|
+
readonly deferred: number;
|
|
89
|
+
readonly lastFireAt?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @stable */
|
|
93
|
+
export interface ReplayBufferCheck extends BaseHealthCheck {
|
|
94
|
+
readonly eventsBuffered: number;
|
|
95
|
+
readonly subscribers?: number;
|
|
96
|
+
readonly subscriptions?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @stable */
|
|
100
|
+
export type HealthCheck =
|
|
101
|
+
| StorageCheck
|
|
102
|
+
| EmbedderCheck
|
|
103
|
+
| SecretsCheck
|
|
104
|
+
| EncryptionCheck
|
|
105
|
+
| ConsolidatorCheck
|
|
106
|
+
| TriggersCheck
|
|
107
|
+
| WorkflowTimersCheck
|
|
108
|
+
| ReplayBufferCheck;
|
|
109
|
+
|
|
110
|
+
/** W-032: durable-timer driver health. @stable */
|
|
111
|
+
export interface WorkflowTimersCheck {
|
|
112
|
+
readonly status: HealthStatus;
|
|
113
|
+
readonly running: boolean;
|
|
114
|
+
readonly sweeps: number;
|
|
115
|
+
readonly fired: number;
|
|
116
|
+
readonly errors: number;
|
|
117
|
+
readonly lastSweepAt?: string;
|
|
118
|
+
readonly nextWakeAt?: string;
|
|
119
|
+
readonly message?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @stable */
|
|
123
|
+
export interface HealthChecks {
|
|
124
|
+
readonly storage?: StorageCheck;
|
|
125
|
+
readonly embedder?: EmbedderCheck;
|
|
126
|
+
readonly secrets?: SecretsCheck;
|
|
127
|
+
readonly encryption?: EncryptionCheck;
|
|
128
|
+
readonly consolidator?: ConsolidatorCheck;
|
|
129
|
+
readonly triggers?: TriggersCheck;
|
|
130
|
+
readonly workflowTimers?: WorkflowTimersCheck;
|
|
131
|
+
readonly replayBuffer?: ReplayBufferCheck;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @stable */
|
|
135
|
+
export interface HealthSummary {
|
|
136
|
+
readonly status: HealthRollup;
|
|
137
|
+
readonly checks: HealthChecks;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @stable */
|
|
141
|
+
export interface HealthCheckOptions {
|
|
142
|
+
readonly store?: GraphorinSqliteStore;
|
|
143
|
+
readonly triggers?: TriggersDaemon;
|
|
144
|
+
readonly workflowTimers?: WorkflowTimerDaemon;
|
|
145
|
+
readonly consolidator?: ConsolidatorDaemon;
|
|
146
|
+
readonly replayBuffer?: ReplayBufferProbe;
|
|
147
|
+
readonly secretsActive?: string;
|
|
148
|
+
readonly encryptionEnabled?: boolean;
|
|
149
|
+
readonly cipherPeerInstalled?: boolean;
|
|
150
|
+
readonly embedderModel?: string;
|
|
151
|
+
readonly embedderLoaded?: boolean;
|
|
152
|
+
/** Highest acceptable WAL size in bytes before warning. Default 50 MB. */
|
|
153
|
+
readonly walWarnThresholdBytes?: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const DEFAULT_WAL_WARN_THRESHOLD_BYTES = 50 * 1024 * 1024;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build the aggregate health summary from runtime probes.
|
|
160
|
+
*
|
|
161
|
+
* @stable
|
|
162
|
+
*/
|
|
163
|
+
export async function collectHealth(options: HealthCheckOptions): Promise<HealthSummary> {
|
|
164
|
+
const checks: {
|
|
165
|
+
-readonly [K in keyof HealthChecks]?: HealthChecks[K];
|
|
166
|
+
} = {};
|
|
167
|
+
|
|
168
|
+
if (options.store !== undefined) {
|
|
169
|
+
const walSize = readWalSizeSafely(options.store);
|
|
170
|
+
const threshold = options.walWarnThresholdBytes ?? DEFAULT_WAL_WARN_THRESHOLD_BYTES;
|
|
171
|
+
const status: HealthStatus = walSize > threshold ? 'warn' : 'ok';
|
|
172
|
+
checks.storage = {
|
|
173
|
+
status,
|
|
174
|
+
walSizeBytes: walSize,
|
|
175
|
+
warnThresholdBytes: threshold,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (options.embedderLoaded !== undefined) {
|
|
180
|
+
checks.embedder = {
|
|
181
|
+
status: options.embedderLoaded ? 'ok' : 'warn',
|
|
182
|
+
modelLoaded: options.embedderLoaded,
|
|
183
|
+
...(options.embedderModel !== undefined ? { model: options.embedderModel } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (options.secretsActive !== undefined) {
|
|
188
|
+
checks.secrets = {
|
|
189
|
+
status: 'ok',
|
|
190
|
+
activeStore: options.secretsActive,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (options.encryptionEnabled !== undefined) {
|
|
195
|
+
// IP-1: a server that is serving this health endpoint with
|
|
196
|
+
// encryption enabled has already opened the store WITH its key at
|
|
197
|
+
// boot (createServer threads the resolved passphrase into
|
|
198
|
+
// createSqliteStore and fails fast otherwise) - that successful
|
|
199
|
+
// keyed open is the factual basis of this check. 'fail' is
|
|
200
|
+
// reserved for an explicit negative cipher-peer probe; the old
|
|
201
|
+
// default blamed a missing peer dep on every encrypted boot.
|
|
202
|
+
const peerDepInstalled = options.cipherPeerInstalled ?? true;
|
|
203
|
+
checks.encryption = {
|
|
204
|
+
status: options.encryptionEnabled && peerDepInstalled === false ? 'fail' : 'ok',
|
|
205
|
+
enabled: options.encryptionEnabled,
|
|
206
|
+
peerDepInstalled,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (options.consolidator !== undefined) {
|
|
211
|
+
try {
|
|
212
|
+
const status = await options.consolidator.status();
|
|
213
|
+
const overall: HealthStatus = status.dlqSize > 0 ? 'warn' : 'ok';
|
|
214
|
+
checks.consolidator = {
|
|
215
|
+
status: status.paused ? 'warn' : overall,
|
|
216
|
+
tier: status.tier,
|
|
217
|
+
running: status.running,
|
|
218
|
+
paused: status.paused,
|
|
219
|
+
queueDepth: status.queueDepth,
|
|
220
|
+
dlqSize: status.dlqSize,
|
|
221
|
+
budgetRemaining: {
|
|
222
|
+
tokens: status.budget.tokensRemaining,
|
|
223
|
+
costUsd: status.budget.costRemaining,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
} catch (err) {
|
|
227
|
+
checks.consolidator = {
|
|
228
|
+
status: 'fail',
|
|
229
|
+
tier: 'unknown',
|
|
230
|
+
running: false,
|
|
231
|
+
paused: false,
|
|
232
|
+
queueDepth: 0,
|
|
233
|
+
dlqSize: 0,
|
|
234
|
+
budgetRemaining: { tokens: 0, costUsd: 0 },
|
|
235
|
+
message: err instanceof Error ? err.message : String(err),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (options.triggers !== undefined) {
|
|
241
|
+
try {
|
|
242
|
+
const status = await options.triggers.status();
|
|
243
|
+
checks.triggers = {
|
|
244
|
+
status: 'ok',
|
|
245
|
+
running: status.running,
|
|
246
|
+
active: status.active,
|
|
247
|
+
disabled: status.disabled,
|
|
248
|
+
deferred: status.deferred,
|
|
249
|
+
...(status.lastFireAt !== undefined ? { lastFireAt: status.lastFireAt } : {}),
|
|
250
|
+
};
|
|
251
|
+
} catch (err) {
|
|
252
|
+
checks.triggers = {
|
|
253
|
+
status: 'fail',
|
|
254
|
+
running: false,
|
|
255
|
+
active: 0,
|
|
256
|
+
disabled: 0,
|
|
257
|
+
deferred: 0,
|
|
258
|
+
message: err instanceof Error ? err.message : String(err),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (options.workflowTimers !== undefined) {
|
|
264
|
+
try {
|
|
265
|
+
const status = await options.workflowTimers.status();
|
|
266
|
+
checks.workflowTimers = {
|
|
267
|
+
status: status.errors > 0 ? 'warn' : 'ok',
|
|
268
|
+
running: status.running,
|
|
269
|
+
sweeps: status.sweeps,
|
|
270
|
+
fired: status.fired,
|
|
271
|
+
errors: status.errors,
|
|
272
|
+
...(status.lastSweepAt !== undefined ? { lastSweepAt: status.lastSweepAt } : {}),
|
|
273
|
+
...(status.nextWakeAt !== undefined ? { nextWakeAt: status.nextWakeAt } : {}),
|
|
274
|
+
};
|
|
275
|
+
} catch (err) {
|
|
276
|
+
checks.workflowTimers = {
|
|
277
|
+
status: 'fail',
|
|
278
|
+
running: false,
|
|
279
|
+
sweeps: 0,
|
|
280
|
+
fired: 0,
|
|
281
|
+
errors: 0,
|
|
282
|
+
message: err instanceof Error ? err.message : String(err),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (options.replayBuffer !== undefined) {
|
|
288
|
+
const probe = options.replayBuffer;
|
|
289
|
+
checks.replayBuffer = {
|
|
290
|
+
status: 'ok',
|
|
291
|
+
eventsBuffered: probe.eventsBuffered,
|
|
292
|
+
...(probe.subscribers !== undefined ? { subscribers: probe.subscribers } : {}),
|
|
293
|
+
...(probe.subscriptions !== undefined ? { subscriptions: probe.subscriptions } : {}),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const status = rollup(checks);
|
|
298
|
+
return Object.freeze({ status, checks: Object.freeze(checks) as HealthChecks });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Promote the worst per-check status to the rollup label.
|
|
303
|
+
*
|
|
304
|
+
* @stable
|
|
305
|
+
*/
|
|
306
|
+
export function rollup(checks: HealthChecks): HealthRollup {
|
|
307
|
+
let worst: HealthRollup = 'ok';
|
|
308
|
+
for (const value of Object.values(checks)) {
|
|
309
|
+
if (value === undefined) continue;
|
|
310
|
+
if (value.status === 'fail') return 'failing';
|
|
311
|
+
if (value.status === 'warn') worst = 'degraded';
|
|
312
|
+
}
|
|
313
|
+
return worst;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function readWalSizeSafely(store: GraphorinSqliteStore): number {
|
|
317
|
+
try {
|
|
318
|
+
return readWalSize(store.connection);
|
|
319
|
+
} catch {
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@graphorin/server/health` - Phase 14c health + Prometheus metrics
|
|
3
|
+
* surface. Phase 14a's `/v1/health` is superseded by the extended
|
|
4
|
+
* routes built here when consumers wire the additional probes.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
type BaseHealthCheck,
|
|
11
|
+
type ConsolidatorCheck,
|
|
12
|
+
collectHealth,
|
|
13
|
+
type EmbedderCheck,
|
|
14
|
+
type EncryptionCheck,
|
|
15
|
+
type HealthCheck,
|
|
16
|
+
type HealthCheckOptions,
|
|
17
|
+
type HealthChecks,
|
|
18
|
+
type HealthRollup,
|
|
19
|
+
type HealthStatus,
|
|
20
|
+
type HealthSummary,
|
|
21
|
+
type ReplayBufferCheck,
|
|
22
|
+
type ReplayBufferProbe,
|
|
23
|
+
rollup,
|
|
24
|
+
type SecretsCheck,
|
|
25
|
+
type StorageCheck,
|
|
26
|
+
type TriggersCheck,
|
|
27
|
+
} from './checks.js';
|
|
28
|
+
export {
|
|
29
|
+
createExtendedHealthRoutes,
|
|
30
|
+
createMetricsRoutes,
|
|
31
|
+
createSecretsHealthRoutes,
|
|
32
|
+
type HealthRouteOptions,
|
|
33
|
+
type MetricsRoutesOptions,
|
|
34
|
+
} from './routes.js';
|