@yiaany/ghostapi 0.1.6 → 0.1.8

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 (323) hide show
  1. package/CHANGELOG.md +48 -2
  2. package/CONTRIBUTING.md +119 -117
  3. package/LICENSE +21 -21
  4. package/README.md +208 -185
  5. package/SECURITY.md +53 -37
  6. package/dist/actions/gateway.d.ts +179 -0
  7. package/dist/actions/gateway.js +514 -0
  8. package/dist/actions/gateway.js.map +1 -0
  9. package/dist/actions/index.d.ts +2 -0
  10. package/dist/actions/index.js +2 -0
  11. package/dist/actions/index.js.map +1 -0
  12. package/dist/ai/aiClient.js +4 -4
  13. package/dist/ai/aiClient.js.map +1 -1
  14. package/dist/ai/aiGenerator.d.ts +2 -2
  15. package/dist/ai/aiGenerator.js +21 -72
  16. package/dist/ai/aiGenerator.js.map +1 -1
  17. package/dist/ai/prompts.js +51 -51
  18. package/dist/ai/prompts.js.map +1 -1
  19. package/dist/approvals/approvalInbox.d.ts +150 -0
  20. package/dist/approvals/approvalInbox.js +412 -0
  21. package/dist/approvals/approvalInbox.js.map +1 -0
  22. package/dist/approvals/index.d.ts +2 -0
  23. package/dist/approvals/index.js +2 -0
  24. package/dist/approvals/index.js.map +1 -0
  25. package/dist/behavior/behaviorStore.d.ts +2 -0
  26. package/dist/behavior/behaviorStore.js +50 -46
  27. package/dist/behavior/behaviorStore.js.map +1 -1
  28. package/dist/cache/index.d.ts +2 -0
  29. package/dist/cache/index.js +58 -14
  30. package/dist/cache/index.js.map +1 -1
  31. package/dist/cli/index.js +438 -58
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/openUrl.d.ts +10 -0
  34. package/dist/cli/openUrl.js +31 -0
  35. package/dist/cli/openUrl.js.map +1 -0
  36. package/dist/cli/parser.d.ts +155 -0
  37. package/dist/cli/parser.js +504 -2
  38. package/dist/cli/parser.js.map +1 -1
  39. package/dist/config/dataPaths.d.ts +31 -0
  40. package/dist/config/dataPaths.js +45 -0
  41. package/dist/config/dataPaths.js.map +1 -0
  42. package/dist/config/localConfig.d.ts +1 -2
  43. package/dist/config/localConfig.js +27 -21
  44. package/dist/config/localConfig.js.map +1 -1
  45. package/dist/config/serverConfig.d.ts +2 -0
  46. package/dist/config/serverConfig.js +8 -1
  47. package/dist/config/serverConfig.js.map +1 -1
  48. package/dist/contracts/contract.d.ts +76 -0
  49. package/dist/contracts/contract.js +603 -0
  50. package/dist/contracts/contract.js.map +1 -0
  51. package/dist/contracts/index.d.ts +2 -0
  52. package/dist/contracts/index.js +2 -0
  53. package/dist/contracts/index.js.map +1 -0
  54. package/dist/credentials/credentialBroker.d.ts +207 -0
  55. package/dist/credentials/credentialBroker.js +588 -0
  56. package/dist/credentials/credentialBroker.js.map +1 -0
  57. package/dist/credentials/index.d.ts +2 -0
  58. package/dist/credentials/index.js +2 -0
  59. package/dist/credentials/index.js.map +1 -0
  60. package/dist/dashboard/app.js +685 -615
  61. package/dist/dashboard/index.html +13 -7
  62. package/dist/dashboard/styles.css +349 -349
  63. package/dist/egress/capabilities.d.ts +32 -0
  64. package/dist/egress/capabilities.js +165 -0
  65. package/dist/egress/capabilities.js.map +1 -0
  66. package/dist/egress/linuxBootstrap.d.ts +1 -0
  67. package/dist/egress/linuxBootstrap.js +121 -0
  68. package/dist/egress/linuxBootstrap.js.map +1 -0
  69. package/dist/egress/run.d.ts +18 -0
  70. package/dist/egress/run.js +305 -0
  71. package/dist/egress/run.js.map +1 -0
  72. package/dist/errors/errorEngine.d.ts +2 -2
  73. package/dist/errors/errorEngine.js +3 -40
  74. package/dist/errors/errorEngine.js.map +1 -1
  75. package/dist/evals/evals.d.ts +126 -0
  76. package/dist/evals/evals.js +466 -0
  77. package/dist/evals/evals.js.map +1 -0
  78. package/dist/evals/index.d.ts +2 -0
  79. package/dist/evals/index.js +2 -0
  80. package/dist/evals/index.js.map +1 -0
  81. package/dist/evidence/index.d.ts +2 -0
  82. package/dist/evidence/index.js +2 -0
  83. package/dist/evidence/index.js.map +1 -0
  84. package/dist/evidence/report.d.ts +143 -0
  85. package/dist/evidence/report.js +556 -0
  86. package/dist/evidence/report.js.map +1 -0
  87. package/dist/fault/faultLab.d.ts +5 -4
  88. package/dist/fault/faultLab.js +66 -24
  89. package/dist/fault/faultLab.js.map +1 -1
  90. package/dist/index.d.ts +46 -0
  91. package/dist/index.js +23 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/inventory/analysis.d.ts +12 -0
  94. package/dist/inventory/analysis.js +457 -0
  95. package/dist/inventory/analysis.js.map +1 -0
  96. package/dist/inventory/index.d.ts +5 -0
  97. package/dist/inventory/index.js +4 -0
  98. package/dist/inventory/index.js.map +1 -0
  99. package/dist/inventory/inventoryController.d.ts +53 -0
  100. package/dist/inventory/inventoryController.js +1044 -0
  101. package/dist/inventory/inventoryController.js.map +1 -0
  102. package/dist/inventory/types.d.ts +579 -0
  103. package/dist/inventory/types.js +787 -0
  104. package/dist/inventory/types.js.map +1 -0
  105. package/dist/landing/assets/index-CxWsopzP.css +1 -0
  106. package/dist/landing/assets/index-DZlpyhqd.js +55 -0
  107. package/dist/landing/assets/index-Dk-Xezs3.js +55 -0
  108. package/dist/landing/index.html +15 -15
  109. package/dist/ledger/actionLedger.d.ts +125 -0
  110. package/dist/ledger/actionLedger.js +594 -0
  111. package/dist/ledger/actionLedger.js.map +1 -0
  112. package/dist/ledger/index.d.ts +2 -0
  113. package/dist/ledger/index.js +2 -0
  114. package/dist/ledger/index.js.map +1 -0
  115. package/dist/mcp/server.js +4 -3
  116. package/dist/mcp/server.js.map +1 -1
  117. package/dist/policy/index.d.ts +3 -0
  118. package/dist/policy/index.js +2 -0
  119. package/dist/policy/index.js.map +1 -0
  120. package/dist/policy/policy.d.ts +14 -0
  121. package/dist/policy/policy.js +282 -0
  122. package/dist/policy/policy.js.map +1 -0
  123. package/dist/policy/types.d.ts +52 -0
  124. package/dist/policy/types.js +2 -0
  125. package/dist/policy/types.js.map +1 -0
  126. package/dist/productTelemetry/index.d.ts +16 -0
  127. package/dist/productTelemetry/index.js +142 -0
  128. package/dist/productTelemetry/index.js.map +1 -0
  129. package/dist/providers/conformance.d.ts +7 -0
  130. package/dist/providers/conformance.js +48 -0
  131. package/dist/providers/conformance.js.map +1 -0
  132. package/dist/providers/index.d.ts +4 -2
  133. package/dist/providers/index.js +3 -1
  134. package/dist/providers/index.js.map +1 -1
  135. package/dist/providers/packs/resendPack.d.ts +2 -0
  136. package/dist/providers/packs/resendPack.js +147 -0
  137. package/dist/providers/packs/resendPack.js.map +1 -0
  138. package/dist/providers/packs/stripePack.d.ts +2 -0
  139. package/dist/providers/packs/stripePack.js +852 -0
  140. package/dist/providers/packs/stripePack.js.map +1 -0
  141. package/dist/providers/registry.d.ts +7 -3
  142. package/dist/providers/registry.js +55 -4
  143. package/dist/providers/registry.js.map +1 -1
  144. package/dist/providers/resend.js +2 -11
  145. package/dist/providers/resend.js.map +1 -1
  146. package/dist/providers/runtime.d.ts +8 -0
  147. package/dist/providers/runtime.js +48 -0
  148. package/dist/providers/runtime.js.map +1 -0
  149. package/dist/providers/stripeWebhook.d.ts +13 -0
  150. package/dist/providers/stripeWebhook.js +50 -0
  151. package/dist/providers/stripeWebhook.js.map +1 -0
  152. package/dist/providers/types.d.ts +153 -0
  153. package/dist/proxy/cacheKey.js +3 -3
  154. package/dist/proxy/cacheKey.js.map +1 -1
  155. package/dist/proxy/providerDetector.d.ts +2 -7
  156. package/dist/proxy/providerDetector.js +4 -4
  157. package/dist/proxy/providerDetector.js.map +1 -1
  158. package/dist/proxy/proxyHandler.js +116 -12
  159. package/dist/proxy/proxyHandler.js.map +1 -1
  160. package/dist/proxy/requestNormalizer.d.ts +0 -1
  161. package/dist/proxy/requestNormalizer.js +2 -5
  162. package/dist/proxy/requestNormalizer.js.map +1 -1
  163. package/dist/reliability/costGovernance.d.ts +175 -0
  164. package/dist/reliability/costGovernance.js +515 -0
  165. package/dist/reliability/costGovernance.js.map +1 -0
  166. package/dist/reliability/index.d.ts +8 -0
  167. package/dist/reliability/index.js +5 -0
  168. package/dist/reliability/index.js.map +1 -0
  169. package/dist/reliability/reconciliation.d.ts +157 -0
  170. package/dist/reliability/reconciliation.js +522 -0
  171. package/dist/reliability/reconciliation.js.map +1 -0
  172. package/dist/reliability/runtimeHealth.d.ts +60 -0
  173. package/dist/reliability/runtimeHealth.js +356 -0
  174. package/dist/reliability/runtimeHealth.js.map +1 -0
  175. package/dist/reliability/slo.d.ts +131 -0
  176. package/dist/reliability/slo.js +375 -0
  177. package/dist/reliability/slo.js.map +1 -0
  178. package/dist/report/safetyReport.js +7 -3
  179. package/dist/report/safetyReport.js.map +1 -1
  180. package/dist/safety/index.d.ts +2 -0
  181. package/dist/safety/index.js +2 -0
  182. package/dist/safety/index.js.map +1 -0
  183. package/dist/safety/safetyController.d.ts +187 -0
  184. package/dist/safety/safetyController.js +477 -0
  185. package/dist/safety/safetyController.js.map +1 -0
  186. package/dist/scenarios/scenarioBundle.d.ts +75 -0
  187. package/dist/scenarios/scenarioBundle.js +750 -0
  188. package/dist/scenarios/scenarioBundle.js.map +1 -0
  189. package/dist/scenarios/scenarioStore.d.ts +1 -0
  190. package/dist/scenarios/scenarioStore.js +47 -32
  191. package/dist/scenarios/scenarioStore.js.map +1 -1
  192. package/dist/security/headerSanitizer.d.ts +2 -0
  193. package/dist/security/headerSanitizer.js +36 -1
  194. package/dist/security/headerSanitizer.js.map +1 -1
  195. package/dist/security/secrets.js +24 -4
  196. package/dist/security/secrets.js.map +1 -1
  197. package/dist/server/accessControl.d.ts +5 -0
  198. package/dist/server/accessControl.js +100 -0
  199. package/dist/server/accessControl.js.map +1 -0
  200. package/dist/server/createServer.d.ts +0 -5
  201. package/dist/server/createServer.js +9 -29
  202. package/dist/server/createServer.js.map +1 -1
  203. package/dist/server/eventsStore.d.ts +6 -1
  204. package/dist/server/eventsStore.js +83 -15
  205. package/dist/server/eventsStore.js.map +1 -1
  206. package/dist/server/routes.js +36 -10
  207. package/dist/server/routes.js.map +1 -1
  208. package/dist/server/sse.d.ts +2 -1
  209. package/dist/server/sse.js +15 -3
  210. package/dist/server/sse.js.map +1 -1
  211. package/dist/setup/setupGenerator.js +53 -6
  212. package/dist/setup/setupGenerator.js.map +1 -1
  213. package/dist/state/stateStore.d.ts +5 -0
  214. package/dist/state/stateStore.js +48 -62
  215. package/dist/state/stateStore.js.map +1 -1
  216. package/dist/storage/fileStore.d.ts +10 -0
  217. package/dist/storage/fileStore.js +131 -0
  218. package/dist/storage/fileStore.js.map +1 -0
  219. package/dist/teamControl/controlPlane.d.ts +255 -0
  220. package/dist/teamControl/controlPlane.js +918 -0
  221. package/dist/teamControl/controlPlane.js.map +1 -0
  222. package/dist/teamControl/deployment.d.ts +33 -0
  223. package/dist/teamControl/deployment.js +67 -0
  224. package/dist/teamControl/deployment.js.map +1 -0
  225. package/dist/teamControl/index.d.ts +4 -0
  226. package/dist/teamControl/index.js +3 -0
  227. package/dist/teamControl/index.js.map +1 -0
  228. package/dist/tests/testGenerator.js +4 -1
  229. package/dist/tests/testGenerator.js.map +1 -1
  230. package/dist/trust/index.d.ts +2 -0
  231. package/dist/trust/index.js +2 -0
  232. package/dist/trust/index.js.map +1 -0
  233. package/dist/trust/trustLadder.d.ts +182 -0
  234. package/dist/trust/trustLadder.js +560 -0
  235. package/dist/trust/trustLadder.js.map +1 -0
  236. package/dist/worlds/index.d.ts +2 -0
  237. package/dist/worlds/index.js +2 -0
  238. package/dist/worlds/index.js.map +1 -0
  239. package/dist/worlds/worlds.d.ts +160 -0
  240. package/dist/worlds/worlds.js +491 -0
  241. package/dist/worlds/worlds.js.map +1 -0
  242. package/docs/assets/dashboard.png +0 -0
  243. package/docs/ci.md +49 -0
  244. package/docs/commercial/README.md +29 -0
  245. package/docs/commercial/data-inventory.md +36 -0
  246. package/docs/commercial/manual-invoicing.md +44 -0
  247. package/docs/commercial/metrics.md +51 -0
  248. package/docs/commercial/pricing.md +103 -0
  249. package/docs/commercial/privacy-policy-draft.md +41 -0
  250. package/docs/commercial/terms-topics-for-counsel.md +51 -0
  251. package/docs/design-partners/README.md +52 -0
  252. package/docs/design-partners/demo-narratives.md +33 -0
  253. package/docs/design-partners/design-partner-offer.md +39 -0
  254. package/docs/design-partners/discovery-questions.md +28 -0
  255. package/docs/design-partners/feedback-capture-template.md +48 -0
  256. package/docs/design-partners/icp-one-pager.md +43 -0
  257. package/docs/design-partners/onboarding-checklist.md +33 -0
  258. package/docs/design-partners/pilot-success-criteria.md +27 -0
  259. package/docs/design-partners/pricing-interview-script.md +18 -0
  260. package/docs/design-partners/telemetry-plan.md +48 -0
  261. package/docs/development/baseline.md +114 -0
  262. package/docs/development/onboarding-smoke.md +37 -0
  263. package/docs/development/verification-0.1.8.md +25 -0
  264. package/docs/enterprise-product-roadmap-ru.md +1879 -0
  265. package/docs/fundraising/README.md +87 -0
  266. package/docs/fundraising/data-room-checklist.md +72 -0
  267. package/docs/fundraising/demo-script.md +52 -0
  268. package/docs/fundraising/design-partners-50.md +74 -0
  269. package/docs/fundraising/launch-posts.md +37 -0
  270. package/docs/fundraising/metrics-and-evidence.md +30 -0
  271. package/docs/fundraising/roadmap-12-month.md +25 -0
  272. package/docs/fundraising/technical-due-diligence-index.md +37 -0
  273. package/docs/fundraising/yc-application.md +25 -0
  274. package/docs/github-actions.md +84 -0
  275. package/docs/hosted-pilot.md +111 -0
  276. package/docs/mcp.md +52 -50
  277. package/docs/operations/disaster-recovery-runbook.md +81 -0
  278. package/docs/operations/kill-switch-runbook.md +32 -0
  279. package/docs/policy.md +61 -0
  280. package/docs/providers/authoring-packs.md +175 -0
  281. package/docs/providers/stripe-core-pack.md +82 -0
  282. package/docs/release-checklist.md +83 -71
  283. package/docs/release-migration-and-rollback.md +63 -0
  284. package/docs/release-readiness.md +39 -0
  285. package/docs/security/action-gateway-threat-model.md +34 -0
  286. package/docs/security/action-ledger-incident-replay-threat-model.md +42 -0
  287. package/docs/security/approval-inbox-threat-model.md +35 -0
  288. package/docs/security/credential-broker-threat-model.md +52 -0
  289. package/docs/security/egress-threat-model.md +126 -0
  290. package/docs/security/inventory-threat-model.md +104 -0
  291. package/docs/security/kill-switch-budgets-threat-model.md +37 -0
  292. package/docs/security/reliability-threat-model.md +111 -0
  293. package/docs/security/trust-ladder-threat-model.md +33 -0
  294. package/docs/team-control-plane.md +72 -0
  295. package/docs/usage.md +571 -57
  296. package/examples/README.md +10 -0
  297. package/examples/agent-instructions/README.md +39 -39
  298. package/examples/ci-smoke/README.md +21 -0
  299. package/examples/ci-smoke/ghostapi.policy.yaml +22 -0
  300. package/examples/ci-smoke/package.json +9 -0
  301. package/examples/ci-smoke/production-egress.mjs +2 -0
  302. package/examples/ci-smoke/safe.mjs +17 -0
  303. package/examples/evals/README.md +24 -0
  304. package/examples/evals/retry-after.eval.json +52 -0
  305. package/examples/generic-rest/README.md +27 -27
  306. package/examples/github-fetch/README.md +35 -35
  307. package/examples/openai-streaming/README.md +16 -0
  308. package/examples/openai-streaming/streaming-tool-call.mjs +31 -0
  309. package/examples/policy/ghostapi.policy.yaml +27 -0
  310. package/examples/record-replay/README.md +19 -0
  311. package/examples/record-replay/replay-requests.json +22 -0
  312. package/examples/record-replay/stripe-checkout.har.json +30 -0
  313. package/examples/resend-node/README.md +22 -22
  314. package/examples/stripe-node/README.md +35 -29
  315. package/examples/stripe-node/checkout-flow.mjs +61 -0
  316. package/examples/twilio-node/README.md +21 -21
  317. package/examples/worlds/README.md +19 -0
  318. package/examples/worlds/subscription-recovery.mjs +12 -0
  319. package/package.json +26 -11
  320. package/dist/providers/stripe.d.ts +0 -2
  321. package/dist/providers/stripe.js +0 -15
  322. package/dist/providers/stripe.js.map +0 -1
  323. package/docs/assets/ghostapi-avatar.png +0 -0
