@yiaany/ghostapi 0.1.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CONTRIBUTING.md +132 -119
  3. package/LICENSE +21 -21
  4. package/PROVENANCE.md +13 -0
  5. package/README.md +78 -84
  6. package/SECURITY.md +56 -53
  7. package/dist/actions/gateway.js +320 -72
  8. package/dist/actions/gateway.js.map +1 -1
  9. package/dist/actions/index.d.ts +2 -2
  10. package/dist/actions/index.js +1 -1
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/agents/agentPrompt.js +7 -5
  13. package/dist/agents/agentPrompt.js.map +1 -1
  14. package/dist/ai/aiClient.js +5 -5
  15. package/dist/ai/aiClient.js.map +1 -1
  16. package/dist/ai/aiGenerator.js +66 -26
  17. package/dist/ai/aiGenerator.js.map +1 -1
  18. package/dist/ai/genericInference.js +151 -36
  19. package/dist/ai/genericInference.js.map +1 -1
  20. package/dist/ai/prompts.js +55 -49
  21. package/dist/ai/prompts.js.map +1 -1
  22. package/dist/approvals/approvalInbox.js +649 -133
  23. package/dist/approvals/approvalInbox.js.map +1 -1
  24. package/dist/approvals/index.d.ts +2 -2
  25. package/dist/approvals/index.js +1 -1
  26. package/dist/approvals/index.js.map +1 -1
  27. package/dist/behavior/behaviorStore.js +34 -14
  28. package/dist/behavior/behaviorStore.js.map +1 -1
  29. package/dist/cache/index.js +14 -5
  30. package/dist/cache/index.js.map +1 -1
  31. package/dist/cli/index.js +226 -93
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/openUrl.js +17 -3
  34. package/dist/cli/openUrl.js.map +1 -1
  35. package/dist/cli/parser.js +79 -18
  36. package/dist/cli/parser.js.map +1 -1
  37. package/dist/config/dataPaths.js +4 -2
  38. package/dist/config/dataPaths.js.map +1 -1
  39. package/dist/config/localConfig.js +5 -2
  40. package/dist/config/localConfig.js.map +1 -1
  41. package/dist/config/serverConfig.js +11 -5
  42. package/dist/config/serverConfig.js.map +1 -1
  43. package/dist/contracts/contract.js +311 -83
  44. package/dist/contracts/contract.js.map +1 -1
  45. package/dist/contracts/index.d.ts +2 -2
  46. package/dist/contracts/index.js +1 -1
  47. package/dist/contracts/index.js.map +1 -1
  48. package/dist/credentials/credentialBroker.js +391 -79
  49. package/dist/credentials/credentialBroker.js.map +1 -1
  50. package/dist/credentials/index.d.ts +2 -2
  51. package/dist/credentials/index.js +1 -1
  52. package/dist/credentials/index.js.map +1 -1
  53. package/dist/dashboard/app.js +823 -685
  54. package/dist/dashboard/dashboard.js.map +1 -1
  55. package/dist/dashboard/index.html +704 -267
  56. package/dist/dashboard/styles.css +1040 -349
  57. package/dist/egress/capabilities.js +55 -25
  58. package/dist/egress/capabilities.js.map +1 -1
  59. package/dist/egress/linuxBootstrap.js +47 -11
  60. package/dist/egress/linuxBootstrap.js.map +1 -1
  61. package/dist/egress/run.js +170 -31
  62. package/dist/egress/run.js.map +1 -1
  63. package/dist/errors/errorEngine.js +15 -3
  64. package/dist/errors/errorEngine.js.map +1 -1
  65. package/dist/errors/providerErrors.js.map +1 -1
  66. package/dist/evals/evals.js +304 -78
  67. package/dist/evals/evals.js.map +1 -1
  68. package/dist/evals/index.d.ts +2 -2
  69. package/dist/evals/index.js +1 -1
  70. package/dist/evals/index.js.map +1 -1
  71. package/dist/evidence/index.d.ts +2 -2
  72. package/dist/evidence/index.js +1 -1
  73. package/dist/evidence/index.js.map +1 -1
  74. package/dist/evidence/report.d.ts +1 -1
  75. package/dist/evidence/report.js +263 -79
  76. package/dist/evidence/report.js.map +1 -1
  77. package/dist/fault/faultLab.js +24 -9
  78. package/dist/fault/faultLab.js.map +1 -1
  79. package/dist/index.d.ts +42 -42
  80. package/dist/index.js +21 -21
  81. package/dist/index.js.map +1 -1
  82. package/dist/inventory/analysis.d.ts +1 -1
  83. package/dist/inventory/analysis.js +174 -50
  84. package/dist/inventory/analysis.js.map +1 -1
  85. package/dist/inventory/index.d.ts +4 -4
  86. package/dist/inventory/index.js +3 -3
  87. package/dist/inventory/index.js.map +1 -1
  88. package/dist/inventory/inventoryController.js +362 -115
  89. package/dist/inventory/inventoryController.js.map +1 -1
  90. package/dist/inventory/types.js +680 -140
  91. package/dist/inventory/types.js.map +1 -1
  92. package/dist/ledger/actionLedger.js +355 -78
  93. package/dist/ledger/actionLedger.js.map +1 -1
  94. package/dist/ledger/index.d.ts +2 -2
  95. package/dist/ledger/index.js +1 -1
  96. package/dist/ledger/index.js.map +1 -1
  97. package/dist/mcp/server.js +26 -11
  98. package/dist/mcp/server.js.map +1 -1
  99. package/dist/policy/index.d.ts +2 -2
  100. package/dist/policy/index.js +1 -1
  101. package/dist/policy/index.js.map +1 -1
  102. package/dist/policy/policy.js +112 -32
  103. package/dist/policy/policy.js.map +1 -1
  104. package/dist/productTelemetry/index.js +71 -20
  105. package/dist/productTelemetry/index.js.map +1 -1
  106. package/dist/providers/conformance.js +10 -5
  107. package/dist/providers/conformance.js.map +1 -1
  108. package/dist/providers/discord.js +2 -2
  109. package/dist/providers/generic.js +3 -3
  110. package/dist/providers/github.js +2 -2
  111. package/dist/providers/index.d.ts +3 -3
  112. package/dist/providers/index.js +2 -2
  113. package/dist/providers/index.js.map +1 -1
  114. package/dist/providers/openai.js +3 -3
  115. package/dist/providers/packs/resendPack.js +51 -25
  116. package/dist/providers/packs/resendPack.js.map +1 -1
  117. package/dist/providers/packs/stripePack.js +590 -121
  118. package/dist/providers/packs/stripePack.js.map +1 -1
  119. package/dist/providers/registry.js +4 -4
  120. package/dist/providers/registry.js.map +1 -1
  121. package/dist/providers/runtime.js +7 -6
  122. package/dist/providers/runtime.js.map +1 -1
  123. package/dist/providers/stripeWebhook.js +39 -10
  124. package/dist/providers/stripeWebhook.js.map +1 -1
  125. package/dist/providers/twilio.js +2 -2
  126. package/dist/proxy/cacheKey.js +9 -2
  127. package/dist/proxy/cacheKey.js.map +1 -1
  128. package/dist/proxy/providerDetector.js +22 -9
  129. package/dist/proxy/providerDetector.js.map +1 -1
  130. package/dist/proxy/proxyHandler.js +57 -22
  131. package/dist/proxy/proxyHandler.js.map +1 -1
  132. package/dist/proxy/requestNormalizer.js +4 -2
  133. package/dist/proxy/requestNormalizer.js.map +1 -1
  134. package/dist/reliability/costGovernance.js +211 -49
  135. package/dist/reliability/costGovernance.js.map +1 -1
  136. package/dist/reliability/index.d.ts +8 -8
  137. package/dist/reliability/index.js +4 -4
  138. package/dist/reliability/index.js.map +1 -1
  139. package/dist/reliability/reconciliation.js +274 -65
  140. package/dist/reliability/reconciliation.js.map +1 -1
  141. package/dist/reliability/runtimeHealth.js +127 -35
  142. package/dist/reliability/runtimeHealth.js.map +1 -1
  143. package/dist/reliability/slo.js +257 -50
  144. package/dist/reliability/slo.js.map +1 -1
  145. package/dist/report/safetyReport.d.ts +6 -0
  146. package/dist/report/safetyReport.js +56 -12
  147. package/dist/report/safetyReport.js.map +1 -1
  148. package/dist/rules/aiRules.js +5 -5
  149. package/dist/rules/aiRules.js.map +1 -1
  150. package/dist/safety/index.d.ts +2 -2
  151. package/dist/safety/index.js +1 -1
  152. package/dist/safety/index.js.map +1 -1
  153. package/dist/safety/safetyController.js +675 -165
  154. package/dist/safety/safetyController.js.map +1 -1
  155. package/dist/scenarios/scenarioBundle.js +204 -71
  156. package/dist/scenarios/scenarioBundle.js.map +1 -1
  157. package/dist/scenarios/scenarioStore.js +88 -29
  158. package/dist/scenarios/scenarioStore.js.map +1 -1
  159. package/dist/security/headerSanitizer.js +52 -10
  160. package/dist/security/headerSanitizer.js.map +1 -1
  161. package/dist/security/secrets.js +10 -5
  162. package/dist/security/secrets.js.map +1 -1
  163. package/dist/server/accessControl.js +37 -10
  164. package/dist/server/accessControl.js.map +1 -1
  165. package/dist/server/createServer.js +1 -1
  166. package/dist/server/createServer.js.map +1 -1
  167. package/dist/server/eventsStore.js +15 -5
  168. package/dist/server/eventsStore.js.map +1 -1
  169. package/dist/server/routes.js +61 -19
  170. package/dist/server/routes.js.map +1 -1
  171. package/dist/server/sse.js +1 -1
  172. package/dist/server/sse.js.map +1 -1
  173. package/dist/setup/sdkDetector.js +7 -2
  174. package/dist/setup/sdkDetector.js.map +1 -1
  175. package/dist/setup/setupGenerator.js +136 -48
  176. package/dist/setup/setupGenerator.js.map +1 -1
  177. package/dist/state/stateExtractor.js +3 -1
  178. package/dist/state/stateExtractor.js.map +1 -1
  179. package/dist/state/stateResolver.js +10 -7
  180. package/dist/state/stateResolver.js.map +1 -1
  181. package/dist/state/stateStore.js +13 -4
  182. package/dist/state/stateStore.js.map +1 -1
  183. package/dist/storage/fileStore.js +12 -4
  184. package/dist/storage/fileStore.js.map +1 -1
  185. package/dist/teamControl/controlPlane.js +846 -174
  186. package/dist/teamControl/controlPlane.js.map +1 -1
  187. package/dist/teamControl/deployment.js +19 -5
  188. package/dist/teamControl/deployment.js.map +1 -1
  189. package/dist/teamControl/index.d.ts +4 -4
  190. package/dist/teamControl/index.js +2 -2
  191. package/dist/teamControl/index.js.map +1 -1
  192. package/dist/tests/testGenerator.js +15 -7
  193. package/dist/tests/testGenerator.js.map +1 -1
  194. package/dist/trust/index.d.ts +2 -2
  195. package/dist/trust/index.js +1 -1
  196. package/dist/trust/index.js.map +1 -1
  197. package/dist/trust/trustLadder.js +309 -63
  198. package/dist/trust/trustLadder.js.map +1 -1
  199. package/dist/worlds/index.d.ts +2 -2
  200. package/dist/worlds/index.js +1 -1
  201. package/dist/worlds/index.js.map +1 -1
  202. package/dist/worlds/worlds.js +364 -65
  203. package/dist/worlds/worlds.js.map +1 -1
  204. package/docs/assets/dashboard.png +0 -0
  205. package/docs/ci.md +49 -49
  206. package/docs/github-actions.md +84 -84
  207. package/docs/mcp.md +58 -52
  208. package/docs/policy.md +63 -61
  209. package/docs/providers/authoring-packs.md +180 -175
  210. package/docs/providers/stripe-core-pack.md +84 -82
  211. package/docs/releases/README.md +13 -0
  212. package/docs/security/action-gateway-threat-model.md +34 -34
  213. package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
  214. package/docs/security/approval-inbox-threat-model.md +35 -35
  215. package/docs/security/credential-broker-threat-model.md +52 -52
  216. package/docs/security/egress-threat-model.md +126 -126
  217. package/docs/security/inventory-threat-model.md +104 -104
  218. package/docs/security/kill-switch-budgets-threat-model.md +37 -37
  219. package/docs/security/reliability-threat-model.md +111 -111
  220. package/docs/security/trust-ladder-threat-model.md +33 -33
  221. package/docs/telemetry.md +14 -0
  222. package/docs/usage.md +680 -571
  223. package/examples/README.md +10 -10
  224. package/examples/agent-instructions/README.md +39 -39
  225. package/examples/ci-smoke/README.md +21 -21
  226. package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
  227. package/examples/ci-smoke/package.json +9 -9
  228. package/examples/ci-smoke/production-egress.mjs +4 -2
  229. package/examples/ci-smoke/safe.mjs +21 -17
  230. package/examples/evals/README.md +24 -24
  231. package/examples/evals/retry-after.eval.json +71 -52
  232. package/examples/generic-rest/README.md +27 -27
  233. package/examples/github-fetch/README.md +38 -35
  234. package/examples/openai-streaming/README.md +16 -16
  235. package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
  236. package/examples/policy/ghostapi.policy.yaml +27 -27
  237. package/examples/record-replay/README.md +19 -19
  238. package/examples/record-replay/replay-requests.json +22 -22
  239. package/examples/record-replay/stripe-checkout.har.json +30 -30
  240. package/examples/resend-node/README.md +22 -22
  241. package/examples/stripe-node/README.md +35 -35
  242. package/examples/stripe-node/checkout-flow.mjs +87 -61
  243. package/examples/twilio-node/README.md +21 -21
  244. package/examples/worlds/README.md +19 -19
  245. package/examples/worlds/subscription-recovery.mjs +21 -12
  246. package/package.json +28 -6
  247. package/dist/landing/assets/index-BXR41fAD.css +0 -1
  248. package/dist/landing/assets/index-CxWsopzP.css +0 -1
  249. package/dist/landing/assets/index-D8_ggMRE.js +0 -55
  250. package/dist/landing/assets/index-DZlpyhqd.js +0 -55
  251. package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
  252. package/dist/landing/index.html +0 -15
  253. package/dist/landing/landing.d.ts +0 -3
  254. package/dist/landing/landing.js +0 -12
  255. package/dist/landing/landing.js.map +0 -1
  256. package/docs/commercial/README.md +0 -29
  257. package/docs/commercial/data-inventory.md +0 -36
  258. package/docs/commercial/manual-invoicing.md +0 -44
  259. package/docs/commercial/metrics.md +0 -51
  260. package/docs/commercial/pricing.md +0 -103
  261. package/docs/commercial/privacy-policy-draft.md +0 -41
  262. package/docs/commercial/terms-topics-for-counsel.md +0 -51
  263. package/docs/design-partners/README.md +0 -52
  264. package/docs/design-partners/demo-narratives.md +0 -33
  265. package/docs/design-partners/design-partner-offer.md +0 -39
  266. package/docs/design-partners/discovery-questions.md +0 -28
  267. package/docs/design-partners/feedback-capture-template.md +0 -48
  268. package/docs/design-partners/icp-one-pager.md +0 -43
  269. package/docs/design-partners/onboarding-checklist.md +0 -33
  270. package/docs/design-partners/pilot-success-criteria.md +0 -27
  271. package/docs/design-partners/pricing-interview-script.md +0 -18
  272. package/docs/design-partners/telemetry-plan.md +0 -48
  273. package/docs/development/baseline.md +0 -114
  274. package/docs/development/onboarding-smoke.md +0 -37
  275. package/docs/development/verification-0.1.8.md +0 -25
  276. package/docs/enterprise-product-roadmap-ru.md +0 -1879
  277. package/docs/fundraising/README.md +0 -87
  278. package/docs/fundraising/data-room-checklist.md +0 -72
  279. package/docs/fundraising/demo-script.md +0 -52
  280. package/docs/fundraising/design-partners-50.md +0 -74
  281. package/docs/fundraising/launch-posts.md +0 -37
  282. package/docs/fundraising/metrics-and-evidence.md +0 -30
  283. package/docs/fundraising/roadmap-12-month.md +0 -25
  284. package/docs/fundraising/technical-due-diligence-index.md +0 -37
  285. package/docs/fundraising/yc-application.md +0 -25
  286. package/docs/hosted-pilot.md +0 -111
  287. package/docs/operations/disaster-recovery-runbook.md +0 -81
  288. package/docs/operations/kill-switch-runbook.md +0 -32
  289. package/docs/release-checklist.md +0 -83
  290. package/docs/release-migration-and-rollback.md +0 -63
  291. package/docs/release-readiness.md +0 -39
  292. package/docs/team-control-plane.md +0 -72
