@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
@@ -2,11 +2,11 @@ import { createHash } from "node:crypto";
2
2
  import { lstat, readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { getDataPaths } from "../config/dataPaths.js";
5
- import { actionApprovalHash, actionHash, validateStoredAction } from "../actions/index.js";
6
- import { createScenarioReplayer, prepareScenarioRecording, validateScenarioBundle, writeScenarioBundle } from "../scenarios/scenarioBundle.js";
5
+ import { actionApprovalHash, actionHash, validateStoredAction, } from "../actions/index.js";
6
+ import { createScenarioReplayer, prepareScenarioRecording, validateScenarioBundle, writeScenarioBundle, } from "../scenarios/scenarioBundle.js";
7
7
  import { sanitizeSecretString } from "../security/secrets.js";
8
- import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
9
- import { createWorld, inspectWorld, SyntheticWorldError } from "../worlds/index.js";
8
+ import { atomicWriteJson, ensurePrivateDirectory, withFileLock, } from "../storage/fileStore.js";
9
+ import { createWorld, inspectWorld, SyntheticWorldError, } from "../worlds/index.js";
10
10
  const LEDGER_SCHEMA_VERSION = 1;
11
11
  const LEDGER_KIND = "ghostapi.action-ledger";
12
12
  const MAX_LEDGER_BYTES = 2 * 1024 * 1024;
@@ -53,17 +53,39 @@ export class LocalActionLedger {
53
53
  argumentsHash: sha256(canonicalJson(record.envelope.arguments)),
54
54
  expectedEffectsHash: sha256(canonicalJson(record.envelope.expectedSideEffects)),
55
55
  riskClass: record.envelope.riskClass,
56
- reversibility: record.envelope.reversibility
56
+ reversibility: record.envelope.reversibility,
57
+ });
58
+ append("identity", {
59
+ actorId: record.envelope.actor.id,
60
+ workloadId: record.envelope.actor.workloadId,
61
+ actorType: record.envelope.actor.type,
62
+ });
63
+ append("policy_decision", {
64
+ policyVersion: record.envelope.policy.version,
65
+ policyHash: record.envelope.policy.hash,
66
+ allowed: true,
67
+ basis: "caller_claimed",
68
+ });
69
+ append("approval", {
70
+ approvalHash: actionApprovalHash(record.approval),
71
+ approverId: record.approval.approvedBy,
72
+ status: "approved",
73
+ basis: "caller_claimed",
57
74
  });
58
- append("identity", { actorId: record.envelope.actor.id, workloadId: record.envelope.actor.workloadId, actorType: record.envelope.actor.type });
59
- append("policy_decision", { policyVersion: record.envelope.policy.version, policyHash: record.envelope.policy.hash, allowed: true, basis: "caller_claimed" });
60
- append("approval", { approvalHash: actionApprovalHash(record.approval), approverId: record.approval.approvedBy, status: "approved", basis: "caller_claimed" });
61
75
  append("credential_grant", record.credentialGrant === undefined
62
76
  ? { status: "not_used" }
63
- : { status: "granted", grantIdHash: hash(record.credentialGrant.grantIdHash, "Credential grant reference"), credentialVersion: positiveInteger(record.credentialGrant.credentialVersion, "Credential version") });
77
+ : {
78
+ status: "granted",
79
+ grantIdHash: hash(record.credentialGrant.grantIdHash, "Credential grant reference"),
80
+ credentialVersion: positiveInteger(record.credentialGrant.credentialVersion, "Credential version"),
81
+ });
64
82
  for (const receipt of record.receipts)
65
83
  appendReceiptEvidence(append, receipt);
66
- append("compensation", { status: record.envelope.reversibility === "compensatable" ? "not_attempted" : "not_supported" });
84
+ append("compensation", {
85
+ status: record.envelope.reversibility === "compensatable"
86
+ ? "not_attempted"
87
+ : "not_supported",
88
+ });
67
89
  return { state, result: entries };
68
90
  });
69
91
  }
@@ -86,7 +108,9 @@ export class LocalActionLedger {
86
108
  tenantId: access.tenantId,
87
109
  exportedAt: this.now().toISOString(),
88
110
  integrity,
89
- entries: state.entries.filter((entry) => entry.tenantId === access.tenantId).map((entry) => structuredClone(entry))
111
+ entries: state.entries
112
+ .filter((entry) => entry.tenantId === access.tenantId)
113
+ .map((entry) => structuredClone(entry)),
90
114
  };
91
115
  }
92
116
  async verifyTenant(accessValue) {
@@ -95,12 +119,19 @@ export class LocalActionLedger {
95
119
  return verifyTenantState(await this.readState(), access.tenantId);
96
120
  }
97
121
  catch (error) {
98
- return { valid: false, error: error instanceof Error ? error.message : "Ledger state could not be validated." };
122
+ return {
123
+ valid: false,
124
+ error: error instanceof Error
125
+ ? error.message
126
+ : "Ledger state could not be validated.",
127
+ };
99
128
  }
100
129
  }