@@ -0,0 +1,157 @@
1
+ import type { LocalActionLedger } from "../ledger/index.js";
2
+ import { type LocalSloController } from "./slo.js";
3
+ export declare const RECONCILIATION_OUTCOMES: readonly ["committed", "not_committed", "unknown", "compensated", "drifted"];
4
+ export type ReconciliationOutcome = (typeof RECONCILIATION_OUTCOMES)[number];
5
+ export type ReconciliationLedgerIntent = "committed" | "committed_unverified" | "ambiguous" | "failed" | "attempted" | "compensated" | "not_committed";
6
+ export type ReconciliationProviderState = {
7
+ receipts: ReadonlyArray<{
8
+ actionId: string;
9
+ }>;
10
+ };
11
+ export type ReconciliationProvider = {
12
+ resolveWorld(input: {
13
+ actionId: string;
14
+ actionHash: string;
15
+ }): Promise<string | null>;
16
+ read(input: {
17
+ worldId: string;
18
+ }): Promise<ReconciliationProviderState | null>;
19
+ };
20
+ export type ReconciliationActionResult = {
21
+ tenantId: string;
22
+ actionId: string;
23
+ actionHash: string;
24
+ ledgerIntent: ReconciliationLedgerIntent;
25
+ providerWorldId: string | null;
26
+ providerPresent: boolean | null;
27
+ outcome: ReconciliationOutcome;
28
+ retrySafe: boolean;
29
+ reconciledFromProvider: boolean;
30
+ reasons: string[];
31
+ };
32
+ export type ReconciliationSli = {
33
+ duplicatePrevention: {
34
+ measured: number;
35
+ ok: number;
36
+ okRateBps: number;
37
+ };
38
+ receiptVerification: {
39
+ measured: number;
40
+ ok: number;
41
+ okRateBps: number;
42
+ };
43
+ availability: {
44
+ measured: number;
45
+ ok: number;
46
+ okRateBps: number;
47
+ };
48
+ executionLatency: {
49
+ measured: number;
50
+ avgMs: number;
51
+ p95Ms: number;
52
+ p99Ms: number;
53
+ basis: "ledger_record_interval";
54
+ };
55
+ };
56
+ export type ReconciliationReport = {
57
+ schemaVersion: 1;
58
+ kind: "ghostapi.reconciliation-report";
59
+ runId: string;
60
+ tenantId: string;
61
+ ranAt: string;
62
+ integrity: "valid";
63
+ actions: ReconciliationActionResult[];
64
+ counts: Record<ReconciliationOutcome, number>;
65
+ sli: ReconciliationSli;
66
+ findingsOpened: number;
67
+ };
68
+ export type ReconciliationFinding = {
69
+ schemaVersion: 1;
70
+ kind: "ghostapi.reconciliation-finding";
71
+ findingId: string;
72
+ tenantId: string;
73
+ actionId: string;
74
+ actionHash: string;
75
+ classification: "drifted" | "unknown";
76
+ detail: string;
77
+ detectedAt: string;
78
+ status: "open" | "resolved";
79
+ resolution?: {
80
+ resolvedAt: string;
81
+ resolvedBy: string;
82
+ reason: string;
83
+ evidenceRef?: string;
84
+ };
85
+ };
86
+ export type ReconciliationState = {
87
+ schemaVersion: 1;
88
+ kind: "ghostapi.reconciliation";
89
+ lastRun: {
90
+ runId: string;
91
+ ranAt: string;
92
+ counts: Record<ReconciliationOutcome, number>;
93
+ } | null;
94
+ findings: ReconciliationFinding[];
95
+ };
96
+ export type ReconciliationOperatorPermission = "reconciliation.manage" | "reconciliation.inspect";
97
+ export type ReconciliationOperator = {
98
+ id: string;
99
+ principalId: string;
100
+ permissions: readonly ReconciliationOperatorPermission[];
101
+ };
102
+ export interface ReconciliationOperatorAuthorizer {
103
+ authenticate(identity: unknown): Promise<ReconciliationOperator>;
104
+ }
105
+ export type ReconciliationServiceOptions = {
106
+ path?: string;
107
+ now?: () => Date;
108
+ ledger: LocalActionLedger;
109
+ capability: object;
110
+ provider: ReconciliationProvider;
111
+ sloController?: LocalSloController;
112
+ operatorAuthorizer?: ReconciliationOperatorAuthorizer;
113
+ };
114
+ export declare class ReconciliationError extends Error {
115
+ constructor(message: string);
116
+ }
117
+ export declare function createLocalReconciliationService(options: ReconciliationServiceOptions): LocalReconciliationService;
118
+ export declare class LocalReconciliationService {
119
+ private readonly path;
120
+ private readonly now;
121
+ private readonly ledger;
122
+ private readonly capability;
123
+ private readonly provider;
124
+ private readonly sloController;
125
+ private readonly sloRecordIdentity;
126
+ private readonly operatorAuthorizer;
127
+ constructor(options: ReconciliationServiceOptions);
128
+ runReconciliation(input: {
129
+ identity: unknown;
130
+ }): Promise<ReconciliationReport>;
131
+ listFindings(input: {
132
+ identity: unknown;
133
+ }): Promise<ReconciliationFinding[]>;
134
+ resolveDrift(input: {
135
+ identity: unknown;
136
+ findingId: string;
137
+ reason: string;
138
+ }): Promise<ReconciliationFinding>;
139
+ resolveUnknown(input: {
140
+ identity: unknown;
141
+ findingId: string;
142
+ reason: string;
143
+ evidenceRef: string;
144
+ }): Promise<ReconciliationFinding>;
145
+ private classify;
146
+ private authorize;
147
+ private timestamp;
148
+ private read;
149
+ private mutate;
150
+ }
151
+ export declare function createDisabledReconciliationOperatorAuthorizer(): ReconciliationOperatorAuthorizer;
152
+ export declare function createTestReconciliationOperatorAuthorizer(): {
153
+ authorizer: ReconciliationOperatorAuthorizer;
154
+ issue(input: ReconciliationOperator): ReconciliationOperator;
155
+ };
156
+ export declare function createWorldStateReconciliationProvider(resolveWorld: (actionId: string, actionHash: string) => Promise<string | null>): ReconciliationProvider;
157
+ export declare function formatReconciliationReport(report: ReconciliationReport): string;
@@ -0,0 +1,522 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, readFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { getDataPaths } from "../config/dataPaths.js";
5
+ import { sanitizeSecretString } from "../security/secrets.js";
6
+ import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
7
+ import { inspectWorld, SyntheticWorldError } from "../worlds/index.js";
8
+ import { createSloRecordIdentity } from "./slo.js";
9
+ const SCHEMA_VERSION = 1;
10
+ const KIND = "ghostapi.reconciliation";
11
+ const MAX_STORE_BYTES = 4 * 1024 * 1024;
12
+ const MAX_FINDINGS = 1_000;
13
+ const IDENTIFIER = /^[a-z0-9][a-z0-9._-]{0,127}$/;
14
+ const HASH = /^[a-f0-9]{64}$/;
15
+ export const RECONCILIATION_OUTCOMES = ["committed", "not_committed", "unknown", "compensated", "drifted"];
16
+ export class ReconciliationError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "ReconciliationError";
20
+ }
21
+ }
22
+ export function createLocalReconciliationService(options) {
23
+ return new LocalReconciliationService(options);
24
+ }
25
+ export class LocalReconciliationService {
26
+ path;
27
+ now;
28
+ ledger;
29
+ capability;
30
+ provider;
31
+ sloController;
32
+ sloRecordIdentity;
33
+ operatorAuthorizer;
34
+ constructor(options) {
35
+ this.path = options.path ?? getDataPaths().reconciliationStore;
36
+ this.now = options.now ?? (() => new Date());
37
+ this.ledger = options.ledger;
38
+ this.capability = options.capability;
39
+ this.provider = options.provider;
40
+ this.sloController = options.sloController;
41
+ this.sloRecordIdentity = createSloRecordIdentity();
42
+ this.operatorAuthorizer = options.operatorAuthorizer ?? createDisabledReconciliationOperatorAuthorizer();
43
+ }
44
+ async runReconciliation(input) {
45
+ await this.authorize(input.identity, "reconciliation.manage");
46
+ const runId = `recon-${randomUUID().replace(/-/g, "").slice(0, 32)}`;
47
+ const now = this.timestamp();
48
+ let exportResult;
49
+ try {
50
+ exportResult = await this.ledger.exportTenant(this.capability);
51
+ }
52
+ catch (error) {
53
+ throw new ReconciliationError(`Reconciliation is blocked because the tenant ledger failed integrity verification: ${error instanceof Error ? error.message : "unknown ledger error"}`);
54
+ }
55
+ const tenantId = exportResult.tenantId;
56
+ const timelines = groupByAction(exportResult.entries);
57
+ const results = [];
58
+ const measurements = [];
59
+ for (const actionId of Object.keys(timelines).sort()) {
60
+ const timeline = timelines[actionId];
61
+ const result = await this.classify(tenantId, actionId, timeline);
62
+ results.push(result);
63
+ measurements.push(measureAction(timeline, result.outcome));
64
+ }
65
+ const counts = countOutcomes(results);
66
+ const sli = aggregateSli(results, measurements);
67
+ const findingsOpened = await this.mutate((state) => {
68
+ state.lastRun = { runId, ranAt: now, counts };
69
+ let opened = 0;
70
+ for (const result of results) {
71
+ if (result.outcome === "drifted" || result.outcome === "unknown") {
72
+ const alreadyOpen = state.findings.some((finding) => finding.actionHash === result.actionHash && finding.status === "open");
73
+ if (!alreadyOpen) {
74
+ if (state.findings.length >= MAX_FINDINGS)
75
+ throw new ReconciliationError("Reconciliation findings limit was reached.");
76
+ state.findings.push(makeFinding(result, now));
77
+ opened += 1;
78
+ }
79
+ }
80
+ }
81
+ return opened;
82
+ });
83
+ if (this.sloController !== undefined) {
84
+ const samples = [];
85
+ for (let index = 0; index < results.length; index += 1) {
86
+ const result = results[index];
87
+ const measurement = measurements[index];
88
+ if (!measurement.executed)
89
+ continue;
90
+ samples.push({ metric: "duplicate_prevention", ok: measurement.duplicateOk, runId, actionId: result.actionId, labels: { tenantId } });
91
+ samples.push({ metric: "availability", ok: measurement.availabilityOk, runId, actionId: result.actionId, labels: { tenantId } });
92
+ if (measurement.verificationOk !== null)
93
+ samples.push({ metric: "receipt_verification", ok: measurement.verificationOk, runId, actionId: result.actionId, labels: { tenantId } });
94
+ if (measurement.latencyMs !== null)
95
+ samples.push({ metric: "execution_latency", ok: true, durationMs: measurement.latencyMs, runId, actionId: result.actionId, labels: { tenantId } });
96
+ }
97
+ if (samples.length > 0)
98
+ await this.sloController.recordSamples(samples, this.sloRecordIdentity);
99
+ }
100
+ return { schemaVersion: 1, kind: "ghostapi.reconciliation-report", runId, tenantId, ranAt: now, integrity: "valid", actions: results, counts, sli, findingsOpened };
101
+ }
102
+ async listFindings(input) {
103
+ await this.authorize(input.identity, "reconciliation.inspect");
104
+ const state = await this.read();
105
+ return clone(state.findings);
106
+ }
107
+ async resolveDrift(input) {
108
+ const operator = await this.authorize(input.identity, "reconciliation.manage");
109
+ const findingId = identifier(input.findingId, "Reconciliation finding id");
110
+ const reason = text(input.reason, "Reconciliation resolution reason", 300);
111
+ return this.mutate((state) => {
112
+ const finding = findFinding(state, findingId);
113
+ if (finding.classification !== "drifted")
114
+ throw new ReconciliationError("Only drifted findings can be resolved with a drift resolution reason.");
115
+ if (finding.status === "resolved")
116
+ throw new ReconciliationError("Reconciliation finding is already resolved.");
117
+ finding.status = "resolved";
118
+ finding.resolution = { resolvedAt: this.timestamp(), resolvedBy: operator.principalId, reason };
119
+ return clone(finding);
120
+ });
121
+ }
122
+ async resolveUnknown(input) {
123
+ const operator = await this.authorize(input.identity, "reconciliation.manage");
124
+ const findingId = identifier(input.findingId, "Reconciliation finding id");
125
+ const reason = text(input.reason, "Reconciliation resolution reason", 300);
126
+ const evidenceRef = evidence(input.evidenceRef, "Reconciliation evidence reference");
127
+ return this.mutate((state) => {
128
+ const finding = findFinding(state, findingId);
129
+ if (finding.classification !== "unknown")
130
+ throw new ReconciliationError("Only unknown findings can be resolved with provider evidence.");
131
+ if (finding.status === "resolved")
132
+ throw new ReconciliationError("Reconciliation finding is already resolved.");
133
+ finding.status = "resolved";
134
+ finding.resolution = { resolvedAt: this.timestamp(), resolvedBy: operator.principalId, reason, evidenceRef };
135
+ return clone(finding);
136
+ });
137
+ }
138
+ async classify(tenantId, actionId, timeline) {
139
+ const actionHash = timeline[0].actionHash;
140
+ const intent = classifyIntent(timeline);
141
+ const reasons = intentReasons(intent);
142
+ const worldId = await this.provider.resolveWorld({ actionId, actionHash });
143
+ let providerPresent = null;
144
+ if (worldId !== null) {
145
+ const world = await this.provider.read({ worldId });
146
+ providerPresent = world !== null && world.receipts.some((receipt) => receipt.actionId === actionId);
147
+ }
148
+ const decided = decideOutcome(intent, providerPresent);
149
+ if (providerPresent === null)
150
+ reasons.push("provider state could not be inspected for this action");
151
+ if (decided.reconciledFromProvider)
152
+ reasons.push("ledger outcome was incomplete; provider evidence confirms the commitment");
153
+ if (decided.outcome === "drifted")
154
+ reasons.push(providerPresent ? "provider has a receipt the ledger does not record" : "ledger records a commitment the provider does not confirm");
155
+ return {
156
+ tenantId,
157
+ actionId,
158
+ actionHash,
159
+ ledgerIntent: intent,
160
+ providerWorldId: worldId,
161
+ providerPresent,
162
+ outcome: decided.outcome,
163
+ retrySafe: decided.retrySafe,
164
+ reconciledFromProvider: decided.reconciledFromProvider,
165
+ reasons
166
+ };
167
+ }
168
+ async authorize(identity, permission) {
169
+ const operator = validateOperator(await this.operatorAuthorizer.authenticate(identity));
170
+ if (!operator.permissions.includes(permission))
171
+ throw new ReconciliationError(`Reconciliation operator lacks required permission: ${permission}.`);
172
+ return operator;
173
+ }
174
+ timestamp() {
175
+ const value = this.now();
176
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
177
+ throw new ReconciliationError("Reconciliation clock is invalid.");
178
+ return value.toISOString();
179
+ }
180
+ async read() {
181
+ await ensurePrivateDirectory(dirname(this.path));
182
+ const info = await lstat(this.path).catch((error) => isErrorCode(error, "ENOENT") ? null : Promise.reject(error));
183
+ if (info === null)
184
+ return emptyState();
185
+ if (!info.isFile() || info.isSymbolicLink())
186
+ throw new ReconciliationError("Reconciliation store must be a regular non-symlink file.");
187
+ const source = await readFile(this.path, "utf8");
188
+ if (Buffer.byteLength(source, "utf8") > MAX_STORE_BYTES)
189
+ throw new ReconciliationError("Reconciliation store exceeds its size limit.");
190
+ try {
191
+ return validateState(JSON.parse(source));
192
+ }
193
+ catch (error) {
194
+ if (error instanceof ReconciliationError)
195
+ throw error;
196
+ throw new ReconciliationError("Reconciliation store is not valid JSON.");
197
+ }
198
+ }
199
+ async mutate(operation) {
200
+ return withFileLock(this.path, async () => {
201
+ const state = await this.read();
202
+ const result = operation(state);
203
+ await atomicWriteJson(this.path, validateState(state));
204
+ return result;
205
+ });
206
+ }
207
+ }
208
+ export function createDisabledReconciliationOperatorAuthorizer() {
209
+ return { async authenticate() { throw new ReconciliationError("Reconciliation operator authorization is not configured."); } };
210
+ }
211
+ export function createTestReconciliationOperatorAuthorizer() {
212
+ const issued = new WeakSet();
213
+ return {
214
+ authorizer: { async authenticate(identity) { if (identity === null || typeof identity !== "object" || !issued.has(identity))
215
+ throw new ReconciliationError("Reconciliation operator identity is not authenticated."); return validateOperator(identity); } },
216
+ issue(input) { const operator = Object.freeze(validateOperator(input)); issued.add(operator); return operator; }
217
+ };
218
+ }
219
+ export function createWorldStateReconciliationProvider(resolveWorld) {
220
+ return {
221
+ async resolveWorld(input) {
222
+ return resolveWorld(input.actionId, input.actionHash);
223
+ },
224
+ async read(input) {
225
+ try {
226
+ const world = await inspectWorld(input.worldId);
227
+ return { receipts: world.state.receipts.map((receipt) => ({ actionId: receipt.actionId })) };
228
+ }
229
+ catch (error) {
230
+ if (error instanceof SyntheticWorldError && error.code === "WORLD_NOT_FOUND")
231
+ return null;
232
+ throw error;
233
+ }
234
+ }
235
+ };
236
+ }
237
+ export function formatReconciliationReport(report) {
238
+ const lines = [`Reconciliation run ${report.runId} for tenant ${report.tenantId} at ${report.ranAt}`];
239
+ lines.push(` ledger integrity: ${report.integrity}`);
240
+ lines.push(` outcomes: committed=${report.counts.committed} not_committed=${report.counts.not_committed} unknown=${report.counts.unknown} compensated=${report.counts.compensated} drifted=${report.counts.drifted}`);
241
+ lines.push(` findings opened: ${report.findingsOpened}`);
242
+ lines.push(` duplicate_prevention ok ${report.sli.duplicatePrevention.ok}/${report.sli.duplicatePrevention.measured} (${report.sli.duplicatePrevention.okRateBps} bps)`);
243
+ lines.push(` receipt_verification ok ${report.sli.receiptVerification.ok}/${report.sli.receiptVerification.measured} (${report.sli.receiptVerification.okRateBps} bps)`);
244
+ lines.push(` availability ok ${report.sli.availability.ok}/${report.sli.availability.measured} (${report.sli.availability.okRateBps} bps)`);
245
+ lines.push(` execution_latency avg ${Math.round(report.sli.executionLatency.avgMs)}ms p95 ${Math.round(report.sli.executionLatency.p95Ms)}ms p99 ${Math.round(report.sli.executionLatency.p99Ms)}ms`);
246
+ for (const action of report.actions) {
247
+ if (action.outcome === "drifted" || action.outcome === "unknown") {
248
+ lines.push(` ${action.outcome}: ${action.actionId} (${action.ledgerIntent}) provider=${action.providerPresent === null ? "unresolved" : action.providerPresent} ${action.reasons.join("; ")}`);
249
+ }
250
+ }
251
+ return lines.join("\n");
252
+ }
253
+ function classifyIntent(timeline) {
254
+ const compensation = lastEntry(timeline, "compensation");
255
+ if (compensation?.data.status === "attempted")
256
+ return "compensated";
257
+ const providerReceipts = entries(timeline, "provider_receipt");
258
+ const verifications = entries(timeline, "verification");
259
+ const attempts = entries(timeline, "execution_attempt");
260
+ if (providerReceipts.length > 0) {
261
+ const verification = verifications.at(-1);
262
+ if (verification?.data.status === "verified")
263
+ return "committed";
264
+ return "committed_unverified";
265
+ }
266
+ const attempt = attempts.at(-1);
267
+ if (attempt !== undefined) {
268
+ if (attempt.data.status === "ambiguous")
269
+ return "ambiguous";
270
+ if (attempt.data.status === "failed")
271
+ return "failed";
272
+ return "attempted";
273
+ }
274
+ return "not_committed";
275
+ }
276
+ function decideOutcome(intent, providerPresent) {
277
+ switch (intent) {
278
+ case "committed":
279
+ if (providerPresent === true)
280
+ return { outcome: "committed", retrySafe: false, reconciledFromProvider: false };
281
+ if (providerPresent === false)
282
+ return { outcome: "drifted", retrySafe: false, reconciledFromProvider: false };
283
+ return { outcome: "unknown", retrySafe: false, reconciledFromProvider: false };
284
+ case "committed_unverified":
285
+ case "ambiguous":
286
+ case "failed":
287
+ case "attempted":
288
+ if (providerPresent === true)
289
+ return { outcome: "committed", retrySafe: false, reconciledFromProvider: true };
290
+ return { outcome: "unknown", retrySafe: false, reconciledFromProvider: false };
291
+ case "not_committed":
292
+ if (providerPresent === true)
293
+ return { outcome: "drifted", retrySafe: false, reconciledFromProvider: false };
294
+ if (providerPresent === false)
295
+ return { outcome: "not_committed", retrySafe: true, reconciledFromProvider: false };
296
+ return { outcome: "unknown", retrySafe: false, reconciledFromProvider: false };
297
+ case "compensated":
298
+ return { outcome: "compensated", retrySafe: false, reconciledFromProvider: false };
299
+ }
300
+ }
301
+ function intentReasons(intent) {
302
+ switch (intent) {
303
+ case "committed": return ["ledger shows a verified provider receipt"];
304
+ case "committed_unverified": return ["ledger records a provider receipt without a verified proof"];
305
+ case "ambiguous": return ["ledger records an ambiguous execution outcome"];
306
+ case "failed": return ["ledger records a failed execution outcome"];
307
+ case "attempted": return ["ledger records an execution attempt without a receipt"];
308
+ case "not_committed": return ["ledger never reached the provider"];
309
+ case "compensated": return ["ledger records an attempted compensation"];
310
+ }
311
+ }
312
+ function measureAction(timeline, outcome) {
313
+ const providerReceipts = entries(timeline, "provider_receipt");
314
+ const verifications = entries(timeline, "verification");
315
+ const attempts = entries(timeline, "execution_attempt");
316
+ const executed = attempts.length > 0 || providerReceipts.length > 0;
317
+ const duplicateOk = providerReceipts.length <= 1 && verifications.length <= 1;
318
+ const verificationOk = providerReceipts.length === 0 ? null : verifications.at(-1)?.data.status === "verified";
319
+ const availabilityOk = !executed ? false : outcome === "committed" || outcome === "not_committed" || outcome === "compensated";
320
+ const receipt = providerReceipts[0];
321
+ const proof = verifications.at(-1);
322
+ const latencyMs = receipt !== undefined && proof !== undefined ? Math.max(0, Date.parse(proof.timestamp) - Date.parse(receipt.timestamp)) : null;
323
+ return { executed, duplicateOk, verificationOk, availabilityOk, latencyMs };
324
+ }
325
+ function aggregateSli(results, measurements) {
326
+ const duplicatePrevention = { measured: 0, ok: 0 };
327
+ const receiptVerification = { measured: 0, ok: 0 };
328
+ const availability = { measured: 0, ok: 0 };
329
+ const latencies = [];
330
+ for (let index = 0; index < results.length; index += 1) {
331
+ const measurement = measurements[index];
332
+ if (measurement.executed) {
333
+ duplicatePrevention.measured += 1;
334
+ if (measurement.duplicateOk)
335
+ duplicatePrevention.ok += 1;
336
+ availability.measured += 1;
337
+ if (measurement.availabilityOk)
338
+ availability.ok += 1;
339
+ }
340
+ if (measurement.verificationOk !== null) {
341
+ receiptVerification.measured += 1;
342
+ if (measurement.verificationOk)
343
+ receiptVerification.ok += 1;
344
+ }
345
+ if (measurement.latencyMs !== null)
346
+ latencies.push(measurement.latencyMs);
347
+ }
348
+ const ordered = [...latencies].sort((left, right) => left - right);
349
+ const percentile = (value) => ordered.length === 0 ? 0 : ordered[Math.min(ordered.length - 1, Math.max(0, Math.ceil((value / 100) * ordered.length) - 1))];
350
+ return {
351
+ duplicatePrevention: { ...duplicatePrevention, okRateBps: rateBps(duplicatePrevention.ok, duplicatePrevention.measured) },
352
+ receiptVerification: { ...receiptVerification, okRateBps: rateBps(receiptVerification.ok, receiptVerification.measured) },
353
+ availability: { ...availability, okRateBps: rateBps(availability.ok, availability.measured) },
354
+ executionLatency: { measured: latencies.length, avgMs: latencies.length === 0 ? 0 : latencies.reduce((sum, value) => sum + value, 0) / latencies.length, p95Ms: percentile(95), p99Ms: percentile(99), basis: "ledger_record_interval" }
355
+ };
356
+ }
357
+ function countOutcomes(results) {
358
+ const counts = { committed: 0, not_committed: 0, unknown: 0, compensated: 0, drifted: 0 };
359
+ for (const result of results)
360
+ counts[result.outcome] += 1;
361
+ return counts;
362
+ }
363
+ function makeFinding(result, now) {
364
+ const classification = result.outcome === "drifted" ? "drifted" : "unknown";
365
+ return {
366
+ schemaVersion: 1,
367
+ kind: "ghostapi.reconciliation-finding",
368
+ findingId: `finding-${randomUUID().replace(/-/g, "").slice(0, 32)}`,
369
+ tenantId: result.tenantId,
370
+ actionId: result.actionId,
371
+ actionHash: result.actionHash,
372
+ classification,
373
+ detail: result.reasons.join("; "),
374
+ detectedAt: now,
375
+ status: "open"
376
+ };
377
+ }
378
+ function groupByAction(entries) {
379
+ const groups = {};
380
+ for (const entry of entries) {
381
+ if (entry.actionId.startsWith("governance-"))
382
+ continue;
383
+ (groups[entry.actionId] ??= []).push(entry);
384
+ }
385
+ return groups;
386
+ }
387
+ function entries(timeline, stage) {
388
+ return timeline.filter((entry) => entry.stage === stage);
389
+ }
390
+ function lastEntry(timeline, stage) {
391
+ return entries(timeline, stage).at(-1);
392
+ }
393
+ function emptyState() {
394
+ return { schemaVersion: 1, kind: "ghostapi.reconciliation", lastRun: null, findings: [] };
395
+ }
396
+ function validateState(value) {
397
+ const state = object(value, "Reconciliation store must be an object.");
398
+ exactKeys(state, ["schemaVersion", "kind", "lastRun", "findings"], "Reconciliation store");
399
+ if (state.schemaVersion !== SCHEMA_VERSION || state.kind !== KIND)
400
+ throw new ReconciliationError("Unsupported reconciliation store schema.");
401
+ const findings = array(state.findings, "Reconciliation findings", MAX_FINDINGS).map(validateFinding);
402
+ unique(findings.map((finding) => finding.findingId), "Reconciliation finding ids");
403
+ let lastRun = null;
404
+ if (state.lastRun !== null) {
405
+ const run = object(state.lastRun, "Reconciliation last run is invalid.");
406
+ exactKeys(run, ["runId", "ranAt", "counts"], "Reconciliation last run");
407
+ const counts = object(run.counts, "Reconciliation outcome counts are invalid.");
408
+ for (const outcome of RECONCILIATION_OUTCOMES) {
409
+ if (typeof counts[outcome] !== "number" || !Number.isInteger(counts[outcome]) || counts[outcome] < 0)
410
+ throw new ReconciliationError("Reconciliation outcome counts are invalid.");
411
+ }
412
+ lastRun = { runId: identifier(run.runId, "Reconciliation run id"), ranAt: timestamp(run.ranAt, "Reconciliation run time"), counts: counts };
413
+ }
414
+ return { schemaVersion: 1, kind: "ghostapi.reconciliation", lastRun, findings };
415
+ }
416
+ function validateFinding(value) {
417
+ const finding = object(value, "Reconciliation finding is invalid.");
418
+ exactKeys(finding, ["schemaVersion", "kind", "findingId", "tenantId", "actionId", "actionHash", "classification", "detail", "detectedAt", "status", "resolution"], "Reconciliation finding", ["resolution"]);
419
+ if (finding.schemaVersion !== SCHEMA_VERSION || finding.kind !== "ghostapi.reconciliation-finding")
420
+ throw new ReconciliationError("Unsupported reconciliation finding schema.");
421
+ if ((finding.classification !== "drifted" && finding.classification !== "unknown") || (finding.status !== "open" && finding.status !== "resolved"))
422
+ throw new ReconciliationError("Reconciliation finding classification or status is invalid.");
423
+ let resolution;
424
+ if (finding.resolution !== undefined) {
425
+ const value = object(finding.resolution, "Reconciliation finding resolution is invalid.");
426
+ exactKeys(value, ["resolvedAt", "resolvedBy", "reason", "evidenceRef"], "Reconciliation finding resolution", ["evidenceRef"]);
427
+ resolution = {
428
+ resolvedAt: timestamp(value.resolvedAt, "Reconciliation resolution time"),
429
+ resolvedBy: identifier(value.resolvedBy, "Reconciliation resolution operator"),
430
+ reason: text(value.reason, "Reconciliation resolution reason", 300),
431
+ ...(value.evidenceRef === undefined ? {} : { evidenceRef: evidence(value.evidenceRef, "Reconciliation evidence reference") })
432
+ };
433
+ }
434
+ if (finding.status === "resolved" && resolution === undefined)
435
+ throw new ReconciliationError("Resolved reconciliation findings require a resolution.");
436
+ if (finding.status === "open" && resolution !== undefined)
437
+ throw new ReconciliationError("Open reconciliation findings cannot carry a resolution.");
438
+ return {
439
+ schemaVersion: 1,
440
+ kind: "ghostapi.reconciliation-finding",
441
+ findingId: identifier(finding.findingId, "Reconciliation finding id"),
442
+ tenantId: identifier(finding.tenantId, "Reconciliation tenant id"),
443
+ actionId: identifier(finding.actionId, "Reconciliation action id"),
444
+ actionHash: hash(finding.actionHash, "Reconciliation action hash"),
445
+ classification: finding.classification,
446
+ detail: text(finding.detail, "Reconciliation finding detail", 600),
447
+ detectedAt: timestamp(finding.detectedAt, "Reconciliation detection time"),
448
+ status: finding.status,
449
+ ...(resolution === undefined ? {} : { resolution })
450
+ };
451
+ }
452
+ function findFinding(state, findingId) {
453
+ const finding = state.findings.find((candidate) => candidate.findingId === findingId);
454
+ if (finding === undefined)
455
+ throw new ReconciliationError("Reconciliation finding was not found.");
456
+ return finding;
457
+ }
458
+ function validateOperator(value) {
459
+ const operator = object(value, "Reconciliation operator is invalid.");
460
+ exactKeys(operator, ["id", "principalId", "permissions"], "Reconciliation operator");
461
+ const permissions = array(operator.permissions, "Reconciliation operator permissions", 8).map((permission) => {
462
+ if (permission !== "reconciliation.manage" && permission !== "reconciliation.inspect")
463
+ throw new ReconciliationError("Reconciliation operator permission is invalid.");
464
+ return permission;
465
+ });
466
+ unique(permissions, "Reconciliation operator permissions");
467
+ return { id: identifier(operator.id, "Reconciliation operator id"), principalId: identifier(operator.principalId, "Reconciliation operator principal id"), permissions };
468
+ }
469
+ function rateBps(count, total) {
470
+ return total === 0 ? 0 : Math.floor((count * 10_000) / total);
471
+ }
472
+ function identifier(value, label) {
473
+ if (typeof value !== "string" || !IDENTIFIER.test(value) || sanitizeSecretString(value) !== value)
474
+ throw new ReconciliationError(`${label} must be a safe identifier.`);
475
+ return value;
476
+ }
477
+ function hash(value, label) {
478
+ if (typeof value !== "string" || !HASH.test(value))
479
+ throw new ReconciliationError(`${label} must be a SHA-256 hash.`);
480
+ return value;
481
+ }
482
+ function evidence(value, label) {
483
+ if (typeof value !== "string" || value.trim() === "" || value.length > 128 || /[\u0000-\u001f]/.test(value) || sanitizeSecretString(value) !== value)
484
+ throw new ReconciliationError(`${label} must be a bounded non-secret reference.`);
485
+ return value.trim();
486
+ }
487
+ function timestamp(value, label) {
488
+ 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)))
489
+ throw new ReconciliationError(`${label} must be an ISO UTC timestamp.`);
490
+ return value;
491
+ }
492
+ function text(value, label, max) {
493
+ if (typeof value !== "string" || value.trim() === "" || value.length > max || /[\u0000-\u001f]/.test(value) || sanitizeSecretString(value) !== value)
494
+ throw new ReconciliationError(`${label} is invalid.`);
495
+ return value.trim();
496
+ }
497
+ function object(value, message) {
498
+ if (value === null || typeof value !== "object" || Array.isArray(value))
499
+ throw new ReconciliationError(message);
500
+ return value;
501
+ }
502
+ function array(value, label, max) {
503
+ if (!Array.isArray(value) || value.length > max)
504
+ throw new ReconciliationError(`${label} is invalid.`);
505
+ return value;
506
+ }
507
+ function exactKeys(value, keys, label, optional = []) {
508
+ for (const key of Object.keys(value))
509
+ if (!keys.includes(key) || (value[key] === undefined && !optional.includes(key)))
510
+ throw new ReconciliationError(`${label} contains unsupported field: ${key}`);
511
+ }
512
+ function unique(values, label) {
513
+ if (new Set(values).size !== values.length)
514
+ throw new ReconciliationError(`${label} must be unique.`);
515
+ }
516
+ function clone(value) {
517
+ return structuredClone(value);
518
+ }
519
+ function isErrorCode(error, code) {
520
+ return error instanceof Error && "code" in error && error.code === code;
521
+ }
522
+ //# sourceMappingURL=reconciliation.js.map