@yiaany/ghostapi 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -1,5 +1,5 @@
1
- export { LocalSloController, SloError, SLO_LATENCY_METRICS, SLO_METRICS, createDisabledSloOperatorAuthorizer, createLocalSloController, createSloRecordIdentity, createTestSloOperatorAuthorizer, examplePilotSloTargets, formatSloReport } from "./slo.js";
2
- export { LocalReconciliationService, ReconciliationError, RECONCILIATION_OUTCOMES, createDisabledReconciliationOperatorAuthorizer, createLocalReconciliationService, createTestReconciliationOperatorAuthorizer, createWorldStateReconciliationProvider, formatReconciliationReport } from "./reconciliation.js";
3
- export { CostGovernanceError, COST_AMOUNT_KEYS, COST_DIMENSIONS, LocalCostGovernance, createDisabledCostOperatorAuthorizer, createLocalCostGovernance, createTestCostOperatorAuthorizer, formatCostReport } from "./costGovernance.js";
4
- export { RuntimeHealthError, backupRuntime, checkRuntimeHealth, formatRuntimeHealth, restoreRuntimeBackup } from "./runtimeHealth.js";
1
+ export { LocalSloController, SloError, SLO_LATENCY_METRICS, SLO_METRICS, createDisabledSloOperatorAuthorizer, createLocalSloController, createSloRecordIdentity, createTestSloOperatorAuthorizer, examplePilotSloTargets, formatSloReport, } from "./slo.js";
2
+ export { LocalReconciliationService, ReconciliationError, RECONCILIATION_OUTCOMES, createDisabledReconciliationOperatorAuthorizer, createLocalReconciliationService, createTestReconciliationOperatorAuthorizer, createWorldStateReconciliationProvider, formatReconciliationReport, } from "./reconciliation.js";
3
+ export { CostGovernanceError, COST_AMOUNT_KEYS, COST_DIMENSIONS, LocalCostGovernance, createDisabledCostOperatorAuthorizer, createLocalCostGovernance, createTestCostOperatorAuthorizer, formatCostReport, } from "./costGovernance.js";
4
+ export { RuntimeHealthError, backupRuntime, checkRuntimeHealth, formatRuntimeHealth, restoreRuntimeBackup, } from "./runtimeHealth.js";
5
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/reliability/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,mBAAmB,EAAE,WAAW,EAAE,mCAAmC,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,+BAA+B,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE5P,OAAO,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,8CAA8C,EAAE,gCAAgC,EAAE,0CAA0C,EAAE,sCAAsC,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjT,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,eAAe,EAAE,mBAAmB,EAAE,oCAAoC,EAAE,yBAAyB,EAAE,gCAAgC,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvO,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/reliability/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,QAAQ,EACR,mBAAmB,EACnB,WAAW,EACX,mCAAmC,EACnC,wBAAwB,EACxB,uBAAuB,EACvB,+BAA+B,EAC/B,sBAAsB,EACtB,eAAe,GAChB,MAAM,UAAU,CAAC;AAclB,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,uBAAuB,EACvB,8CAA8C,EAC9C,gCAAgC,EAChC,0CAA0C,EAC1C,sCAAsC,EACtC,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAgB7B,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,oCAAoC,EACpC,yBAAyB,EACzB,gCAAgC,EAChC,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAmB7B,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC"}
@@ -3,16 +3,22 @@ import { lstat, readFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  import { getDataPaths } from "../config/dataPaths.js";
5
5
  import { sanitizeSecretString } from "../security/secrets.js";
6
- import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
6
+ import { atomicWriteJson, ensurePrivateDirectory, withFileLock, } from "../storage/fileStore.js";
7
7
  import { inspectWorld, SyntheticWorldError } from "../worlds/index.js";
8
- import { createSloRecordIdentity } from "./slo.js";
8
+ import { createSloRecordIdentity, } from "./slo.js";
9
9
  const SCHEMA_VERSION = 1;
10
10
  const KIND = "ghostapi.reconciliation";
11
11
  const MAX_STORE_BYTES = 4 * 1024 * 1024;
12
12
  const MAX_FINDINGS = 1_000;
13
13
  const IDENTIFIER = /^[a-z0-9][a-z0-9._-]{0,127}$/;
14
14
  const HASH = /^[a-f0-9]{64}$/;
15
- export const RECONCILIATION_OUTCOMES = ["committed", "not_committed", "unknown", "compensated", "drifted"];
15
+ export const RECONCILIATION_OUTCOMES = [
16
+ "committed",
17
+ "not_committed",
18
+ "unknown",
19
+ "compensated",
20
+ "drifted",
21
+ ];
16
22
  export class ReconciliationError extends Error {
17
23
  constructor(message) {
18
24
  super(message);
@@ -39,7 +45,9 @@ export class LocalReconciliationService {
39
45
  this.provider = options.provider;
40
46
  this.sloController = options.sloController;
41
47
  this.sloRecordIdentity = createSloRecordIdentity();
42
- this.operatorAuthorizer = options.operatorAuthorizer ?? createDisabledReconciliationOperatorAuthorizer();
48
+ this.operatorAuthorizer =
49
+ options.operatorAuthorizer ??
50
+ createDisabledReconciliationOperatorAuthorizer();
43
51
  }
44
52
  async runReconciliation(input) {
45
53
  await this.authorize(input.identity, "reconciliation.manage");
@@ -69,7 +77,8 @@ export class LocalReconciliationService {
69
77
  let opened = 0;
70
78
  for (const result of results) {
71
79
  if (result.outcome === "drifted" || result.outcome === "unknown") {
72
- const alreadyOpen = state.findings.some((finding) => finding.actionHash === result.actionHash && finding.status === "open");
80
+ const alreadyOpen = state.findings.some((finding) => finding.actionHash === result.actionHash &&
81
+ finding.status === "open");
73
82
  if (!alreadyOpen) {
74
83
  if (state.findings.length >= MAX_FINDINGS)
75
84
  throw new ReconciliationError("Reconciliation findings limit was reached.");
@@ -87,17 +96,53 @@ export class LocalReconciliationService {
87
96
  const measurement = measurements[index];
88
97
  if (!measurement.executed)
89
98
  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 } });
99
+ samples.push({
100
+ metric: "duplicate_prevention",
101
+ ok: measurement.duplicateOk,
102
+ runId,
103
+ actionId: result.actionId,
104
+ labels: { tenantId },
105
+ });
106
+ samples.push({
107
+ metric: "availability",
108
+ ok: measurement.availabilityOk,
109
+ runId,
110
+ actionId: result.actionId,
111
+ labels: { tenantId },
112
+ });
92
113
  if (measurement.verificationOk !== null)
93
- samples.push({ metric: "receipt_verification", ok: measurement.verificationOk, runId, actionId: result.actionId, labels: { tenantId } });
114
+ samples.push({
115
+ metric: "receipt_verification",
116
+ ok: measurement.verificationOk,
117
+ runId,
118
+ actionId: result.actionId,
119
+ labels: { tenantId },
120
+ });
94
121
  if (measurement.latencyMs !== null)
95
- samples.push({ metric: "execution_latency", ok: true, durationMs: measurement.latencyMs, runId, actionId: result.actionId, labels: { tenantId } });
122
+ samples.push({
123
+ metric: "execution_latency",
124
+ ok: true,
125
+ durationMs: measurement.latencyMs,
126
+ runId,
127
+ actionId: result.actionId,
128
+ labels: { tenantId },
129
+ });
96
130
  }
97
131
  if (samples.length > 0)
98
132
  await this.sloController.recordSamples(samples, this.sloRecordIdentity);
99
133
  }
100
- return { schemaVersion: 1, kind: "ghostapi.reconciliation-report", runId, tenantId, ranAt: now, integrity: "valid", actions: results, counts, sli, findingsOpened };
134
+ return {
135
+ schemaVersion: 1,
136
+ kind: "ghostapi.reconciliation-report",
137
+ runId,
138
+ tenantId,
139
+ ranAt: now,
140
+ integrity: "valid",
141
+ actions: results,
142
+ counts,
143
+ sli,
144
+ findingsOpened,
145
+ };
101
146
  }
102
147
  async listFindings(input) {
103
148
  await this.authorize(input.identity, "reconciliation.inspect");
@@ -115,7 +160,11 @@ export class LocalReconciliationService {
115
160
  if (finding.status === "resolved")
116
161
  throw new ReconciliationError("Reconciliation finding is already resolved.");
117
162
  finding.status = "resolved";
118
- finding.resolution = { resolvedAt: this.timestamp(), resolvedBy: operator.principalId, reason };
163
+ finding.resolution = {
164
+ resolvedAt: this.timestamp(),
165
+ resolvedBy: operator.principalId,
166
+ reason,
167
+ };
119
168
  return clone(finding);
120
169
  });
121
170
  }
@@ -131,7 +180,12 @@ export class LocalReconciliationService {
131
180
  if (finding.status === "resolved")
132
181
  throw new ReconciliationError("Reconciliation finding is already resolved.");
133
182
  finding.status = "resolved";
134
- finding.resolution = { resolvedAt: this.timestamp(), resolvedBy: operator.principalId, reason, evidenceRef };
183
+ finding.resolution = {
184
+ resolvedAt: this.timestamp(),
185
+ resolvedBy: operator.principalId,
186
+ reason,
187
+ evidenceRef,
188
+ };
135
189
  return clone(finding);
136
190
  });
137
191
  }