@@ -3,7 +3,7 @@ import { lstat, readFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  import { getDataPaths } from "../config/dataPaths.js";
5
5
  import { sanitizeSecretString } from "../security/secrets.js";
6
- import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
6
+ import { atomicWriteJson, ensurePrivateDirectory, withFileLock, } from "../storage/fileStore.js";
7
7
  const SCHEMA_VERSION = 1;
8
8
  const MAX_STORE_BYTES = 1024 * 1024;
9
9
  const MAX_CONTROLS = 1_000;
@@ -15,7 +15,17 @@ const DEFAULT_LEASE_TTL_MS = 60_000;
15
15
  const KILL_SWITCH_ERROR_CODE = "SAFETY_KILL_SWITCH";
16
16
  const IDENTIFIER = /^[a-z0-9][a-z0-9._-]{0,127}$/;
17
17
  const HASH = /^[a-f0-9]{64}$/;
18
- export const SAFETY_SCOPE_KINDS = ["global", "organization", "project", "environment", "agent", "workload", "provider", "operation", "risk_class"];
18
+ export const SAFETY_SCOPE_KINDS = [
19
+ "global",
20
+ "organization",
21
+ "project",
22
+ "environment",
23
+ "agent",
24
+ "workload",
25
+ "provider",
26
+ "operation",
27
+ "risk_class",
28
+ ];
19
29
  export class SafetyControllerError extends Error {
20
30
  code;
21
31
  constructor(message, code) {
@@ -36,7 +46,8 @@ export class LocalSafetyController {
36
46
  constructor(options = {}) {
37
47
  this.path = options.path ?? getDataPaths().safetyController;
38
48
  this.now = options.now ?? (() => new Date());
39
- this.emergencyAuthorizer = options.emergencyAuthorizer ?? createDisabledSafetyEmergencyAuthorizer();
49
+ this.emergencyAuthorizer =
50
+ options.emergencyAuthorizer ?? createDisabledSafetyEmergencyAuthorizer();
40
51
  this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
41
52
  }
42
53
  async configureBudget(input) {
@@ -98,12 +109,12 @@ export class LocalSafetyController {
98
109
  }
99
110
  async admit(actionValue) {
100
111
  const action = validateAction(actionValue);
101
- const now = this.timestamp();
102
112
  const key = ledgerKey(action);
103
113
  const replay = await this.mutate((state, current) => {
104
114
  pruneExpiredLeases(state, current, this.leaseTtlMs);
105
115
  const sameIdempotency = state.ledger.find((candidate) => candidate.action.idempotencyKey === action.idempotencyKey);
106
- if (sameIdempotency !== undefined && sameIdempotency.action.actionHash !== action.actionHash)
116
+ if (sameIdempotency !== undefined &&
117
+ sameIdempotency.action.actionHash !== action.actionHash)
107
118
  throw new SafetyControllerError("Safety idempotency key collides with a different action hash.");
108
119
  const existing = state.ledger.find((candidate) => candidate.key === key);
109
120
  if (existing !== undefined)
@@ -112,7 +123,12 @@ export class LocalSafetyController {
112
123
  if (state.ledger.length >= MAX_LEDGER)
113
124
  throw new SafetyControllerError("Safety ledger limit was reached.");
114
125
  assertBudgets(state, action, current);
115
- state.ledger.push({ key, action, status: "reserved", admittedAt: current });
126
+ state.ledger.push({
127
+ key,
128
+ action,
129
+ status: "reserved",
130
+ admittedAt: current,
131
+ });
116
132
  appendAudit(state, "action.admitted", key, "budget reservation created", current);
117
133
  return false;
118
134
  });
@@ -168,11 +184,14 @@ export class LocalSafetyController {
168
184
  entry.completedAt = current;
169
185
  entry.outcome = outcome;
170
186
  }
171
- appendAudit(state, outcome.success ? "action.completed" : "action.unknown", key, outcome.reason ?? (outcome.success ? "action completed" : "action outcome is unknown"), current);
187
+ appendAudit(state, outcome.success ? "action.completed" : "action.unknown", key, outcome.reason ??
188
+ (outcome.success
189
+ ? "action completed"
190
+ : "action outcome is unknown"), current);
172
191
  if (entry !== undefined)
173
192
  evaluateCircuits(state, entry.action, current);
174
193
  });
175
- }
194
+ },
176
195
  };
177
196
  }
178
197
  async enqueue(actionValue) {
@@ -184,7 +203,11 @@ export class LocalSafetyController {
184
203
  return clone(existing);
185
204
  if (state.queue.length >= MAX_QUEUE)
186
205
  throw new SafetyControllerError("Safety queue is full; backpressure is active.");
187
- const item = { id: `queue-${sha256(`${ledgerKey(action)}:${now}`).slice(0, 32)}`, action, queuedAt: now };
206
+ const item = {
207
+ id: `queue-${sha256(`${ledgerKey(action)}:${now}`).slice(0, 32)}`,
208
+ action,
209
+ queuedAt: now,
210
+ };
188
211
  state.queue.push(item);
189
212
  appendAudit(state, "queue.enqueued", item.id, "action is queued without execution", now);
190
213
  return clone(item);
@@ -199,7 +222,9 @@ export class LocalSafetyController {
199
222
  assertActionAllowed(state, item.action, now);
200
223
  }
201
224
  catch (error) {
202
- const reason = error instanceof Error ? error.message : "Safety queue action is denied.";
225
+ const reason = error instanceof Error
226
+ ? error.message
227
+ : "Safety queue action is denied.";
203
228
  addDeadLetter(state, item, reason, now);
204
229
  appendAudit(state, "queue.dead_lettered", item.id, reason, now);
205
230
  return null;
@@ -215,19 +240,34 @@ export class LocalSafetyController {
215
240
  const before = await this.read();
216
241
  if (before.switches.some((control) => sameScope(control.scope, globalScope) && control.enabled))
217
242
  throw new SafetyControllerError("Game-day test requires an inactive global kill switch.");
218
- await this.stop({ identity: input.stopIdentity, scope: globalScope, reason: `${reason} stop drill` });
243
+ await this.stop({
244
+ identity: input.stopIdentity,
245
+ scope: globalScope,
246
+ reason: `${reason} stop drill`,
247
+ });
219
248
  const stoppedAt = this.timestamp();
220
- await this.admit(action).then(() => { throw new SafetyControllerError("Game-day kill switch did not block the action."); }, (error) => {
221
- if (!(error instanceof SafetyControllerError) || error.code !== KILL_SWITCH_ERROR_CODE)
249
+ await this.admit(action).then(() => {
250
+ throw new SafetyControllerError("Game-day kill switch did not block the action.");
251
+ }, (error) => {
252
+ if (!(error instanceof SafetyControllerError) ||
253
+ error.code !== KILL_SWITCH_ERROR_CODE)
222
254
  throw error;
223
255
  });
224
- await this.reenable({ identity: input.reenableIdentity, scope: globalScope, reason: `${reason} recovery drill` });
256
+ await this.reenable({
257
+ identity: input.reenableIdentity,
258
+ scope: globalScope,
259
+ reason: `${reason} recovery drill`,
260
+ });
225
261
  const reenabledAt = this.timestamp();
226
262
  await this.mutate((state, now) => appendAudit(state, "game_day.passed", ledgerKey(action), reason, now));
227
263
  return { passed: true, stoppedAt, reenabledAt };
228
264
  }
229
- async inspect() { return this.read(); }
230
- async readStateForTesting() { return this.read(); }
265
+ async inspect() {
266
+ return this.read();
267
+ }
268
+ async readStateForTesting() {
269
+ return this.read();
270
+ }
231
271
  async authorize(identity, permission) {
232
272
  const operator = validateOperator(await this.emergencyAuthorizer.authenticate(identity));
233
273
  if (!operator.permissions.includes(permission))
@@ -268,24 +308,62 @@ export class LocalSafetyController {
268
308
  return value.toISOString();
269
309
  }
270
310
  leaseExpired(entry, now) {
271
- return entry.status === "reserved" && Date.parse(now) - Date.parse(entry.admittedAt) > this.leaseTtlMs;
311
+ return (entry.status === "reserved" &&
312
+ Date.parse(now) - Date.parse(entry.admittedAt) > this.leaseTtlMs);
272
313
  }
273
314
  }
274
315
  export function createDisabledSafetyEmergencyAuthorizer() {
275
- return { async authenticate() { throw new SafetyControllerError("Emergency authorization is not configured."); } };
316
+ return {
317
+ async authenticate() {
318
+ throw new SafetyControllerError("Emergency authorization is not configured.");
319
+ },
320
+ };
276
321
  }
277
322
  export function createTestSafetyEmergencyAuthorizer() {
278
323
  const issued = new WeakSet();
279
324
  return {
280
- authorizer: { async authenticate(identity) { if (identity === null || typeof identity !== "object" || !issued.has(identity))
281
- throw new SafetyControllerError("Emergency operator identity is not authenticated."); return validateOperator(identity); } },
282
- issue(input) { const operator = Object.freeze(validateOperator(input)); issued.add(operator); return operator; }
325
+ authorizer: {
326
+ async authenticate(identity) {
327
+ if (identity === null ||
328
+ typeof identity !== "object" ||
329
+ !issued.has(identity))
330
+ throw new SafetyControllerError("Emergency operator identity is not authenticated.");
331
+ return validateOperator(identity);
332
+ },
333
+ },
334
+ issue(input) {
335
+ const operator = Object.freeze(validateOperator(input));
336
+ issued.add(operator);
337
+ return operator;
338
+ },
339
+ };
340
+ }
341
+ function emptyState() {
342
+ return {
343
+ schemaVersion: 1,
344
+ switches: [],
345
+ budgets: [],
346
+ circuits: [],
347
+ ledger: [],
348
+ queue: [],
349
+ deadLetters: [],
350
+ auditAnchor: sha256("ghostapi.safety.audit.v1"),
351
+ audit: [],
283
352
  };
284
353
  }
285
- function emptyState() { return { schemaVersion: 1, switches: [], budgets: [], circuits: [], ledger: [], queue: [], deadLetters: [], auditAnchor: sha256("ghostapi.safety.audit.v1"), audit: [] }; }
286
354
  function validateState(value) {
287
355
  const state = object(value, "Safety controller store must be an object.");
288
- exactKeys(state, ["schemaVersion", "switches", "budgets", "circuits", "ledger", "queue", "deadLetters", "auditAnchor", "audit"], "Safety controller store");
356
+ exactKeys(state, [
357
+ "schemaVersion",
358
+ "switches",
359
+ "budgets",
360
+ "circuits",
361
+ "ledger",
362
+ "queue",
363
+ "deadLetters",
364
+ "auditAnchor",
365
+ "audit",
366
+ ], "Safety controller store");
289
367
  if (state.schemaVersion !== SCHEMA_VERSION)
290
368
  throw new SafetyControllerError("Unsupported safety controller schema version.");
291
369
  const switches = array(state.switches, "Safety switches", MAX_CONTROLS).map(validateSwitch);
@@ -304,29 +382,105 @@ function validateState(value) {
304
382
  const auditAnchor = hash(state.auditAnchor, "Safety audit anchor");
305
383
  const audit = array(state.audit, "Safety audit", MAX_AUDIT).map(validateAudit);
306
384
  validateAuditChain(auditAnchor, audit);
307
- return { schemaVersion: 1, switches, budgets, circuits, ledger, queue, deadLetters, auditAnchor, audit };
385
+ return {
386
+ schemaVersion: 1,
387
+ switches,
388
+ budgets,
389
+ circuits,
390
+ ledger,
391
+ queue,
392
+ deadLetters,
393
+ auditAnchor,
394
+ audit,
395
+ };
396
+ }
397
+ function validateSwitch(value) {
398
+ const control = object(value, "Safety switch is invalid.");
399
+ exactKeys(control, ["scope", "enabled", "changedAt", "reason", "changedBy"], "Safety switch");
400
+ if (typeof control.enabled !== "boolean")
401
+ throw new SafetyControllerError("Safety switch enabled flag is invalid.");
402
+ return {
403
+ scope: validateScope(control.scope),
404
+ enabled: control.enabled,
405
+ changedAt: timestamp(control.changedAt, "Safety switch time"),
406
+ reason: text(control.reason, "Safety switch reason", 200),
407
+ changedBy: identifier(control.changedBy, "Safety switch operator"),
408
+ };
308
409
  }
309
- function validateSwitch(value) { const control = object(value, "Safety switch is invalid."); exactKeys(control, ["scope", "enabled", "changedAt", "reason", "changedBy"], "Safety switch"); if (typeof control.enabled !== "boolean")
310
- throw new SafetyControllerError("Safety switch enabled flag is invalid."); return { scope: validateScope(control.scope), enabled: control.enabled, changedAt: timestamp(control.changedAt, "Safety switch time"), reason: text(control.reason, "Safety switch reason", 200), changedBy: identifier(control.changedBy, "Safety switch operator") }; }
311
410
  function validateBudget(value) {
312
411
  const budget = object(value, "Safety budget is invalid.");
313
412
  exactKeys(budget, ["id", "scope", "windowMs", "velocityWindowMs", "limits"], "Safety budget");
314
413
  const limits = object(budget.limits, "Safety budget limits are invalid.");
315
- exactKeys(limits, ["monetaryAmountMinor", "requests", "messages", "mutations", "deletes", "tokenCost", "concurrency", "velocity"], "Safety budget limits", ["monetaryAmountMinor", "requests", "messages", "mutations", "deletes", "tokenCost", "concurrency", "velocity"]);
414
+ exactKeys(limits, [
415
+ "monetaryAmountMinor",
416
+ "requests",
417
+ "messages",
418
+ "mutations",
419
+ "deletes",
420
+ "tokenCost",
421
+ "concurrency",
422
+ "velocity",
423
+ ], "Safety budget limits", [
424
+ "monetaryAmountMinor",
425
+ "requests",
426
+ "messages",
427
+ "mutations",
428
+ "deletes",
429
+ "tokenCost",
430
+ "concurrency",
431
+ "velocity",
432
+ ]);
316
433
  const parsed = {};
317
- for (const key of ["monetaryAmountMinor", "requests", "messages", "mutations", "deletes", "tokenCost", "concurrency", "velocity"])
434
+ for (const key of [
435
+ "monetaryAmountMinor",
436
+ "requests",
437
+ "messages",
438
+ "mutations",
439
+ "deletes",
440
+ "tokenCost",
441
+ "concurrency",
442
+ "velocity",
443
+ ])
318
444
  if (limits[key] !== undefined)
319
445
  parsed[key] = nonNegative(limits[key], `Safety budget ${key}`, Number.MAX_SAFE_INTEGER);
320
446
  if (Object.keys(parsed).length === 0)
321
447
  throw new SafetyControllerError("Safety budget requires at least one limit.");
322
- return { id: identifier(budget.id, "Safety budget id"), scope: validateScope(budget.scope), windowMs: boundedMs(budget.windowMs, "Safety budget window"), velocityWindowMs: boundedMs(budget.velocityWindowMs, "Safety velocity window"), limits: parsed };
448
+ return {
449
+ id: identifier(budget.id, "Safety budget id"),
450
+ scope: validateScope(budget.scope),
451
+ windowMs: boundedMs(budget.windowMs, "Safety budget window"),
452
+ velocityWindowMs: boundedMs(budget.velocityWindowMs, "Safety velocity window"),
453
+ limits: parsed,
454
+ };
323
455
  }
324
456
  function validateCircuit(value) {
325
457
  const circuit = object(value, "Safety circuit is invalid.");
326
- exactKeys(circuit, ["id", "scope", "windowMs", "minimumSamples", "maxFailureRateBps", "maxPolicyViolations", "maxLatencyMs", "maxReconciliationMismatches", "state", "openedAt", "reason"], "Safety circuit", ["openedAt", "reason"]);
458
+ exactKeys(circuit, [
459
+ "id",
460
+ "scope",
461
+ "windowMs",
462
+ "minimumSamples",
463
+ "maxFailureRateBps",
464
+ "maxPolicyViolations",
465
+ "maxLatencyMs",
466
+ "maxReconciliationMismatches",
467
+ "state",
468
+ "openedAt",
469
+ "reason",
470
+ ], "Safety circuit", ["openedAt", "reason"]);
327
471
  if (circuit.state !== "closed" && circuit.state !== "open")
328
472
  throw new SafetyControllerError("Safety circuit state is invalid.");
329
- const result = { id: identifier(circuit.id, "Safety circuit id"), scope: validateScope(circuit.scope), windowMs: boundedMs(circuit.windowMs, "Safety circuit window"), minimumSamples: positive(circuit.minimumSamples, "Safety circuit minimum samples", MAX_LEDGER), maxFailureRateBps: bps(circuit.maxFailureRateBps, "Safety circuit failure rate"), maxPolicyViolations: nonNegative(circuit.maxPolicyViolations, "Safety circuit policy violations", MAX_LEDGER), maxLatencyMs: nonNegative(circuit.maxLatencyMs, "Safety circuit latency", 24 * 60 * 60 * 1000), maxReconciliationMismatches: nonNegative(circuit.maxReconciliationMismatches, "Safety circuit reconciliation mismatches", MAX_LEDGER), state: circuit.state };
473
+ const result = {
474
+ id: identifier(circuit.id, "Safety circuit id"),
475
+ scope: validateScope(circuit.scope),
476
+ windowMs: boundedMs(circuit.windowMs, "Safety circuit window"),
477
+ minimumSamples: positive(circuit.minimumSamples, "Safety circuit minimum samples", MAX_LEDGER),
478
+ maxFailureRateBps: bps(circuit.maxFailureRateBps, "Safety circuit failure rate"),
479
+ maxPolicyViolations: nonNegative(circuit.maxPolicyViolations, "Safety circuit policy violations", MAX_LEDGER),
480
+ maxLatencyMs: nonNegative(circuit.maxLatencyMs, "Safety circuit latency", 24 * 60 * 60 * 1000),
481
+ maxReconciliationMismatches: nonNegative(circuit.maxReconciliationMismatches, "Safety circuit reconciliation mismatches", MAX_LEDGER),
482
+ state: circuit.state,
483
+ };
330
484
  if (result.state === "open") {
331
485
  if (circuit.openedAt === undefined || circuit.reason === undefined)
332
486
  throw new SafetyControllerError("Open safety circuit requires audit metadata.");
@@ -337,141 +491,497 @@ function validateCircuit(value) {
337
491
  }
338
492
  function validateAction(value) {
339
493
  const action = object(value, "Safety action is invalid.");
340
- exactKeys(action, ["organizationId", "projectId", "environment", "actorId", "workloadId", "provider", "operation", "riskClass", "idempotencyKey", "actionHash", "costs"], "Safety action");
341
- if (action.riskClass !== "read" && action.riskClass !== "write" && action.riskClass !== "money_movement" && action.riskClass !== "external_communication" && action.riskClass !== "delete" && action.riskClass !== "deploy")
494
+ exactKeys(action, [
495
+ "organizationId",
496
+ "projectId",
497
+ "environment",
498
+ "actorId",
499
+ "workloadId",
500
+ "provider",
501
+ "operation",
502
+ "riskClass",
503
+ "idempotencyKey",
504
+ "actionHash",
505
+ "costs",
506
+ ], "Safety action");
507
+ if (action.riskClass !== "read" &&
508
+ action.riskClass !== "write" &&
509
+ action.riskClass !== "money_movement" &&
510
+ action.riskClass !== "external_communication" &&
511
+ action.riskClass !== "delete" &&
512
+ action.riskClass !== "deploy")
342
513
  throw new SafetyControllerError("Safety action risk class is invalid.");
343
514
  const costs = object(action.costs, "Safety action costs are invalid.");
344
- exactKeys(costs, ["monetaryAmountMinor", "requests", "messages", "mutations", "deletes", "tokenCost"], "Safety action costs");
345
- return { organizationId: identifier(action.organizationId, "Safety organization id"), projectId: identifier(action.projectId, "Safety project id"), environment: identifier(action.environment, "Safety environment"), actorId: identifier(action.actorId, "Safety actor id"), workloadId: identifier(action.workloadId, "Safety workload id"), provider: identifier(action.provider, "Safety provider"), operation: identifier(action.operation, "Safety operation"), riskClass: action.riskClass, idempotencyKey: identifier(action.idempotencyKey, "Safety idempotency key"), actionHash: hash(action.actionHash, "Safety action hash"), costs: { monetaryAmountMinor: nonNegative(costs.monetaryAmountMinor, "Safety monetary amount", Number.MAX_SAFE_INTEGER), requests: nonNegative(costs.requests, "Safety request count", Number.MAX_SAFE_INTEGER), messages: nonNegative(costs.messages, "Safety message count", Number.MAX_SAFE_INTEGER), mutations: nonNegative(costs.mutations, "Safety mutation count", Number.MAX_SAFE_INTEGER), deletes: nonNegative(costs.deletes, "Safety delete count", Number.MAX_SAFE_INTEGER), tokenCost: nonNegative(costs.tokenCost, "Safety token cost", Number.MAX_SAFE_INTEGER) } };
346
- }
347
- function validateScope(value) { const scope = object(value, "Safety scope is invalid."); exactKeys(scope, ["kind", "id"], "Safety scope", ["id"]); if (!SAFETY_SCOPE_KINDS.includes(scope.kind))
348
- throw new SafetyControllerError("Safety scope kind is invalid."); if (scope.kind === "global") {
349
- if (scope.id !== undefined)
350
- throw new SafetyControllerError("Global safety scope cannot have an id.");
351
- return { kind: "global" };
352
- } if (scope.id === undefined)
353
- throw new SafetyControllerError("Scoped safety control requires an id."); return { kind: scope.kind, id: identifier(scope.id, "Safety scope id") }; }
354
- function validateOperator(value) { const operator = object(value, "Emergency operator is invalid."); exactKeys(operator, ["id", "principalId", "permissions"], "Emergency operator"); const permissions = array(operator.permissions, "Emergency operator permissions", 8).map((permission) => { if (permission !== "safety.stop" && permission !== "safety.reenable" && permission !== "safety.configure" && permission !== "safety.inspect")
355
- throw new SafetyControllerError("Emergency operator permission is invalid."); return permission; }); unique(permissions, "Emergency operator permissions"); return { id: identifier(operator.id, "Emergency operator id"), principalId: identifier(operator.principalId, "Emergency operator principal id"), permissions }; }
356
- function validateLedger(value) { const entry = object(value, "Safety ledger entry is invalid."); exactKeys(entry, ["key", "action", "status", "admittedAt", "completedAt", "outcome"], "Safety ledger entry", ["completedAt", "outcome"]); if (entry.status !== "reserved" && entry.status !== "completed")
357
- throw new SafetyControllerError("Safety ledger status is invalid."); const action = validateAction(entry.action); const completedAt = entry.completedAt === undefined ? undefined : timestamp(entry.completedAt, "Safety completion time"); const outcome = entry.outcome === undefined ? undefined : validateOutcome(entry.outcome); if (entry.status === "completed" && (completedAt === undefined || outcome === undefined))
358
- throw new SafetyControllerError("Completed safety ledger entry is invalid."); if (entry.status === "reserved" && (completedAt !== undefined || outcome !== undefined))
359
- throw new SafetyControllerError("Reserved safety ledger entry is invalid."); const key = text(entry.key, "Safety ledger key", 300); if (key !== ledgerKey(action))
360
- throw new SafetyControllerError("Safety ledger key does not match its action."); return { key, action, status: entry.status, admittedAt: timestamp(entry.admittedAt, "Safety admission time"), ...(completedAt === undefined ? {} : { completedAt }), ...(outcome === undefined ? {} : { outcome }) }; }
361
- function validateOutcome(value) { const outcome = object(value, "Safety action outcome is invalid."); exactKeys(outcome, ["success", "policyViolation", "reconciliationMismatch", "latencyMs", "reason"], "Safety action outcome", ["policyViolation", "reconciliationMismatch", "reason"]); if (typeof outcome.success !== "boolean" || (outcome.policyViolation !== undefined && typeof outcome.policyViolation !== "boolean") || (outcome.reconciliationMismatch !== undefined && typeof outcome.reconciliationMismatch !== "boolean"))
362
- throw new SafetyControllerError("Safety action outcome flags are invalid."); return { success: outcome.success, policyViolation: outcome.policyViolation ?? false, reconciliationMismatch: outcome.reconciliationMismatch ?? false, latencyMs: nonNegative(outcome.latencyMs, "Safety action latency", 24 * 60 * 60 * 1000), ...(outcome.reason === undefined ? {} : { reason: text(outcome.reason, "Safety action outcome reason", 200) }) }; }
363
- function validateQueueItem(value) { const item = object(value, "Safety queue item is invalid."); exactKeys(item, ["id", "action", "queuedAt"], "Safety queue item"); return { id: identifier(item.id, "Safety queue id"), action: validateAction(item.action), queuedAt: timestamp(item.queuedAt, "Safety queue time") }; }
364
- function validateDeadLetter(value) { const item = object(value, "Safety dead-letter item is invalid."); exactKeys(item, ["id", "action", "queuedAt", "reason", "deadLetteredAt"], "Safety dead-letter item"); return { id: identifier(item.id, "Safety queue id"), action: validateAction(item.action), queuedAt: timestamp(item.queuedAt, "Safety queue time"), reason: text(item.reason, "Safety dead-letter reason", 200), deadLetteredAt: timestamp(item.deadLetteredAt, "Safety dead-letter time") }; }
365
- function validateAudit(value) { const record = object(value, "Safety audit record is invalid."); exactKeys(record, ["sequence", "event", "subject", "reason", "actorPrincipalId", "timestamp", "previousHash", "recordHash"], "Safety audit record", ["actorPrincipalId"]); const base = { sequence: positive(record.sequence, "Safety audit sequence", MAX_AUDIT), event: text(record.event, "Safety audit event", 80), subject: text(record.subject, "Safety audit subject", 300), reason: text(record.reason, "Safety audit reason", 200), ...(record.actorPrincipalId === undefined ? {} : { actorPrincipalId: identifier(record.actorPrincipalId, "Safety audit actor") }), timestamp: timestamp(record.timestamp, "Safety audit time"), previousHash: hash(record.previousHash, "Safety audit previous hash") }; if (hash(record.recordHash, "Safety audit record hash") !== sha256(canonical(base)))
366
- throw new SafetyControllerError("Safety audit record hash is invalid."); return { ...base, recordHash: record.recordHash }; }
367
- function validateAuditChain(anchor, audit) { let previous = anchor; let sequence = 1; for (const record of audit) {
368
- if (record.sequence !== sequence || record.previousHash !== previous)
369
- throw new SafetyControllerError("Safety audit chain is invalid.");
370
- previous = record.recordHash;
371
- sequence += 1;
372
- } }
373
- function setSwitch(state, scope, enabled, reason, principalId, now) { const existing = state.switches.find((candidate) => sameScope(candidate.scope, scope)); if (existing === undefined) {
374
- if (state.switches.length >= MAX_CONTROLS)
375
- throw new SafetyControllerError("Safety switch limit was reached.");
376
- state.switches.push({ scope, enabled, reason, changedBy: principalId, changedAt: now });
377
- }
378
- else {
379
- existing.enabled = enabled;
380
- existing.reason = reason;
381
- existing.changedBy = principalId;
382
- existing.changedAt = now;
383
- } }
384
- function assertActionAllowed(state, action, now) { const stopped = state.switches.find((control) => control.enabled && scopeMatches(control.scope, action)); if (stopped !== undefined)
385
- throw new SafetyControllerError(`Kill switch is active for ${scopeKey(stopped.scope)}.`, KILL_SWITCH_ERROR_CODE); const circuit = state.circuits.find((candidate) => candidate.state === "open" && scopeMatches(candidate.scope, action)); if (circuit !== undefined)
386
- throw new SafetyControllerError(`Safety circuit breaker is open: ${circuit.id}.`); for (const circuitCandidate of state.circuits)
387
- if (circuitCandidate.state === "closed" && scopeMatches(circuitCandidate.scope, action))
388
- evaluateCircuit(state, circuitCandidate, now); const opened = state.circuits.find((candidate) => candidate.state === "open" && scopeMatches(candidate.scope, action)); if (opened !== undefined)
389
- throw new SafetyControllerError(`Safety circuit breaker is open: ${opened.id}.`); }
390
- function assertBudgets(state, action, now) { for (const budget of state.budgets) {
391
- if (!scopeMatches(budget.scope, action))
392
- continue;
393
- const windowEntries = state.ledger.filter((entry) => scopeMatches(budget.scope, entry.action) && Date.parse(entry.admittedAt) >= Date.parse(now) - budget.windowMs);
394
- const velocityEntries = state.ledger.filter((entry) => scopeMatches(budget.scope, entry.action) && Date.parse(entry.admittedAt) >= Date.parse(now) - budget.velocityWindowMs);
395
- const totals = sumCosts(windowEntries.map((entry) => entry.action.costs));
396
- const velocity = sumCosts(velocityEntries.map((entry) => entry.action.costs)).requests;
397
- const concurrent = windowEntries.filter((entry) => entry.status === "reserved").length;
398
- for (const key of ["monetaryAmountMinor", "requests", "messages", "mutations", "deletes", "tokenCost"])
399
- if (budget.limits[key] !== undefined && totals[key] + action.costs[key] > budget.limits[key])
400
- throw new SafetyControllerError(`Safety budget exceeded: ${budget.id} ${key}.`);
401
- if (budget.limits.concurrency !== undefined && concurrent + 1 > budget.limits.concurrency)
402
- throw new SafetyControllerError(`Safety budget exceeded: ${budget.id} concurrency.`);
403
- if (budget.limits.velocity !== undefined && velocity + action.costs.requests > budget.limits.velocity)
404
- throw new SafetyControllerError(`Safety velocity limit exceeded: ${budget.id}.`);
405
- } }
406
- function evaluateCircuits(state, action, now) { for (const circuit of state.circuits)
407
- if (circuit.state === "closed" && scopeMatches(circuit.scope, action))
408
- evaluateCircuit(state, circuit, now); }
409
- function evaluateCircuit(state, circuit, now) { const entries = state.ledger.filter((entry) => entry.status === "completed" && entry.completedAt !== undefined && entry.outcome !== undefined && scopeMatches(circuit.scope, entry.action) && Date.parse(entry.completedAt) >= Date.parse(now) - circuit.windowMs); if (entries.length < circuit.minimumSamples)
410
- return; const failures = entries.filter((entry) => !entry.outcome.success).length; const violations = entries.filter((entry) => entry.outcome.policyViolation).length; const mismatches = entries.filter((entry) => entry.outcome.reconciliationMismatch).length; const latencyExceeded = entries.some((entry) => entry.outcome.latencyMs > circuit.maxLatencyMs); const reason = rateBps(failures, entries.length) > circuit.maxFailureRateBps ? "failure rate exceeded" : violations > circuit.maxPolicyViolations ? "policy violation threshold exceeded" : mismatches > circuit.maxReconciliationMismatches ? "reconciliation mismatch threshold exceeded" : latencyExceeded ? "latency threshold exceeded" : undefined; if (reason !== undefined) {
411
- circuit.state = "open";
412
- circuit.openedAt = now;
413
- circuit.reason = reason;
414
- appendAudit(state, "circuit.opened", circuit.id, reason, now);
415
- } }
416
- function deadLetterMatchingQueued(state, scope, reason, now) { const remaining = []; for (const item of state.queue) {
417
- if (scopeMatches(scope, item.action)) {
418
- addDeadLetter(state, item, `kill switch: ${reason}`, now);
419
- appendAudit(state, "queue.dead_lettered", item.id, "kill switch stopped queued action", now);
515
+ exactKeys(costs, [
516
+ "monetaryAmountMinor",
517
+ "requests",
518
+ "messages",
519
+ "mutations",
520
+ "deletes",
521
+ "tokenCost",
522
+ ], "Safety action costs");
523
+ return {
524
+ organizationId: identifier(action.organizationId, "Safety organization id"),
525
+ projectId: identifier(action.projectId, "Safety project id"),
526
+ environment: identifier(action.environment, "Safety environment"),
527
+ actorId: identifier(action.actorId, "Safety actor id"),
528
+ workloadId: identifier(action.workloadId, "Safety workload id"),
529
+ provider: identifier(action.provider, "Safety provider"),
530
+ operation: identifier(action.operation, "Safety operation"),
531
+ riskClass: action.riskClass,
532
+ idempotencyKey: identifier(action.idempotencyKey, "Safety idempotency key"),
533
+ actionHash: hash(action.actionHash, "Safety action hash"),
534
+ costs: {
535
+ monetaryAmountMinor: nonNegative(costs.monetaryAmountMinor, "Safety monetary amount", Number.MAX_SAFE_INTEGER),
536
+ requests: nonNegative(costs.requests, "Safety request count", Number.MAX_SAFE_INTEGER),
537
+ messages: nonNegative(costs.messages, "Safety message count", Number.MAX_SAFE_INTEGER),
538
+ mutations: nonNegative(costs.mutations, "Safety mutation count", Number.MAX_SAFE_INTEGER),
539
+ deletes: nonNegative(costs.deletes, "Safety delete count", Number.MAX_SAFE_INTEGER),
540
+ tokenCost: nonNegative(costs.tokenCost, "Safety token cost", Number.MAX_SAFE_INTEGER),
541
+ },
542
+ };
543
+ }
544
+ function validateScope(value) {
545
+ const scope = object(value, "Safety scope is invalid.");
546
+ exactKeys(scope, ["kind", "id"], "Safety scope", ["id"]);
547
+ if (!SAFETY_SCOPE_KINDS.includes(scope.kind))
548
+ throw new SafetyControllerError("Safety scope kind is invalid.");
549
+ if (scope.kind === "global") {
550
+ if (scope.id !== undefined)
551
+ throw new SafetyControllerError("Global safety scope cannot have an id.");
552
+ return { kind: "global" };
553
+ }
554
+ if (scope.id === undefined)
555
+ throw new SafetyControllerError("Scoped safety control requires an id.");
556
+ return {
557
+ kind: scope.kind,
558
+ id: identifier(scope.id, "Safety scope id"),
559
+ };
560
+ }
561
+ function validateOperator(value) {
562
+ const operator = object(value, "Emergency operator is invalid.");
563
+ exactKeys(operator, ["id", "principalId", "permissions"], "Emergency operator");
564
+ const permissions = array(operator.permissions, "Emergency operator permissions", 8).map((permission) => {
565
+ if (permission !== "safety.stop" &&
566
+ permission !== "safety.reenable" &&
567
+ permission !== "safety.configure" &&
568
+ permission !== "safety.inspect")
569
+ throw new SafetyControllerError("Emergency operator permission is invalid.");
570
+ return permission;
571
+ });
572
+ unique(permissions, "Emergency operator permissions");
573
+ return {
574
+ id: identifier(operator.id, "Emergency operator id"),
575
+ principalId: identifier(operator.principalId, "Emergency operator principal id"),
576
+ permissions,
577
+ };
578
+ }
579
+ function validateLedger(value) {
580
+ const entry = object(value, "Safety ledger entry is invalid.");
581
+ exactKeys(entry, ["key", "action", "status", "admittedAt", "completedAt", "outcome"], "Safety ledger entry", ["completedAt", "outcome"]);
582
+ if (entry.status !== "reserved" && entry.status !== "completed")
583
+ throw new SafetyControllerError("Safety ledger status is invalid.");
584
+ const action = validateAction(entry.action);
585
+ const completedAt = entry.completedAt === undefined
586
+ ? undefined
587
+ : timestamp(entry.completedAt, "Safety completion time");
588
+ const outcome = entry.outcome === undefined ? undefined : validateOutcome(entry.outcome);
589
+ if (entry.status === "completed" &&
590
+ (completedAt === undefined || outcome === undefined))
591
+ throw new SafetyControllerError("Completed safety ledger entry is invalid.");
592
+ if (entry.status === "reserved" &&
593
+ (completedAt !== undefined || outcome !== undefined))
594
+ throw new SafetyControllerError("Reserved safety ledger entry is invalid.");
595
+ const key = text(entry.key, "Safety ledger key", 300);
596
+ if (key !== ledgerKey(action))
597
+ throw new SafetyControllerError("Safety ledger key does not match its action.");
598
+ return {
599
+ key,
600
+ action,
601
+ status: entry.status,
602
+ admittedAt: timestamp(entry.admittedAt, "Safety admission time"),
603
+ ...(completedAt === undefined ? {} : { completedAt }),
604
+ ...(outcome === undefined ? {} : { outcome }),
605
+ };
606
+ }
607
+ function validateOutcome(value) {
608
+ const outcome = object(value, "Safety action outcome is invalid.");
609
+ exactKeys(outcome, [
610
+ "success",
611
+ "policyViolation",
612
+ "reconciliationMismatch",
613
+ "latencyMs",
614
+ "reason",
615
+ ], "Safety action outcome", ["policyViolation", "reconciliationMismatch", "reason"]);
616
+ if (typeof outcome.success !== "boolean" ||
617
+ (outcome.policyViolation !== undefined &&
618
+ typeof outcome.policyViolation !== "boolean") ||
619
+ (outcome.reconciliationMismatch !== undefined &&
620
+ typeof outcome.reconciliationMismatch !== "boolean"))
621
+ throw new SafetyControllerError("Safety action outcome flags are invalid.");
622
+ return {
623
+ success: outcome.success,
624
+ policyViolation: outcome.policyViolation ?? false,
625
+ reconciliationMismatch: outcome.reconciliationMismatch ?? false,
626
+ latencyMs: nonNegative(outcome.latencyMs, "Safety action latency", 24 * 60 * 60 * 1000),
627
+ ...(outcome.reason === undefined
628
+ ? {}
629
+ : { reason: text(outcome.reason, "Safety action outcome reason", 200) }),
630
+ };
631
+ }
632
+ function validateQueueItem(value) {
633
+ const item = object(value, "Safety queue item is invalid.");
634
+ exactKeys(item, ["id", "action", "queuedAt"], "Safety queue item");
635
+ return {
636
+ id: identifier(item.id, "Safety queue id"),
637
+ action: validateAction(item.action),
638
+ queuedAt: timestamp(item.queuedAt, "Safety queue time"),
639
+ };
640
+ }
641
+ function validateDeadLetter(value) {
642
+ const item = object(value, "Safety dead-letter item is invalid.");
643
+ exactKeys(item, ["id", "action", "queuedAt", "reason", "deadLetteredAt"], "Safety dead-letter item");
644
+ return {
645
+ id: identifier(item.id, "Safety queue id"),
646
+ action: validateAction(item.action),
647
+ queuedAt: timestamp(item.queuedAt, "Safety queue time"),
648
+ reason: text(item.reason, "Safety dead-letter reason", 200),
649
+ deadLetteredAt: timestamp(item.deadLetteredAt, "Safety dead-letter time"),
650
+ };
651
+ }
652
+ function validateAudit(value) {
653
+ const record = object(value, "Safety audit record is invalid.");
654
+ exactKeys(record, [
655
+ "sequence",
656
+ "event",
657
+ "subject",
658
+ "reason",
659
+ "actorPrincipalId",
660
+ "timestamp",
661
+ "previousHash",
662
+ "recordHash",
663
+ ], "Safety audit record", ["actorPrincipalId"]);
664
+ const base = {
665
+ sequence: positive(record.sequence, "Safety audit sequence", MAX_AUDIT),
666
+ event: text(record.event, "Safety audit event", 80),
667
+ subject: text(record.subject, "Safety audit subject", 300),
668
+ reason: text(record.reason, "Safety audit reason", 200),
669
+ ...(record.actorPrincipalId === undefined
670
+ ? {}
671
+ : {
672
+ actorPrincipalId: identifier(record.actorPrincipalId, "Safety audit actor"),
673
+ }),
674
+ timestamp: timestamp(record.timestamp, "Safety audit time"),
675
+ previousHash: hash(record.previousHash, "Safety audit previous hash"),
676
+ };
677
+ if (hash(record.recordHash, "Safety audit record hash") !==
678
+ sha256(canonical(base)))
679
+ throw new SafetyControllerError("Safety audit record hash is invalid.");
680
+ return { ...base, recordHash: record.recordHash };
681
+ }
682
+ function validateAuditChain(anchor, audit) {
683
+ let previous = anchor;
684
+ let sequence = 1;
685
+ for (const record of audit) {
686
+ if (record.sequence !== sequence || record.previousHash !== previous)
687
+ throw new SafetyControllerError("Safety audit chain is invalid.");
688
+ previous = record.recordHash;
689
+ sequence += 1;
420
690
  }
421
- else
422
- remaining.push(item);
423
- } state.queue = remaining; }
424
- function addDeadLetter(state, item, reason, now) { if (state.deadLetters.length >= MAX_DEAD_LETTERS)
425
- state.deadLetters.shift(); state.deadLetters.push({ ...item, reason: text(reason, "Safety dead-letter reason", 200), deadLetteredAt: now }); }
426
- function pruneExpiredLeases(state, now, leaseTtlMs) { const cutoff = Date.parse(now) - leaseTtlMs; state.ledger = state.ledger.filter((entry) => entry.status === "completed" || Date.parse(entry.admittedAt) >= cutoff); }
427
- function findLedger(state, key) { const entry = state.ledger.find((candidate) => candidate.key === key); if (entry === undefined)
428
- throw new SafetyControllerError("Safety lease was not found."); return entry; }
429
- function scopeMatches(scope, action) { return scope.kind === "global" || scope.id === scopeValue(scope.kind, action); }
430
- function scopeValue(kind, action) { switch (kind) {
431
- case "organization": return action.organizationId;
432
- case "project": return action.projectId;
433
- case "environment": return action.environment;
434
- case "agent": return action.actorId;
435
- case "workload": return action.workloadId;
436
- case "provider": return action.provider;
437
- case "operation": return action.operation;
438
- case "risk_class": return action.riskClass;
439
- } }
440
- function scopeKey(scope) { return scope.kind === "global" ? "global" : `${scope.kind}:${scope.id}`; }
441
- function sameScope(left, right) { return left.kind === right.kind && left.id === right.id; }
442
- function ledgerKey(action) { return `${action.idempotencyKey}:${action.actionHash}`; }
443
- function sumCosts(values) { return values.reduce((total, value) => ({ monetaryAmountMinor: total.monetaryAmountMinor + value.monetaryAmountMinor, requests: total.requests + value.requests, messages: total.messages + value.messages, mutations: total.mutations + value.mutations, deletes: total.deletes + value.deletes, tokenCost: total.tokenCost + value.tokenCost }), { monetaryAmountMinor: 0, requests: 0, messages: 0, mutations: 0, deletes: 0, tokenCost: 0 }); }
444
- function appendAudit(state, event, subject, reason, timestampValue, actorPrincipalId) { if (state.audit.length >= MAX_AUDIT)
445
- throw new SafetyControllerError("Safety audit limit was reached."); const previousHash = state.audit.at(-1)?.recordHash ?? state.auditAnchor; const base = { sequence: state.audit.length + 1, event: text(event, "Safety audit event", 80), subject: text(subject, "Safety audit subject", 300), reason: text(reason, "Safety audit reason", 200), ...(actorPrincipalId === undefined ? {} : { actorPrincipalId: identifier(actorPrincipalId, "Safety audit actor") }), timestamp: timestampValue, previousHash }; state.audit.push({ ...base, recordHash: sha256(canonical(base)) }); }
446
- function boundedMs(value, label) { return positive(value, label, 30 * 24 * 60 * 60 * 1000); }
447
- function positive(value, label, max) { if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > max)
448
- throw new SafetyControllerError(`${label} is invalid.`); return value; }
449
- function nonNegative(value, label, max) { if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > max)
450
- throw new SafetyControllerError(`${label} is invalid.`); return value; }
451
- function bps(value, label) { return nonNegative(value, label, 10_000); }
452
- function rateBps(count, total) { return total === 0 ? 0 : Math.floor((count * 10_000) / total); }
453
- function identifier(value, label) { if (typeof value !== "string" || !IDENTIFIER.test(value) || sanitizeSecretString(value) !== value)
454
- throw new SafetyControllerError(`${label} must be a safe identifier.`); return value; }
455
- function hash(value, label) { if (typeof value !== "string" || !HASH.test(value))
456
- throw new SafetyControllerError(`${label} must be a SHA-256 hash.`); return value; }
457
- function timestamp(value, label) { if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) || !Number.isFinite(Date.parse(value)))
458
- throw new SafetyControllerError(`${label} must be an ISO UTC timestamp.`); return value; }
459
- function text(value, label, max) { if (typeof value !== "string" || value.trim() === "" || value.length > max || /[\u0000-\u001f]/.test(value) || sanitizeSecretString(value) !== value)
460
- throw new SafetyControllerError(`${label} is invalid.`); return value.trim(); }
461
- function object(value, message) { if (value === null || typeof value !== "object" || Array.isArray(value))
462
- throw new SafetyControllerError(message); return value; }
463
- function array(value, label, max) { if (!Array.isArray(value) || value.length > max)
464
- throw new SafetyControllerError(`${label} is invalid.`); return value; }
465
- function exactKeys(value, keys, label, optional = []) { for (const key of Object.keys(value))
466
- if (!keys.includes(key) || (value[key] === undefined && !optional.includes(key)))
467
- throw new SafetyControllerError(`${label} contains unsupported field: ${key}`); }
468
- function unique(values, label) { if (new Set(values).size !== values.length)
469
- throw new SafetyControllerError(`${label} must be unique.`); }
470
- function canonical(value) { if (value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number")
471
- return JSON.stringify(value); if (Array.isArray(value))
472
- return `[${value.map(canonical).join(",")}]`; if (typeof value === "object")
473
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`; throw new SafetyControllerError("Safety data must be JSON."); }
474
- function sha256(value) { return createHash("sha256").update(value, "utf8").digest("hex"); }
475
- function clone(value) { return structuredClone(value); }
476
- function isErrorCode(error, code) { return error instanceof Error && "code" in error && error.code === code; }
691
+ }
692
+ function setSwitch(state, scope, enabled, reason, principalId, now) {
693
+ const existing = state.switches.find((candidate) => sameScope(candidate.scope, scope));
694
+ if (existing === undefined) {
695
+ if (state.switches.length >= MAX_CONTROLS)
696
+ throw new SafetyControllerError("Safety switch limit was reached.");
697
+ state.switches.push({
698
+ scope,
699
+ enabled,
700
+ reason,
701
+ changedBy: principalId,
702
+ changedAt: now,
703
+ });
704
+ }
705
+ else {
706
+ existing.enabled = enabled;
707
+ existing.reason = reason;
708
+ existing.changedBy = principalId;
709
+ existing.changedAt = now;
710
+ }
711
+ }
712
+ function assertActionAllowed(state, action, now) {
713
+ const stopped = state.switches.find((control) => control.enabled && scopeMatches(control.scope, action));
714
+ if (stopped !== undefined)
715
+ throw new SafetyControllerError(`Kill switch is active for ${scopeKey(stopped.scope)}.`, KILL_SWITCH_ERROR_CODE);
716
+ const circuit = state.circuits.find((candidate) => candidate.state === "open" && scopeMatches(candidate.scope, action));
717
+ if (circuit !== undefined)
718
+ throw new SafetyControllerError(`Safety circuit breaker is open: ${circuit.id}.`);
719
+ for (const circuitCandidate of state.circuits)
720
+ if (circuitCandidate.state === "closed" &&
721
+ scopeMatches(circuitCandidate.scope, action))
722
+ evaluateCircuit(state, circuitCandidate, now);
723
+ const opened = state.circuits.find((candidate) => candidate.state === "open" && scopeMatches(candidate.scope, action));
724
+ if (opened !== undefined)
725
+ throw new SafetyControllerError(`Safety circuit breaker is open: ${opened.id}.`);
726
+ }
727
+ function assertBudgets(state, action, now) {
728
+ for (const budget of state.budgets) {
729
+ if (!scopeMatches(budget.scope, action))
730
+ continue;
731
+ const windowEntries = state.ledger.filter((entry) => scopeMatches(budget.scope, entry.action) &&
732
+ Date.parse(entry.admittedAt) >= Date.parse(now) - budget.windowMs);
733
+ const velocityEntries = state.ledger.filter((entry) => scopeMatches(budget.scope, entry.action) &&
734
+ Date.parse(entry.admittedAt) >=
735
+ Date.parse(now) - budget.velocityWindowMs);
736
+ const totals = sumCosts(windowEntries.map((entry) => entry.action.costs));
737
+ const velocity = sumCosts(velocityEntries.map((entry) => entry.action.costs)).requests;
738
+ const concurrent = windowEntries.filter((entry) => entry.status === "reserved").length;
739
+ for (const key of [
740
+ "monetaryAmountMinor",
741
+ "requests",
742
+ "messages",
743
+ "mutations",
744
+ "deletes",
745
+ "tokenCost",
746
+ ])
747
+ if (budget.limits[key] !== undefined &&
748
+ totals[key] + action.costs[key] > budget.limits[key])
749
+ throw new SafetyControllerError(`Safety budget exceeded: ${budget.id} ${key}.`);
750
+ if (budget.limits.concurrency !== undefined &&
751
+ concurrent + 1 > budget.limits.concurrency)
752
+ throw new SafetyControllerError(`Safety budget exceeded: ${budget.id} concurrency.`);
753
+ if (budget.limits.velocity !== undefined &&
754
+ velocity + action.costs.requests > budget.limits.velocity)
755
+ throw new SafetyControllerError(`Safety velocity limit exceeded: ${budget.id}.`);
756
+ }
757
+ }
758
+ function evaluateCircuits(state, action, now) {
759
+ for (const circuit of state.circuits)
760
+ if (circuit.state === "closed" && scopeMatches(circuit.scope, action))
761
+ evaluateCircuit(state, circuit, now);
762
+ }
763
+ function evaluateCircuit(state, circuit, now) {
764
+ const entries = state.ledger.filter((entry) => entry.status === "completed" &&
765
+ entry.completedAt !== undefined &&
766
+ entry.outcome !== undefined &&
767
+ scopeMatches(circuit.scope, entry.action) &&
768
+ Date.parse(entry.completedAt) >= Date.parse(now) - circuit.windowMs);
769
+ if (entries.length < circuit.minimumSamples)
770
+ return;
771
+ const failures = entries.filter((entry) => !entry.outcome.success).length;
772
+ const violations = entries.filter((entry) => entry.outcome.policyViolation).length;
773
+ const mismatches = entries.filter((entry) => entry.outcome.reconciliationMismatch).length;
774
+ const latencyExceeded = entries.some((entry) => entry.outcome.latencyMs > circuit.maxLatencyMs);
775
+ const reason = rateBps(failures, entries.length) > circuit.maxFailureRateBps
776
+ ? "failure rate exceeded"
777
+ : violations > circuit.maxPolicyViolations
778
+ ? "policy violation threshold exceeded"
779
+ : mismatches > circuit.maxReconciliationMismatches
780
+ ? "reconciliation mismatch threshold exceeded"
781
+ : latencyExceeded
782
+ ? "latency threshold exceeded"
783
+ : undefined;
784
+ if (reason !== undefined) {
785
+ circuit.state = "open";
786
+ circuit.openedAt = now;
787
+ circuit.reason = reason;
788
+ appendAudit(state, "circuit.opened", circuit.id, reason, now);
789
+ }
790
+ }
791
+ function deadLetterMatchingQueued(state, scope, reason, now) {
792
+ const remaining = [];
793
+ for (const item of state.queue) {
794
+ if (scopeMatches(scope, item.action)) {
795
+ addDeadLetter(state, item, `kill switch: ${reason}`, now);
796
+ appendAudit(state, "queue.dead_lettered", item.id, "kill switch stopped queued action", now);
797
+ }
798
+ else
799
+ remaining.push(item);
800
+ }
801
+ state.queue = remaining;
802
+ }
803
+ function addDeadLetter(state, item, reason, now) {
804
+ if (state.deadLetters.length >= MAX_DEAD_LETTERS)
805
+ state.deadLetters.shift();
806
+ state.deadLetters.push({
807
+ ...item,
808
+ reason: text(reason, "Safety dead-letter reason", 200),
809
+ deadLetteredAt: now,
810
+ });
811
+ }
812
+ function pruneExpiredLeases(state, now, leaseTtlMs) {
813
+ const cutoff = Date.parse(now) - leaseTtlMs;
814
+ state.ledger = state.ledger.filter((entry) => entry.status === "completed" || Date.parse(entry.admittedAt) >= cutoff);
815
+ }
816
+ function findLedger(state, key) {
817
+ const entry = state.ledger.find((candidate) => candidate.key === key);
818
+ if (entry === undefined)
819
+ throw new SafetyControllerError("Safety lease was not found.");
820
+ return entry;
821
+ }
822
+ function scopeMatches(scope, action) {
823
+ return scope.kind === "global" || scope.id === scopeValue(scope.kind, action);
824
+ }
825
+ function scopeValue(kind, action) {
826
+ switch (kind) {
827
+ case "organization":
828
+ return action.organizationId;
829
+ case "project":
830
+ return action.projectId;
831
+ case "environment":
832
+ return action.environment;
833
+ case "agent":
834
+ return action.actorId;
835
+ case "workload":
836
+ return action.workloadId;
837
+ case "provider":
838
+ return action.provider;
839
+ case "operation":
840
+ return action.operation;
841
+ case "risk_class":
842
+ return action.riskClass;
843
+ }
844
+ }
845
+ function scopeKey(scope) {
846
+ return scope.kind === "global" ? "global" : `${scope.kind}:${scope.id}`;
847
+ }
848
+ function sameScope(left, right) {
849
+ return left.kind === right.kind && left.id === right.id;
850
+ }
851
+ function ledgerKey(action) {
852
+ return `${action.idempotencyKey}:${action.actionHash}`;
853
+ }
854
+ function sumCosts(values) {
855
+ return values.reduce((total, value) => ({
856
+ monetaryAmountMinor: total.monetaryAmountMinor + value.monetaryAmountMinor,
857
+ requests: total.requests + value.requests,
858
+ messages: total.messages + value.messages,
859
+ mutations: total.mutations + value.mutations,
860
+ deletes: total.deletes + value.deletes,
861
+ tokenCost: total.tokenCost + value.tokenCost,
862
+ }), {
863
+ monetaryAmountMinor: 0,
864
+ requests: 0,
865
+ messages: 0,
866
+ mutations: 0,
867
+ deletes: 0,
868
+ tokenCost: 0,
869
+ });
870
+ }
871
+ function appendAudit(state, event, subject, reason, timestampValue, actorPrincipalId) {
872
+ if (state.audit.length >= MAX_AUDIT)
873
+ throw new SafetyControllerError("Safety audit limit was reached.");
874
+ const previousHash = state.audit.at(-1)?.recordHash ?? state.auditAnchor;
875
+ const base = {
876
+ sequence: state.audit.length + 1,
877
+ event: text(event, "Safety audit event", 80),
878
+ subject: text(subject, "Safety audit subject", 300),
879
+ reason: text(reason, "Safety audit reason", 200),
880
+ ...(actorPrincipalId === undefined
881
+ ? {}
882
+ : {
883
+ actorPrincipalId: identifier(actorPrincipalId, "Safety audit actor"),
884
+ }),
885
+ timestamp: timestampValue,
886
+ previousHash,
887
+ };
888
+ state.audit.push({ ...base, recordHash: sha256(canonical(base)) });
889
+ }
890
+ function boundedMs(value, label) {
891
+ return positive(value, label, 30 * 24 * 60 * 60 * 1000);
892
+ }
893
+ function positive(value, label, max) {
894
+ if (typeof value !== "number" ||
895
+ !Number.isInteger(value) ||
896
+ value < 1 ||
897
+ value > max)
898
+ throw new SafetyControllerError(`${label} is invalid.`);
899
+ return value;
900
+ }
901
+ function nonNegative(value, label, max) {
902
+ if (typeof value !== "number" ||
903
+ !Number.isInteger(value) ||
904
+ value < 0 ||
905
+ value > max)
906
+ throw new SafetyControllerError(`${label} is invalid.`);
907
+ return value;
908
+ }
909
+ function bps(value, label) {
910
+ return nonNegative(value, label, 10_000);
911
+ }
912
+ function rateBps(count, total) {
913
+ return total === 0 ? 0 : Math.floor((count * 10_000) / total);
914
+ }
915
+ function identifier(value, label) {
916
+ if (typeof value !== "string" ||
917
+ !IDENTIFIER.test(value) ||
918
+ sanitizeSecretString(value) !== value)
919
+ throw new SafetyControllerError(`${label} must be a safe identifier.`);
920
+ return value;
921
+ }
922
+ function hash(value, label) {
923
+ if (typeof value !== "string" || !HASH.test(value))
924
+ throw new SafetyControllerError(`${label} must be a SHA-256 hash.`);
925
+ return value;
926
+ }
927
+ function timestamp(value, label) {
928
+ if (typeof value !== "string" ||
929
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
930
+ !Number.isFinite(Date.parse(value)))
931
+ throw new SafetyControllerError(`${label} must be an ISO UTC timestamp.`);
932
+ return value;
933
+ }
934
+ function text(value, label, max) {
935
+ if (typeof value !== "string" ||
936
+ value.trim() === "" ||
937
+ value.length > max ||
938
+ /[\u0000-\u001f]/.test(value) ||
939
+ sanitizeSecretString(value) !== value)
940
+ throw new SafetyControllerError(`${label} is invalid.`);
941
+ return value.trim();
942
+ }
943
+ function object(value, message) {
944
+ if (value === null || typeof value !== "object" || Array.isArray(value))
945
+ throw new SafetyControllerError(message);
946
+ return value;
947
+ }
948
+ function array(value, label, max) {
949
+ if (!Array.isArray(value) || value.length > max)
950
+ throw new SafetyControllerError(`${label} is invalid.`);
951
+ return value;
952
+ }
953
+ function exactKeys(value, keys, label, optional = []) {
954
+ for (const key of Object.keys(value))
955
+ if (!keys.includes(key) ||
956
+ (value[key] === undefined && !optional.includes(key)))
957
+ throw new SafetyControllerError(`${label} contains unsupported field: ${key}`);
958
+ }
959
+ function unique(values, label) {
960
+ if (new Set(values).size !== values.length)
961
+ throw new SafetyControllerError(`${label} must be unique.`);
962
+ }
963
+ function canonical(value) {
964
+ if (value === null ||
965
+ typeof value === "boolean" ||
966
+ typeof value === "string" ||
967
+ typeof value === "number")
968
+ return JSON.stringify(value);
969
+ if (Array.isArray(value))
970
+ return `[${value.map(canonical).join(",")}]`;
971
+ if (typeof value === "object")
972
+ return `{${Object.keys(value)
973
+ .sort()
974
+ .map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
975
+ .join(",")}}`;
976
+ throw new SafetyControllerError("Safety data must be JSON.");
977
+ }
978
+ function sha256(value) {
979
+ return createHash("sha256").update(value, "utf8").digest("hex");
980
+ }
981
+ function clone(value) {
982
+ return structuredClone(value);
983
+ }
984
+ function isErrorCode(error, code) {
985
+ return error instanceof Error && "code" in error && error.code === code;
986
+ }
477
987
  //# sourceMappingURL=safetyController.js.map