101
130
  async configureRetention(accessValue, retentionDaysValue) {
102
131
  const access = this.authorize(accessValue, "manage_retention");
103
- const retentionDays = retentionDaysValue === null ? null : boundedInteger(retentionDaysValue, "Retention days", 1, 3_650);
132
+ const retentionDays = retentionDaysValue === null
133
+ ? null
134
+ : boundedInteger(retentionDaysValue, "Retention days", 1, 3_650);
104
135
  return this.mutate(async (state) => {
105
136
  const tenant = getOrCreateTenant(state, access.tenantId);
106
137
  tenant.retentionDays = retentionDays;
@@ -143,17 +174,36 @@ export class LocalActionLedger {
143
174
  const intent = timeline.find((entry) => entry.stage === "intent");
144
175
  if (intent === undefined)
145
176
  throw new ActionLedgerError("Incident replay requires an action intent in the tenant-scoped ledger timeline.");
146
- const verification = timeline.filter((entry) => entry.stage === "verification").at(-1);
147
- const outcome = verification?.data.status === "verified" ? "verified" : "requires_reconciliation";
177
+ const verification = timeline
178
+ .filter((entry) => entry.stage === "verification")
179
+ .at(-1);
180
+ const outcome = verification?.data.status === "verified"
181
+ ? "verified"
182
+ : "requires_reconciliation";
148
183
  const actionDigest = intent.actionHash;
149
184
  const runId = deterministicUuid(actionDigest);
150
185
  const capture = {
151
- interactions: [{
152
- request: { method: "POST", url: `https://incident.sandbox.localhost/incidents/${actionId}/runs/${runId}`, headers: { "content-type": "application/json" }, body: { action: actionId, actionHash: actionDigest } },
153
- response: { status: outcome === "verified" ? 200 : 409, headers: { "content-type": "application/json" }, body: { outcome, action: actionId } }
154
- }]
186
+ interactions: [
187
+ {
188
+ request: {
189
+ method: "POST",
190
+ url: `https://incident.sandbox.localhost/incidents/${actionId}/runs/${runId}`,
191
+ headers: { "content-type": "application/json" },
192
+ body: { action: actionId, actionHash: actionDigest },
193
+ },
194
+ response: {
195
+ status: outcome === "verified" ? 200 : 409,
196
+ headers: { "content-type": "application/json" },
197
+ body: { outcome, action: actionId },
198
+ },
199
+ },
200
+ ],
155
201
  };
156
- const bundle = prepareScenarioRecording(capture, { title: `Incident ${actionId}`, allowedSandboxHosts: ["incident.sandbox.localhost"], recordedAt: this.now().toISOString() });
202
+ const bundle = prepareScenarioRecording(capture, {
203
+ title: `Incident ${actionId}`,
204
+ allowedSandboxHosts: ["incident.sandbox.localhost"],
205
+ recordedAt: this.now().toISOString(),
206
+ });
157
207
  const worldId = `incident-${actionDigest.slice(0, 16)}`;
158
208
  const seed = `incident-${actionDigest.slice(0, 32)}`;
159
209
  try {
@@ -162,14 +212,24 @@ export class LocalActionLedger {
162
212
  throw new ActionLedgerError("Incident synthetic world id collides with a different deterministic seed.");
163
213
  }
164
214
  catch (error) {
165
- if (error instanceof SyntheticWorldError && error.code === "WORLD_NOT_FOUND")
166
- await createWorld({ id: worldId, seed, title: `Incident replay ${actionId}` });
215
+ if (error instanceof SyntheticWorldError &&
216
+ error.code === "WORLD_NOT_FOUND")
217
+ await createWorld({
218
+ id: worldId,
219
+ seed,
220
+ title: `Incident replay ${actionId}`,
221
+ });
167
222
  else if (error instanceof ActionLedgerError)
168
223
  throw error;
169
224
  else
170
225
  throw error;
171
226
  }
172
- const request = { method: "POST", path: `/incidents/${actionId}/runs/${runId}`, headers: { "content-type": "application/json" }, body: { action: actionId, actionHash: actionDigest } };
227
+ const request = {
228
+ method: "POST",
229
+ path: `/incidents/${actionId}/runs/${runId}`,
230
+ headers: { "content-type": "application/json" },
231
+ body: { action: actionId, actionHash: actionDigest },
232
+ };
173
233
  const fixture = {
174
234
  schemaVersion: 1,
175
235
  kind: "ghostapi.incident-fixture",
@@ -180,7 +240,7 @@ export class LocalActionLedger {
180
240
  syntheticWorld: { id: worldId, version: "1.0.0", seed },
181
241
  bundle,
182
242
  request,
183
- expected: { status: outcome === "verified" ? 200 : 409, outcome }
243
+ expected: { status: outcome === "verified" ? 200 : 409, outcome },
184
244
  };
185
245
  await ensurePrivateDirectory(this.incidentsPath);
186
246
  const prefix = `incident-${actionDigest.slice(0, 16)}`;
@@ -197,7 +257,9 @@ export class LocalActionLedger {
197
257
  const replay = createScenarioReplayer(fixture.bundle);
198
258
  const result = replay.replay(fixture.request);
199
259
  const outcome = readOutcome(result.body);
200
- if (result.status !== fixture.expected.status || outcome !== fixture.expected.outcome || replay.remaining !== 0)
260
+ if (result.status !== fixture.expected.status ||
261
+ outcome !== fixture.expected.outcome ||
262
+ replay.remaining !== 0)
201
263
  throw new ActionLedgerError("Incident fixture did not reproduce its expected local outcome.");
202
264
  return { status: result.status, outcome, remaining: replay.remaining };
203
265
  }
@@ -205,7 +267,11 @@ export class LocalActionLedger {
205
267
  const authorized = this.accessAuthorizer(access, permission, accessTenantId(access));
206
268
  if (!authorized.permissions.includes(permission))
207
269
  throw new ActionLedgerError("Ledger access authorizer returned an insufficient permission set.");
208
- return { tenantId: identifier(authorized.tenantId, "Ledger tenant id"), principalId: identifier(authorized.principalId, "Ledger principal id"), permissions: [...authorized.permissions] };
270
+ return {
271
+ tenantId: identifier(authorized.tenantId, "Ledger tenant id"),
272
+ principalId: identifier(authorized.principalId, "Ledger principal id"),
273
+ permissions: [...authorized.permissions],
274
+ };
209
275
  }
210
276
  async readState() {
211
277
  await ensurePrivateDirectory(getDataPaths().root);
@@ -243,7 +309,11 @@ export function createTestLedgerAccessAuthorizer() {
243
309
  const granted = new WeakMap();
244
310
  return {
245
311
  issue(input) {
246
- const access = { tenantId: identifier(input.tenantId, "Ledger tenant id"), principalId: identifier(input.principalId, "Ledger principal id"), permissions: normalizePermissions(input.permissions) };
312
+ const access = {
313
+ tenantId: identifier(input.tenantId, "Ledger tenant id"),
314
+ principalId: identifier(input.principalId, "Ledger principal id"),
315
+ permissions: normalizePermissions(input.permissions),
316
+ };
247
317
  const capability = Object.freeze({ tenantId: access.tenantId });
248
318
  granted.set(capability, access);
249
319
  return capability;
@@ -252,16 +322,30 @@ export function createTestLedgerAccessAuthorizer() {
252
322
  if (access === null || typeof access !== "object")
253
323
  throw new ActionLedgerError("Ledger access is not a verified capability.");
254
324
  const grantedAccess = granted.get(access);
255
- if (grantedAccess === undefined || grantedAccess.tenantId !== tenantId || !grantedAccess.permissions.includes(permission))
325
+ if (grantedAccess === undefined ||
326
+ grantedAccess.tenantId !== tenantId ||
327
+ !grantedAccess.permissions.includes(permission))
256
328
  throw new ActionLedgerError("Ledger access is unauthorized.");
257
329
  return grantedAccess;
258
- }
330
+ },
259
331
  };
260
332
  }
261
333
  export function validateIncidentFixture(value) {
262
334
  const fixture = object(value, "Incident fixture must be an object.");
263
- exactKeys(fixture, ["schemaVersion", "kind", "tenantId", "actionId", "actionHash", "sourceLedgerHeadHash", "syntheticWorld", "bundle", "request", "expected"], "Incident fixture");
264
- if (fixture.schemaVersion !== 1 || fixture.kind !== "ghostapi.incident-fixture")
335
+ exactKeys(fixture, [
336
+ "schemaVersion",
337
+ "kind",
338
+ "tenantId",
339
+ "actionId",
340
+ "actionHash",
341
+ "sourceLedgerHeadHash",
342
+ "syntheticWorld",
343
+ "bundle",
344
+ "request",
345
+ "expected",
346
+ ], "Incident fixture");
347
+ if (fixture.schemaVersion !== 1 ||
348
+ fixture.kind !== "ghostapi.incident-fixture")
265
349
  throw new ActionLedgerError("Unsupported incident fixture schema.");
266
350
  const world = object(fixture.syntheticWorld, "Incident fixture world is invalid.");
267
351
  exactKeys(world, ["id", "version", "seed"], "Incident fixture world");
@@ -269,7 +353,9 @@ export function validateIncidentFixture(value) {
269
353
  throw new ActionLedgerError("Incident fixture world version is invalid.");
270
354
  const expected = object(fixture.expected, "Incident fixture expected outcome is invalid.");
271
355
  exactKeys(expected, ["status", "outcome"], "Incident fixture expected outcome");
272
- if ((expected.status !== 200 && expected.status !== 409) || (expected.outcome !== "verified" && expected.outcome !== "requires_reconciliation"))
356
+ if ((expected.status !== 200 && expected.status !== 409) ||
357
+ (expected.outcome !== "verified" &&
358
+ expected.outcome !== "requires_reconciliation"))
273
359
  throw new ActionLedgerError("Incident fixture expected outcome is invalid.");
274
360
  const request = object(fixture.request, "Incident fixture request is invalid.");
275
361
  return {
@@ -279,46 +365,102 @@ export function validateIncidentFixture(value) {
279
365
  actionId: identifier(fixture.actionId, "Incident fixture action id"),
280
366
  actionHash: hash(fixture.actionHash, "Incident fixture action hash"),
281
367
  sourceLedgerHeadHash: hash(fixture.sourceLedgerHeadHash, "Incident fixture ledger head hash"),
282
- syntheticWorld: { id: identifier(world.id, "Incident fixture world id"), version: "1.0.0", seed: safeString(world.seed, "Incident fixture world seed", 64) },
368
+ syntheticWorld: {
369
+ id: identifier(world.id, "Incident fixture world id"),
370
+ version: "1.0.0",
371
+ seed: safeString(world.seed, "Incident fixture world seed", 64),
372
+ },
283
373
  bundle: prepareBundle(fixture.bundle),
284
- request: { method: safeString(request.method, "Incident fixture request method", 10), path: safeString(request.path, "Incident fixture request path", 8_000), headers: normalizeFixtureHeaders(request.headers), body: sanitizeStructured(request.body, "Incident fixture request body") },
285
- expected: { status: expected.status, outcome: expected.outcome }
374
+ request: {
375
+ method: safeString(request.method, "Incident fixture request method", 10),
376
+ path: safeString(request.path, "Incident fixture request path", 8_000),
377
+ headers: normalizeFixtureHeaders(request.headers),
378
+ body: sanitizeStructured(request.body, "Incident fixture request body"),
379
+ },
380
+ expected: { status: expected.status, outcome: expected.outcome },
286
381
  };
287
382
  }
288
383
  function prepareBundle(value) {
289
384
  // The scenario module performs strict data-only validation; no replay transport is created here.
290
385
  const bundle = validateScenarioBundle(value);
291
- if (bundle.interactions.length !== 1 || bundle.metadata.sandboxHosts.length !== 1 || bundle.metadata.sandboxHosts[0] !== "incident.sandbox.localhost")
386
+ if (bundle.interactions.length !== 1 ||
387
+ bundle.metadata.sandboxHosts.length !== 1 ||
388
+ bundle.metadata.sandboxHosts[0] !== "incident.sandbox.localhost")
292
389
  throw new ActionLedgerError("Incident fixture must contain one local-only replay interaction.");
293
390
  return bundle;
294
391
  }
295
392
  function appendReceiptEvidence(append, receipt) {
296
393
  const status = safeString(receipt.status, "Action receipt status", 32);
297
394
  if (status === "attempted")
298
- append("execution_attempt", { attempt: 1, status: "attempted", receiptHash: hash(receipt.receiptHash, "Action receipt hash") });
395
+ append("execution_attempt", {
396
+ attempt: 1,
397
+ status: "attempted",
398
+ receiptHash: hash(receipt.receiptHash, "Action receipt hash"),
399
+ });
299
400
  if (status === "committed")
300
- append("provider_receipt", { outcome: "committed", receiptHash: hash(receipt.receiptHash, "Action receipt hash"), providerReceiptHash: receipt.providerRequestId === undefined ? null : sha256(receipt.providerRequestId) });
401
+ append("provider_receipt", {
402
+ outcome: "committed",
403
+ receiptHash: hash(receipt.receiptHash, "Action receipt hash"),
404
+ providerReceiptHash: receipt.providerRequestId === undefined
405
+ ? null
406
+ : sha256(receipt.providerRequestId),
407
+ });
301
408
  if (status === "verified")
302
- append("verification", { status: "verified", receiptHash: hash(receipt.receiptHash, "Action receipt hash"), proofHash: sha256(canonicalJson(receipt.result ?? null)) });
409
+ append("verification", {
410
+ status: "verified",
411
+ receiptHash: hash(receipt.receiptHash, "Action receipt hash"),
412
+ proofHash: sha256(canonicalJson(receipt.result ?? null)),
413
+ });
303
414
  if (status === "failed") {
304
415
  const ambiguous = receipt.failure?.code === "unknown_outcome";
305
- append("execution_attempt", { attempt: 1, status: ambiguous ? "ambiguous" : "failed", receiptHash: hash(receipt.receiptHash, "Action receipt hash") });
306
- append("verification", { status: ambiguous ? "ambiguous" : "failed", receiptHash: hash(receipt.receiptHash, "Action receipt hash"), proofHash: null });
416
+ append("execution_attempt", {
417
+ attempt: 1,
418
+ status: ambiguous ? "ambiguous" : "failed",
419
+ receiptHash: hash(receipt.receiptHash, "Action receipt hash"),
420
+ });
421
+ append("verification", {
422
+ status: ambiguous ? "ambiguous" : "failed",
423
+ receiptHash: hash(receipt.receiptHash, "Action receipt hash"),
424
+ proofHash: null,
425
+ });
307
426
  }
308
427
  }
309
428
  function normalizeActionRecord(value) {
310
- const stored = validateStoredAction({ schemaVersion: 1, kind: "ghostapi.action-record", envelope: value.envelope, actionHash: actionHash(value.envelope), approval: value.approval, receipts: value.receipts });
311
- return { envelope: stored.envelope, approval: stored.approval, receipts: stored.receipts, ...(value.credentialGrant === undefined ? {} : { credentialGrant: value.credentialGrant }) };
429
+ const stored = validateStoredAction({
430
+ schemaVersion: 1,
431
+ kind: "ghostapi.action-record",
432
+ envelope: value.envelope,
433
+ actionHash: actionHash(value.envelope),
434
+ approval: value.approval,
435
+ receipts: value.receipts,
436
+ });
437
+ return {
438
+ envelope: stored.envelope,
439
+ approval: stored.approval,
440
+ receipts: stored.receipts,
441
+ ...(value.credentialGrant === undefined
442
+ ? {}
443
+ : { credentialGrant: value.credentialGrant }),
444
+ };
312
445
  }
313
446
  function emptyState() {
314
- return { schemaVersion: 1, kind: "ghostapi.action-ledger", entries: [], tenants: [] };
447
+ return {
448
+ schemaVersion: 1,
449
+ kind: "ghostapi.action-ledger",
450
+ entries: [],
451
+ tenants: [],
452
+ };
315
453
  }
316
454
  function validateState(value) {
317
455
  const state = object(value, "Action ledger state must be an object.");
318
456
  exactKeys(state, ["schemaVersion", "kind", "entries", "tenants"], "Action ledger state");
319
- if (state.schemaVersion !== LEDGER_SCHEMA_VERSION || state.kind !== LEDGER_KIND)
457
+ if (state.schemaVersion !== LEDGER_SCHEMA_VERSION ||
458
+ state.kind !== LEDGER_KIND)
320
459
  throw new ActionLedgerError("Unsupported action ledger schema.");
321
- if (!Array.isArray(state.entries) || state.entries.length > MAX_ENTRIES || !Array.isArray(state.tenants) || state.tenants.length > MAX_TENANTS)
460
+ if (!Array.isArray(state.entries) ||
461
+ state.entries.length > MAX_ENTRIES ||
462
+ !Array.isArray(state.tenants) ||
463
+ state.tenants.length > MAX_TENANTS)
322
464
  throw new ActionLedgerError("Action ledger state exceeds its bounded limits.");
323
465
  const entries = state.entries.map(validateEntry);
324
466
  const tenants = state.tenants.map(validateTenant);
@@ -326,14 +468,29 @@ function validateState(value) {
326
468
  throw new ActionLedgerError("Action ledger tenant state is duplicated.");
327
469
  for (const tenant of tenants)
328
470
  assertTenantIntegrity({ schemaVersion: 1, kind: "ghostapi.action-ledger", entries, tenants }, tenant.tenantId);
329
- if (new Set(entries.map((entry) => entry.tenantId)).size !== tenants.length && entries.some((entry) => !tenants.some((tenant) => tenant.tenantId === entry.tenantId)))
471
+ if (new Set(entries.map((entry) => entry.tenantId)).size !== tenants.length &&
472
+ entries.some((entry) => !tenants.some((tenant) => tenant.tenantId === entry.tenantId)))
330
473
  throw new ActionLedgerError("Action ledger entry has no tenant state.");
331
474
  return { schemaVersion: 1, kind: "ghostapi.action-ledger", entries, tenants };
332
475
  }
333
476
  function validateEntry(value) {
334
477
  const entry = object(value, "Ledger entry must be an object.");
335
- exactKeys(entry, ["schemaVersion", "kind", "tenantId", "sequence", "timestamp", "actionId", "actionHash", "stage", "data", "previousHash", "entryHash"], "Ledger entry");
336
- if (entry.schemaVersion !== 1 || entry.kind !== "ghostapi.action-ledger-entry" || !isStage(entry.stage))
478
+ exactKeys(entry, [
479
+ "schemaVersion",
480
+ "kind",
481
+ "tenantId",
482
+ "sequence",
483
+ "timestamp",
484
+ "actionId",
485
+ "actionHash",
486
+ "stage",
487
+ "data",
488
+ "previousHash",
489
+ "entryHash",
490
+ ], "Ledger entry");
491
+ if (entry.schemaVersion !== 1 ||
492
+ entry.kind !== "ghostapi.action-ledger-entry" ||
493
+ !isStage(entry.stage))
337
494
  throw new ActionLedgerError("Ledger entry schema is invalid.");
338
495
  const normalized = {
339
496
  schemaVersion: 1,
@@ -345,7 +502,7 @@ function validateEntry(value) {
345
502
  actionHash: hash(entry.actionHash, "Ledger action hash"),
346
503
  stage: entry.stage,
347
504
  data: normalizeData(entry.data),
348
- previousHash: hash(entry.previousHash, "Ledger previous hash")
505
+ previousHash: hash(entry.previousHash, "Ledger previous hash"),
349
506
  };
350
507
  const entryHash = hash(entry.entryHash, "Ledger entry hash");
351
508
  if (entryHash !== sha256(canonicalJson(normalized)))
@@ -354,11 +511,32 @@ function validateEntry(value) {
354
511
  }
355
512
  function validateTenant(value) {
356
513
  const tenant = object(value, "Ledger tenant state is invalid.");
357
- exactKeys(tenant, ["tenantId", "entryCount", "headHash", "retentionDays", "legalHold", "deletionRequestedAt"], "Ledger tenant state");
358
- const deletionRequestedAt = tenant.deletionRequestedAt === undefined ? undefined : timestamp(tenant.deletionRequestedAt, "Ledger deletion request timestamp");
359
- if (tenant.retentionDays !== null && (!Number.isInteger(tenant.retentionDays) || typeof tenant.retentionDays !== "number" || tenant.retentionDays < 1 || tenant.retentionDays > 3_650) || typeof tenant.legalHold !== "boolean")
514
+ exactKeys(tenant, [
515
+ "tenantId",
516
+ "entryCount",
517
+ "headHash",
518
+ "retentionDays",
519
+ "legalHold",
520
+ "deletionRequestedAt",
521
+ ], "Ledger tenant state");
522
+ const deletionRequestedAt = tenant.deletionRequestedAt === undefined
523
+ ? undefined
524
+ : timestamp(tenant.deletionRequestedAt, "Ledger deletion request timestamp");
525
+ if ((tenant.retentionDays !== null &&
526
+ (!Number.isInteger(tenant.retentionDays) ||
527
+ typeof tenant.retentionDays !== "number" ||
528
+ tenant.retentionDays < 1 ||
529
+ tenant.retentionDays > 3_650)) ||
530
+ typeof tenant.legalHold !== "boolean")
360
531
  throw new ActionLedgerError("Ledger tenant retention state is invalid.");
361
- return { tenantId: identifier(tenant.tenantId, "Ledger tenant id"), entryCount: nonNegativeInteger(tenant.entryCount, "Ledger tenant entry count"), headHash: hash(tenant.headHash, "Ledger tenant head hash"), retentionDays: tenant.retentionDays, legalHold: tenant.legalHold, ...(deletionRequestedAt === undefined ? {} : { deletionRequestedAt }) };
532
+ return {
533
+ tenantId: identifier(tenant.tenantId, "Ledger tenant id"),
534
+ entryCount: nonNegativeInteger(tenant.entryCount, "Ledger tenant entry count"),
535
+ headHash: hash(tenant.headHash, "Ledger tenant head hash"),
536
+ retentionDays: tenant.retentionDays,
537
+ legalHold: tenant.legalHold,
538
+ ...(deletionRequestedAt === undefined ? {} : { deletionRequestedAt }),
539
+ };
362
540
  }
363
541
  function appendEntry(state, tenantId, actionId, actionDigest, stage, data, timestampValue) {
364
542
  const tenant = getOrCreateTenant(state, tenantId);
@@ -377,7 +555,7 @@ function appendEntry(state, tenantId, actionId, actionDigest, stage, data, times
377
555
  actionHash: hash(actionDigest, "Ledger action hash"),
378
556
  stage,
379
557
  data: normalizeData(data),
380
- previousHash: tenant.entryCount === 0 ? genesisHash(tenantId) : tenant.headHash
558
+ previousHash: tenant.entryCount === 0 ? genesisHash(tenantId) : tenant.headHash,
381
559
  };
382
560
  const appended = { ...entry, entryHash: sha256(canonicalJson(entry)) };
383
561
  state.entries.push(appended);
@@ -394,10 +572,13 @@ function rotateForRetention(state, now) {
394
572
  .filter((entry) => entry.tenantId === tenant.tenantId)
395
573
  .sort((left, right) => left.sequence - right.sequence)
396
574
  .filter((entry) => Date.parse(entry.timestamp) >= cutoff);
397
- if (surviving.length === state.entries.filter((entry) => entry.tenantId === tenant.tenantId).length)
575
+ if (surviving.length ===
576
+ state.entries.filter((entry) => entry.tenantId === tenant.tenantId).length)
398
577
  continue;
399
578
  const rebuilt = relinkTenantChain(tenant.tenantId, surviving);
400
- state.entries = state.entries.filter((entry) => entry.tenantId !== tenant.tenantId).concat(rebuilt);
579
+ state.entries = state.entries
580
+ .filter((entry) => entry.tenantId !== tenant.tenantId)
581
+ .concat(rebuilt);
401
582
  tenant.entryCount = rebuilt.length;
402
583
  tenant.headHash = rebuilt.at(-1)?.entryHash ?? genesisHash(tenant.tenantId);
403
584
  }
@@ -405,7 +586,18 @@ function rotateForRetention(state, now) {
405
586
  function relinkTenantChain(tenantId, surviving) {
406
587
  let previousHash = genesisHash(tenantId);
407
588
  return surviving.map((entry, index) => {
408
- const base = { schemaVersion: 1, kind: "ghostapi.action-ledger-entry", tenantId, sequence: index + 1, timestamp: entry.timestamp, actionId: entry.actionId, actionHash: entry.actionHash, stage: entry.stage, data: entry.data, previousHash };
589
+ const base = {
590
+ schemaVersion: 1,
591
+ kind: "ghostapi.action-ledger-entry",
592
+ tenantId,
593
+ sequence: index + 1,
594
+ timestamp: entry.timestamp,
595
+ actionId: entry.actionId,
596
+ actionHash: entry.actionHash,
597
+ stage: entry.stage,
598
+ data: entry.data,
599
+ previousHash,
600
+ };
409
601
  const entryHash = sha256(canonicalJson(base));
410
602
  previousHash = entryHash;
411
603
  return { ...base, entryHash };
@@ -417,7 +609,13 @@ function getOrCreateTenant(state, tenantId) {
417
609
  return existing;
418
610
  if (state.tenants.length >= MAX_TENANTS)
419
611
  throw new ActionLedgerError(`Action ledger tenant limit of ${MAX_TENANTS} was reached.`);
420
- const tenant = { tenantId, entryCount: 0, headHash: genesisHash(tenantId), retentionDays: null, legalHold: false };
612
+ const tenant = {
613
+ tenantId,
614
+ entryCount: 0,
615
+ headHash: genesisHash(tenantId),
616
+ retentionDays: null,
617
+ legalHold: false,
618
+ };
421
619
  state.tenants.push(tenant);
422
620
  return tenant;
423
621
  }
@@ -429,26 +627,58 @@ function assertTenantIntegrity(state, tenantId) {
429
627
  function verifyTenantState(state, tenantId) {
430
628
  const tenant = state.tenants.find((candidate) => candidate.tenantId === tenantId);
431
629
  if (tenant === undefined)
432
- return { valid: true, entryCount: 0, headHash: genesisHash(tenantId), tracked: false };
630
+ return {
631
+ valid: true,
632
+ entryCount: 0,
633
+ headHash: genesisHash(tenantId),
634
+ tracked: false,
635
+ };
433
636
  const entries = state.entries.filter((entry) => entry.tenantId === tenantId);
434
637
  if (entries.length !== tenant.entryCount)
435
- return { valid: false, error: "Ledger tenant entry count does not match the append-only record." };
638
+ return {
639
+ valid: false,
640
+ error: "Ledger tenant entry count does not match the append-only record.",
641
+ };
436
642
  let previousHash = genesisHash(tenantId);
437
643
  for (let index = 0; index < entries.length; index += 1) {
438
644
  const entry = entries[index];
439
645
  if (entry.sequence !== index + 1 || entry.previousHash !== previousHash)
440
- return { valid: false, error: "Ledger entry ordering or chain link is corrupted." };
441
- const expectedHash = sha256(canonicalJson({ schemaVersion: entry.schemaVersion, kind: entry.kind, tenantId: entry.tenantId, sequence: entry.sequence, timestamp: entry.timestamp, actionId: entry.actionId, actionHash: entry.actionHash, stage: entry.stage, data: entry.data, previousHash: entry.previousHash }));
646
+ return {
647
+ valid: false,
648
+ error: "Ledger entry ordering or chain link is corrupted.",
649
+ };
650
+ const expectedHash = sha256(canonicalJson({
651
+ schemaVersion: entry.schemaVersion,
652
+ kind: entry.kind,
653
+ tenantId: entry.tenantId,
654
+ sequence: entry.sequence,
655
+ timestamp: entry.timestamp,
656
+ actionId: entry.actionId,
657
+ actionHash: entry.actionHash,
658
+ stage: entry.stage,
659
+ data: entry.data,
660
+ previousHash: entry.previousHash,
661
+ }));
442
662
  if (entry.entryHash !== expectedHash)
443
663
  return { valid: false, error: "Ledger entry content hash is corrupted." };
444
664
  previousHash = entry.entryHash;
445
665
  }
446
666
  if (tenant.headHash !== previousHash)
447
- return { valid: false, error: "Ledger tenant head hash does not match the append-only record." };
448
- return { valid: true, entryCount: entries.length, headHash: tenant.headHash, tracked: true };
667
+ return {
668
+ valid: false,
669
+ error: "Ledger tenant head hash does not match the append-only record.",
670
+ };
671
+ return {
672
+ valid: true,
673
+ entryCount: entries.length,
674
+ headHash: tenant.headHash,
675
+ tracked: true,
676
+ };
449
677
  }
450
678
  function entriesForAction(state, tenantId, actionId) {
451
- return state.entries.filter((entry) => entry.tenantId === tenantId && entry.actionId === actionId).map((entry) => structuredClone(entry));
679
+ return state.entries
680
+ .filter((entry) => entry.tenantId === tenantId && entry.actionId === actionId)
681
+ .map((entry) => structuredClone(entry));
452
682
  }
453
683
  function normalizeData(value) {
454
684
  const data = object(value, "Ledger structured data must be an object.");
@@ -457,7 +687,8 @@ function normalizeData(value) {
457
687
  throw new ActionLedgerError("Ledger structured data has too many fields.");
458
688
  const normalized = {};
459
689
  for (const [key, entry] of entries) {
460
- if (!/^[a-z][a-zA-Z0-9]{0,63}$/.test(key) || /(?:authorization|cookie|token|secret|password|email|phone|address|card|payload|body)/i.test(key))
690
+ if (!/^[a-z][a-zA-Z0-9]{0,63}$/.test(key) ||
691
+ /(?:authorization|cookie|token|secret|password|email|phone|address|card|payload|body)/i.test(key))
461
692
  throw new ActionLedgerError(`Ledger structured field is forbidden: ${key}`);
462
693
  if (typeof entry === "string")
463
694
  normalized[key] = safeString(entry, `Ledger structured field ${key}`, 512);
@@ -488,7 +719,8 @@ function sanitizeStructured(value, label) {
488
719
  if (Buffer.byteLength(serialized, "utf8") > 64 * 1024)
489
720
  throw new ActionLedgerError(`${label} exceeds 65536 bytes.`);
490
721
  const sanitized = sanitizeSecretString(serialized);
491
- if (sanitized !== serialized || /(?:@|\b\d{3}[ .-]\d{3}[ .-]\d{4}\b)/.test(serialized))
722
+ if (sanitized !== serialized ||
723
+ /(?:@|\b\d{3}[ .-]\d{3}[ .-]\d{4}\b)/.test(serialized))
492
724
  throw new ActionLedgerError(`${label} contains a secret-shaped value or prohibited PII.`);
493
725
  return structuredClone(value);
494
726
  }
@@ -497,7 +729,9 @@ function accessTenantId(value) {
497
729
  throw new ActionLedgerError("Ledger access is not a verified capability.");
498
730
  const tenantId = value.tenantId;
499
731
  // Test capabilities deliberately expose no mutable fields, so authorizers receive an opaque scope hint instead.
500
- return typeof tenantId === "string" ? identifier(tenantId, "Ledger tenant id") : "opaque";
732
+ return typeof tenantId === "string"
733
+ ? identifier(tenantId, "Ledger tenant id")
734
+ : "opaque";
501
735
  }
502
736
  function denyLedgerAccess() {
503
737
  throw new ActionLedgerError("Ledger access is disabled until a verified tenant access authorizer is configured.");
@@ -506,18 +740,48 @@ function governanceActionId(kind, state, tenantId) {
506
740
  return `governance-${kind}-${tenantId}-${state.entries.filter((entry) => entry.tenantId === tenantId).length + 1}`.slice(0, 128);
507
741
  }
508
742
  function deterministicUuid(digest) {
509
- const hex = digest.slice(0, 12) + "4" + digest.slice(13, 16) + "8" + digest.slice(17, 32);
743
+ const hex = digest.slice(0, 12) +
744
+ "4" +
745
+ digest.slice(13, 16) +
746
+ "8" +
747
+ digest.slice(17, 32);
510
748
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
511
749
  }
512
750
  function readOutcome(value) {
513
751
  const body = object(value, "Incident replay response body is invalid.");
514
- return body.outcome === "verified" || body.outcome === "requires_reconciliation" ? body.outcome : (() => { throw new ActionLedgerError("Incident replay response outcome is invalid."); })();
752
+ return body.outcome === "verified" ||
753
+ body.outcome === "requires_reconciliation"
754
+ ? body.outcome
755
+ : (() => {
756
+ throw new ActionLedgerError("Incident replay response outcome is invalid.");
757
+ })();
515
758
  }
516
759
  function isStage(value) {
517
- return value === "intent" || value === "identity" || value === "policy_decision" || value === "approval" || value === "credential_grant" || value === "execution_attempt" || value === "provider_receipt" || value === "verification" || value === "compensation" || value === "retention" || value === "legal_hold" || value === "deletion_request";
760
+ return (value === "intent" ||
761
+ value === "identity" ||
762
+ value === "policy_decision" ||
763
+ value === "approval" ||
764
+ value === "credential_grant" ||
765
+ value === "execution_attempt" ||
766
+ value === "provider_receipt" ||
767
+ value === "verification" ||
768
+ value === "compensation" ||
769
+ value === "retention" ||
770
+ value === "legal_hold" ||
771
+ value === "deletion_request");
518
772
  }
519
773
  function normalizePermissions(value) {
520
- if (!Array.isArray(value) || value.length === 0 || value.some((permission) => !["append", "read", "export", "manage_retention", "manage_hold", "request_deletion"].includes(permission)) || new Set(value).size !== value.length)
774
+ if (!Array.isArray(value) ||
775
+ value.length === 0 ||
776
+ value.some((permission) => ![
777
+ "append",
778
+ "read",
779
+ "export",
780
+ "manage_retention",
781
+ "manage_hold",
782
+ "request_deletion",
783
+ ].includes(permission)) ||
784
+ new Set(value).size !== value.length)
521
785
  throw new ActionLedgerError("Ledger permissions are invalid.");
522
786
  return [...value];
523
787
  }
@@ -532,7 +796,9 @@ function exactKeys(value, expected, label) {
532
796
  throw new ActionLedgerError(`${label} contains unsupported field: ${key}`);
533
797
  }
534
798
  function identifier(value, label) {
535
- if (typeof value !== "string" || !IDENTIFIER.test(value) || sanitizeSecretString(value) !== value)
799
+ if (typeof value !== "string" ||
800
+ !IDENTIFIER.test(value) ||
801
+ sanitizeSecretString(value) !== value)
536
802
  throw new ActionLedgerError(`${label} must be a safe stable identifier.`);
537
803
  return value;
538
804
  }
@@ -552,17 +818,25 @@ function nonNegativeInteger(value, label) {
552
818
  return value;
553
819
  }
554
820
  function boundedInteger(value, label, minimum, maximum) {
555
- if (!Number.isInteger(value) || typeof value !== "number" || value < minimum || value > maximum)
821
+ if (!Number.isInteger(value) ||
822
+ typeof value !== "number" ||
823
+ value < minimum ||
824
+ value > maximum)
556
825
  throw new ActionLedgerError(`${label} must be an integer from ${minimum} through ${maximum}.`);
557
826
  return value;
558
827
  }
559
828
  function timestamp(value, label) {
560
- 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)))
829
+ if (typeof value !== "string" ||
830
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
831
+ !Number.isFinite(Date.parse(value)))
561
832
  throw new ActionLedgerError(`${label} must be an ISO UTC timestamp.`);
562
833
  return value;
563
834
  }
564
835
  function safeString(value, label, maximum) {
565
- if (typeof value !== "string" || value.length === 0 || value.length > maximum || sanitizeSecretString(value) !== value)
836
+ if (typeof value !== "string" ||
837
+ value.length === 0 ||
838
+ value.length > maximum ||
839
+ sanitizeSecretString(value) !== value)
566
840
  throw new ActionLedgerError(`${label} must be a bounded non-secret string.`);
567
841
  return value;
568
842
  }
@@ -584,7 +858,10 @@ function canonicalJson(value) {
584
858
  return `[${value.map(canonicalJson).join(",")}]`;
585
859
  if (typeof value === "object") {
586
860
  const record = value;
587
- return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
861
+ return `{${Object.keys(record)
862
+ .sort()
863
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
864
+ .join(",")}}`;
588
865
  }
589
866
  throw new ActionLedgerError("Ledger values must be JSON data.");
590
867
  }