@@ -143,7 +197,9 @@ export class LocalReconciliationService {
143
197
  let providerPresent = null;
144
198
  if (worldId !== null) {
145
199
  const world = await this.provider.read({ worldId });
146
- providerPresent = world !== null && world.receipts.some((receipt) => receipt.actionId === actionId);
200
+ providerPresent =
201
+ world !== null &&
202
+ world.receipts.some((receipt) => receipt.actionId === actionId);
147
203
  }
148
204
  const decided = decideOutcome(intent, providerPresent);
149
205
  if (providerPresent === null)
@@ -151,7 +207,9 @@ export class LocalReconciliationService {
151
207
  if (decided.reconciledFromProvider)
152
208
  reasons.push("ledger outcome was incomplete; provider evidence confirms the commitment");
153
209
  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");
210
+ reasons.push(providerPresent
211
+ ? "provider has a receipt the ledger does not record"
212
+ : "ledger records a commitment the provider does not confirm");
155
213
  return {
156
214
  tenantId,
157
215
  actionId,
@@ -162,7 +220,7 @@ export class LocalReconciliationService {
162
220
  outcome: decided.outcome,
163
221
  retrySafe: decided.retrySafe,
164
222
  reconciledFromProvider: decided.reconciledFromProvider,
165
- reasons
223
+ reasons,
166
224
  };
167
225
  }
168
226
  async authorize(identity, permission) {
@@ -206,14 +264,29 @@ export class LocalReconciliationService {
206
264
  }
207
265
  }
208
266
  export function createDisabledReconciliationOperatorAuthorizer() {
209
- return { async authenticate() { throw new ReconciliationError("Reconciliation operator authorization is not configured."); } };
267
+ return {
268
+ async authenticate() {
269
+ throw new ReconciliationError("Reconciliation operator authorization is not configured.");
270
+ },
271
+ };
210
272
  }
211
273
  export function createTestReconciliationOperatorAuthorizer() {
212
274
  const issued = new WeakSet();
213
275
  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; }
276
+ authorizer: {
277
+ async authenticate(identity) {
278
+ if (identity === null ||
279
+ typeof identity !== "object" ||
280
+ !issued.has(identity))
281
+ throw new ReconciliationError("Reconciliation operator identity is not authenticated.");
282
+ return validateOperator(identity);
283
+ },
284
+ },
285
+ issue(input) {
286
+ const operator = Object.freeze(validateOperator(input));
287
+ issued.add(operator);
288
+ return operator;
289
+ },
217
290
  };
218
291
  }
219
292
  export function createWorldStateReconciliationProvider(resolveWorld) {
@@ -224,18 +297,25 @@ export function createWorldStateReconciliationProvider(resolveWorld) {
224
297
  async read(input) {
225
298
  try {
226
299
  const world = await inspectWorld(input.worldId);
227
- return { receipts: world.state.receipts.map((receipt) => ({ actionId: receipt.actionId })) };
300
+ return {
301
+ receipts: world.state.receipts.map((receipt) => ({
302
+ actionId: receipt.actionId,
303
+ })),
304
+ };
228
305
  }
229
306
  catch (error) {
230
- if (error instanceof SyntheticWorldError && error.code === "WORLD_NOT_FOUND")
307
+ if (error instanceof SyntheticWorldError &&
308
+ error.code === "WORLD_NOT_FOUND")
231
309
  return null;
232
310
  throw error;
233
311
  }
234
- }
312
+ },
235
313
  };
236
314
  }
237
315
  export function formatReconciliationReport(report) {
238
- const lines = [`Reconciliation run ${report.runId} for tenant ${report.tenantId} at ${report.ranAt}`];
316
+ const lines = [
317
+ `Reconciliation run ${report.runId} for tenant ${report.tenantId} at ${report.ranAt}`,
318
+ ];
239
319
  lines.push(` ledger integrity: ${report.integrity}`);
240
320
  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
321
  lines.push(` findings opened: ${report.findingsOpened}`);
@@ -277,36 +357,79 @@ function decideOutcome(intent, providerPresent) {
277
357
  switch (intent) {
278
358
  case "committed":
279
359
  if (providerPresent === true)
280
- return { outcome: "committed", retrySafe: false, reconciledFromProvider: false };
360
+ return {
361
+ outcome: "committed",
362
+ retrySafe: false,
363
+ reconciledFromProvider: false,
364
+ };
281
365
  if (providerPresent === false)
282
- return { outcome: "drifted", retrySafe: false, reconciledFromProvider: false };
283
- return { outcome: "unknown", retrySafe: false, reconciledFromProvider: false };
366
+ return {
367
+ outcome: "drifted",
368
+ retrySafe: false,
369
+ reconciledFromProvider: false,
370
+ };
371
+ return {
372
+ outcome: "unknown",
373
+ retrySafe: false,
374
+ reconciledFromProvider: false,
375
+ };
284
376
  case "committed_unverified":
285
377
  case "ambiguous":
286
378
  case "failed":
287
379
  case "attempted":
288
380
  if (providerPresent === true)
289
- return { outcome: "committed", retrySafe: false, reconciledFromProvider: true };
290
- return { outcome: "unknown", retrySafe: false, reconciledFromProvider: false };
381
+ return {
382
+ outcome: "committed",
383
+ retrySafe: false,
384
+ reconciledFromProvider: true,
385
+ };
386
+ return {
387
+ outcome: "unknown",
388
+ retrySafe: false,
389
+ reconciledFromProvider: false,
390
+ };
291
391
  case "not_committed":
292
392
  if (providerPresent === true)
293
- return { outcome: "drifted", retrySafe: false, reconciledFromProvider: false };
393
+ return {
394
+ outcome: "drifted",
395
+ retrySafe: false,
396
+ reconciledFromProvider: false,
397
+ };
294
398
  if (providerPresent === false)
295
- return { outcome: "not_committed", retrySafe: true, reconciledFromProvider: false };
296
- return { outcome: "unknown", retrySafe: false, reconciledFromProvider: false };
399
+ return {
400
+ outcome: "not_committed",
401
+ retrySafe: true,
402
+ reconciledFromProvider: false,
403
+ };
404
+ return {
405
+ outcome: "unknown",
406
+ retrySafe: false,
407
+ reconciledFromProvider: false,
408
+ };
297
409
  case "compensated":
298
- return { outcome: "compensated", retrySafe: false, reconciledFromProvider: false };
410
+ return {
411
+ outcome: "compensated",
412
+ retrySafe: false,
413
+ reconciledFromProvider: false,
414
+ };
299
415
  }
300
416
  }
301
417
  function intentReasons(intent) {
302
418
  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"];
419
+ case "committed":
420
+ return ["ledger shows a verified provider receipt"];
421
+ case "committed_unverified":
422
+ return ["ledger records a provider receipt without a verified proof"];
423
+ case "ambiguous":
424
+ return ["ledger records an ambiguous execution outcome"];
425
+ case "failed":
426
+ return ["ledger records a failed execution outcome"];
427
+ case "attempted":
428
+ return ["ledger records an execution attempt without a receipt"];
429
+ case "not_committed":
430
+ return ["ledger never reached the provider"];
431
+ case "compensated":
432
+ return ["ledger records an attempted compensation"];
310
433
  }
311
434
  }
312
435
  function measureAction(timeline, outcome) {
@@ -315,11 +438,19 @@ function measureAction(timeline, outcome) {
315
438
  const attempts = entries(timeline, "execution_attempt");
316
439
  const executed = attempts.length > 0 || providerReceipts.length > 0;
317
440
  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";
441
+ const verificationOk = providerReceipts.length === 0
442
+ ? null
443
+ : verifications.at(-1)?.data.status === "verified";
444
+ const availabilityOk = !executed
445
+ ? false
446
+ : outcome === "committed" ||
447
+ outcome === "not_committed" ||
448
+ outcome === "compensated";
320
449
  const receipt = providerReceipts[0];
321
450
  const proof = verifications.at(-1);
322
- const latencyMs = receipt !== undefined && proof !== undefined ? Math.max(0, Date.parse(proof.timestamp) - Date.parse(receipt.timestamp)) : null;
451
+ const latencyMs = receipt !== undefined && proof !== undefined
452
+ ? Math.max(0, Date.parse(proof.timestamp) - Date.parse(receipt.timestamp))
453
+ : null;
323
454
  return { executed, duplicateOk, verificationOk, availabilityOk, latencyMs };
324
455
  }
325
456
  function aggregateSli(results, measurements) {
@@ -346,16 +477,41 @@ function aggregateSli(results, measurements) {
346
477
  latencies.push(measurement.latencyMs);
347
478
  }
348
479
  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))];
480
+ const percentile = (value) => ordered.length === 0
481
+ ? 0
482
+ : ordered[Math.min(ordered.length - 1, Math.max(0, Math.ceil((value / 100) * ordered.length) - 1))];
350
483
  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" }
484
+ duplicatePrevention: {
485
+ ...duplicatePrevention,
486
+ okRateBps: rateBps(duplicatePrevention.ok, duplicatePrevention.measured),
487
+ },
488
+ receiptVerification: {
489
+ ...receiptVerification,
490
+ okRateBps: rateBps(receiptVerification.ok, receiptVerification.measured),
491
+ },
492
+ availability: {
493
+ ...availability,
494
+ okRateBps: rateBps(availability.ok, availability.measured),
495
+ },
496
+ executionLatency: {
497
+ measured: latencies.length,
498
+ avgMs: latencies.length === 0
499
+ ? 0
500
+ : latencies.reduce((sum, value) => sum + value, 0) / latencies.length,
501
+ p95Ms: percentile(95),
502
+ p99Ms: percentile(99),
503
+ basis: "ledger_record_interval",
504
+ },
355
505
  };
356
506
  }
357
507
  function countOutcomes(results) {
358
- const counts = { committed: 0, not_committed: 0, unknown: 0, compensated: 0, drifted: 0 };
508
+ const counts = {
509
+ committed: 0,
510
+ not_committed: 0,
511
+ unknown: 0,
512
+ compensated: 0,
513
+ drifted: 0,
514
+ };
359
515
  for (const result of results)
360
516
  counts[result.outcome] += 1;
361
517
  return counts;
@@ -372,7 +528,7 @@ function makeFinding(result, now) {
372
528
  classification,
373
529
  detail: result.reasons.join("; "),
374
530
  detectedAt: now,
375
- status: "open"
531
+ status: "open",
376
532
  };
377
533
  }
378
534
  function groupByAction(entries) {
@@ -391,7 +547,12 @@ function lastEntry(timeline, stage) {
391
547
  return entries(timeline, stage).at(-1);
392
548
  }
393
549
  function emptyState() {
394
- return { schemaVersion: 1, kind: "ghostapi.reconciliation", lastRun: null, findings: [] };
550
+ return {
551
+ schemaVersion: 1,
552
+ kind: "ghostapi.reconciliation",
553
+ lastRun: null,
554
+ findings: [],
555
+ };
395
556
  }
396
557
  function validateState(value) {
397
558
  const state = object(value, "Reconciliation store must be an object.");
@@ -406,19 +567,45 @@ function validateState(value) {
406
567
  exactKeys(run, ["runId", "ranAt", "counts"], "Reconciliation last run");
407
568
  const counts = object(run.counts, "Reconciliation outcome counts are invalid.");
408
569
  for (const outcome of RECONCILIATION_OUTCOMES) {
409
- if (typeof counts[outcome] !== "number" || !Number.isInteger(counts[outcome]) || counts[outcome] < 0)
570
+ if (typeof counts[outcome] !== "number" ||
571
+ !Number.isInteger(counts[outcome]) ||
572
+ counts[outcome] < 0)
410
573
  throw new ReconciliationError("Reconciliation outcome counts are invalid.");
411
574
  }
412
- lastRun = { runId: identifier(run.runId, "Reconciliation run id"), ranAt: timestamp(run.ranAt, "Reconciliation run time"), counts: counts };
575
+ lastRun = {
576
+ runId: identifier(run.runId, "Reconciliation run id"),
577
+ ranAt: timestamp(run.ranAt, "Reconciliation run time"),
578
+ counts: counts,
579
+ };
413
580
  }
414
- return { schemaVersion: 1, kind: "ghostapi.reconciliation", lastRun, findings };
581
+ return {
582
+ schemaVersion: 1,
583
+ kind: "ghostapi.reconciliation",
584
+ lastRun,
585
+ findings,
586
+ };
415
587
  }
416
588
  function validateFinding(value) {
417
589
  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")
590
+ exactKeys(finding, [
591
+ "schemaVersion",
592
+ "kind",
593
+ "findingId",
594
+ "tenantId",
595
+ "actionId",
596
+ "actionHash",
597
+ "classification",
598
+ "detail",
599
+ "detectedAt",
600
+ "status",
601
+ "resolution",
602
+ ], "Reconciliation finding", ["resolution"]);
603
+ if (finding.schemaVersion !== SCHEMA_VERSION ||
604
+ finding.kind !== "ghostapi.reconciliation-finding")
420
605
  throw new ReconciliationError("Unsupported reconciliation finding schema.");
421
- if ((finding.classification !== "drifted" && finding.classification !== "unknown") || (finding.status !== "open" && finding.status !== "resolved"))
606
+ if ((finding.classification !== "drifted" &&
607
+ finding.classification !== "unknown") ||
608
+ (finding.status !== "open" && finding.status !== "resolved"))
422
609
  throw new ReconciliationError("Reconciliation finding classification or status is invalid.");
423
610
  let resolution;
424
611
  if (finding.resolution !== undefined) {
@@ -428,7 +615,11 @@ function validateFinding(value) {
428
615
  resolvedAt: timestamp(value.resolvedAt, "Reconciliation resolution time"),
429
616
  resolvedBy: identifier(value.resolvedBy, "Reconciliation resolution operator"),
430
617
  reason: text(value.reason, "Reconciliation resolution reason", 300),
431
- ...(value.evidenceRef === undefined ? {} : { evidenceRef: evidence(value.evidenceRef, "Reconciliation evidence reference") })
618
+ ...(value.evidenceRef === undefined
619
+ ? {}
620
+ : {
621
+ evidenceRef: evidence(value.evidenceRef, "Reconciliation evidence reference"),
622
+ }),
432
623
  };
433
624
  }
434
625
  if (finding.status === "resolved" && resolution === undefined)
@@ -446,7 +637,7 @@ function validateFinding(value) {
446
637
  detail: text(finding.detail, "Reconciliation finding detail", 600),
447
638
  detectedAt: timestamp(finding.detectedAt, "Reconciliation detection time"),
448
639
  status: finding.status,
449
- ...(resolution === undefined ? {} : { resolution })
640
+ ...(resolution === undefined ? {} : { resolution }),
450
641
  };
451
642
  }
452
643
  function findFinding(state, findingId) {
@@ -459,18 +650,25 @@ function validateOperator(value) {
459
650
  const operator = object(value, "Reconciliation operator is invalid.");
460
651
  exactKeys(operator, ["id", "principalId", "permissions"], "Reconciliation operator");
461
652
  const permissions = array(operator.permissions, "Reconciliation operator permissions", 8).map((permission) => {
462
- if (permission !== "reconciliation.manage" && permission !== "reconciliation.inspect")
653
+ if (permission !== "reconciliation.manage" &&
654
+ permission !== "reconciliation.inspect")
463
655
  throw new ReconciliationError("Reconciliation operator permission is invalid.");
464
656
  return permission;
465
657
  });
466
658
  unique(permissions, "Reconciliation operator permissions");
467
- return { id: identifier(operator.id, "Reconciliation operator id"), principalId: identifier(operator.principalId, "Reconciliation operator principal id"), permissions };
659
+ return {
660
+ id: identifier(operator.id, "Reconciliation operator id"),
661
+ principalId: identifier(operator.principalId, "Reconciliation operator principal id"),
662
+ permissions,
663
+ };
468
664
  }
469
665
  function rateBps(count, total) {
470
666
  return total === 0 ? 0 : Math.floor((count * 10_000) / total);
471
667
  }
472
668
  function identifier(value, label) {
473
- if (typeof value !== "string" || !IDENTIFIER.test(value) || sanitizeSecretString(value) !== value)
669
+ if (typeof value !== "string" ||
670
+ !IDENTIFIER.test(value) ||
671
+ sanitizeSecretString(value) !== value)
474
672
  throw new ReconciliationError(`${label} must be a safe identifier.`);
475
673
  return value;
476
674
  }
@@ -480,17 +678,27 @@ function hash(value, label) {
480
678
  return value;
481
679
  }
482
680
  function evidence(value, label) {
483
- if (typeof value !== "string" || value.trim() === "" || value.length > 128 || /[\u0000-\u001f]/.test(value) || sanitizeSecretString(value) !== value)
681
+ if (typeof value !== "string" ||
682
+ value.trim() === "" ||
683
+ value.length > 128 ||
684
+ /[\u0000-\u001f]/.test(value) ||
685
+ sanitizeSecretString(value) !== value)
484
686
  throw new ReconciliationError(`${label} must be a bounded non-secret reference.`);
485
687
  return value.trim();
486
688
  }
487
689
  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)))
690
+ if (typeof value !== "string" ||
691
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
692
+ !Number.isFinite(Date.parse(value)))
489
693
  throw new ReconciliationError(`${label} must be an ISO UTC timestamp.`);
490
694
  return value;
491
695
  }
492
696
  function text(value, label, max) {
493
- if (typeof value !== "string" || value.trim() === "" || value.length > max || /[\u0000-\u001f]/.test(value) || sanitizeSecretString(value) !== value)
697
+ if (typeof value !== "string" ||
698
+ value.trim() === "" ||
699
+ value.length > max ||
700
+ /[\u0000-\u001f]/.test(value) ||
701
+ sanitizeSecretString(value) !== value)
494
702
  throw new ReconciliationError(`${label} is invalid.`);
495
703
  return value.trim();
496
704
  }
@@ -506,7 +714,8 @@ function array(value, label, max) {
506
714
  }
507
715
  function exactKeys(value, keys, label, optional = []) {
508
716
  for (const key of Object.keys(value))
509
- if (!keys.includes(key) || (value[key] === undefined && !optional.includes(key)))
717
+ if (!keys.includes(key) ||
718
+ (value[key] === undefined && !optional.includes(key)))
510
719
  throw new ReconciliationError(`${label} contains unsupported field: ${key}`);
511
720
  }
512
721
  function unique(values, label) {