@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.
- package/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +132 -119
- package/LICENSE +21 -21
- package/PROVENANCE.md +13 -0
- package/README.md +78 -84
- package/SECURITY.md +56 -53
- package/dist/actions/gateway.js +320 -72
- package/dist/actions/gateway.js.map +1 -1
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +1 -1
- package/dist/actions/index.js.map +1 -1
- package/dist/agents/agentPrompt.js +7 -5
- package/dist/agents/agentPrompt.js.map +1 -1
- package/dist/ai/aiClient.js +5 -5
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.js +66 -26
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/genericInference.js +151 -36
- package/dist/ai/genericInference.js.map +1 -1
- package/dist/ai/prompts.js +55 -49
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.js +649 -133
- package/dist/approvals/approvalInbox.js.map +1 -1
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/behavior/behaviorStore.js +34 -14
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.js +14 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +226 -93
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.js +17 -3
- package/dist/cli/openUrl.js.map +1 -1
- package/dist/cli/parser.js +79 -18
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.js +4 -2
- package/dist/config/dataPaths.js.map +1 -1
- package/dist/config/localConfig.js +5 -2
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.js +11 -5
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.js +311 -83
- package/dist/contracts/contract.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/credentials/credentialBroker.js +391 -79
- package/dist/credentials/credentialBroker.js.map +1 -1
- package/dist/credentials/index.d.ts +2 -2
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/index.js.map +1 -1
- package/dist/dashboard/app.js +823 -685
- package/dist/dashboard/dashboard.js.map +1 -1
- package/dist/dashboard/index.html +704 -267
- package/dist/dashboard/styles.css +1040 -349
- package/dist/egress/capabilities.js +55 -25
- package/dist/egress/capabilities.js.map +1 -1
- package/dist/egress/linuxBootstrap.js +47 -11
- package/dist/egress/linuxBootstrap.js.map +1 -1
- package/dist/egress/run.js +170 -31
- package/dist/egress/run.js.map +1 -1
- package/dist/errors/errorEngine.js +15 -3
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/errors/providerErrors.js.map +1 -1
- package/dist/evals/evals.js +304 -78
- package/dist/evals/evals.js.map +1 -1
- package/dist/evals/index.d.ts +2 -2
- package/dist/evals/index.js +1 -1
- package/dist/evals/index.js.map +1 -1
- package/dist/evidence/index.d.ts +2 -2
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/index.js.map +1 -1
- package/dist/evidence/report.d.ts +1 -1
- package/dist/evidence/report.js +263 -79
- package/dist/evidence/report.js.map +1 -1
- package/dist/fault/faultLab.js +24 -9
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +42 -42
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +1 -1
- package/dist/inventory/analysis.js +174 -50
- package/dist/inventory/analysis.js.map +1 -1
- package/dist/inventory/index.d.ts +4 -4
- package/dist/inventory/index.js +3 -3
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/inventoryController.js +362 -115
- package/dist/inventory/inventoryController.js.map +1 -1
- package/dist/inventory/types.js +680 -140
- package/dist/inventory/types.js.map +1 -1
- package/dist/ledger/actionLedger.js +355 -78
- package/dist/ledger/actionLedger.js.map +1 -1
- package/dist/ledger/index.d.ts +2 -2
- package/dist/ledger/index.js +1 -1
- package/dist/ledger/index.js.map +1 -1
- package/dist/mcp/server.js +26 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/policy.js +112 -32
- package/dist/policy/policy.js.map +1 -1
- package/dist/productTelemetry/index.js +71 -20
- package/dist/productTelemetry/index.js.map +1 -1
- package/dist/providers/conformance.js +10 -5
- package/dist/providers/conformance.js.map +1 -1
- package/dist/providers/discord.js +2 -2
- package/dist/providers/generic.js +3 -3
- package/dist/providers/github.js +2 -2
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/packs/resendPack.js +51 -25
- package/dist/providers/packs/resendPack.js.map +1 -1
- package/dist/providers/packs/stripePack.js +590 -121
- package/dist/providers/packs/stripePack.js.map +1 -1
- package/dist/providers/registry.js +4 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/runtime.js +7 -6
- package/dist/providers/runtime.js.map +1 -1
- package/dist/providers/stripeWebhook.js +39 -10
- package/dist/providers/stripeWebhook.js.map +1 -1
- package/dist/providers/twilio.js +2 -2
- package/dist/proxy/cacheKey.js +9 -2
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.js +22 -9
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +57 -22
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.js +4 -2
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.js +211 -49
- package/dist/reliability/costGovernance.js.map +1 -1
- package/dist/reliability/index.d.ts +8 -8
- package/dist/reliability/index.js +4 -4
- package/dist/reliability/index.js.map +1 -1
- package/dist/reliability/reconciliation.js +274 -65
- package/dist/reliability/reconciliation.js.map +1 -1
- package/dist/reliability/runtimeHealth.js +127 -35
- package/dist/reliability/runtimeHealth.js.map +1 -1
- package/dist/reliability/slo.js +257 -50
- package/dist/reliability/slo.js.map +1 -1
- package/dist/report/safetyReport.d.ts +6 -0
- package/dist/report/safetyReport.js +56 -12
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/rules/aiRules.js +5 -5
- package/dist/rules/aiRules.js.map +1 -1
- package/dist/safety/index.d.ts +2 -2
- package/dist/safety/index.js +1 -1
- package/dist/safety/index.js.map +1 -1
- package/dist/safety/safetyController.js +675 -165
- package/dist/safety/safetyController.js.map +1 -1
- package/dist/scenarios/scenarioBundle.js +204 -71
- package/dist/scenarios/scenarioBundle.js.map +1 -1
- package/dist/scenarios/scenarioStore.js +88 -29
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.js +52 -10
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +10 -5
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.js +37 -10
- package/dist/server/accessControl.js.map +1 -1
- package/dist/server/createServer.js +1 -1
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.js +15 -5
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +61 -19
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.js +1 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/sdkDetector.js +7 -2
- package/dist/setup/sdkDetector.js.map +1 -1
- package/dist/setup/setupGenerator.js +136 -48
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateExtractor.js +3 -1
- package/dist/state/stateExtractor.js.map +1 -1
- package/dist/state/stateResolver.js +10 -7
- package/dist/state/stateResolver.js.map +1 -1
- package/dist/state/stateStore.js +13 -4
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.js +12 -4
- package/dist/storage/fileStore.js.map +1 -1
- package/dist/teamControl/controlPlane.js +846 -174
- package/dist/teamControl/controlPlane.js.map +1 -1
- package/dist/teamControl/deployment.js +19 -5
- package/dist/teamControl/deployment.js.map +1 -1
- package/dist/teamControl/index.d.ts +4 -4
- package/dist/teamControl/index.js +2 -2
- package/dist/teamControl/index.js.map +1 -1
- package/dist/tests/testGenerator.js +15 -7
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -2
- package/dist/trust/index.js +1 -1
- package/dist/trust/index.js.map +1 -1
- package/dist/trust/trustLadder.js +309 -63
- package/dist/trust/trustLadder.js.map +1 -1
- package/dist/worlds/index.d.ts +2 -2
- package/dist/worlds/index.js +1 -1
- package/dist/worlds/index.js.map +1 -1
- package/dist/worlds/worlds.js +364 -65
- package/dist/worlds/worlds.js.map +1 -1
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -49
- package/docs/github-actions.md +84 -84
- package/docs/mcp.md +58 -52
- package/docs/policy.md +63 -61
- package/docs/providers/authoring-packs.md +180 -175
- package/docs/providers/stripe-core-pack.md +84 -82
- package/docs/releases/README.md +13 -0
- package/docs/security/action-gateway-threat-model.md +34 -34
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
- package/docs/security/approval-inbox-threat-model.md +35 -35
- package/docs/security/credential-broker-threat-model.md +52 -52
- package/docs/security/egress-threat-model.md +126 -126
- package/docs/security/inventory-threat-model.md +104 -104
- package/docs/security/kill-switch-budgets-threat-model.md +37 -37
- package/docs/security/reliability-threat-model.md +111 -111
- package/docs/security/trust-ladder-threat-model.md +33 -33
- package/docs/telemetry.md +14 -0
- package/docs/usage.md +680 -571
- package/examples/README.md +10 -10
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -21
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
- package/examples/ci-smoke/package.json +9 -9
- package/examples/ci-smoke/production-egress.mjs +4 -2
- package/examples/ci-smoke/safe.mjs +21 -17
- package/examples/evals/README.md +24 -24
- package/examples/evals/retry-after.eval.json +71 -52
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +38 -35
- package/examples/openai-streaming/README.md +16 -16
- package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
- package/examples/policy/ghostapi.policy.yaml +27 -27
- package/examples/record-replay/README.md +19 -19
- package/examples/record-replay/replay-requests.json +22 -22
- package/examples/record-replay/stripe-checkout.har.json +30 -30
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -35
- package/examples/stripe-node/checkout-flow.mjs +87 -61
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -19
- package/examples/worlds/subscription-recovery.mjs +21 -12
- package/package.json +28 -6
- package/dist/landing/assets/index-BXR41fAD.css +0 -1
- package/dist/landing/assets/index-CxWsopzP.css +0 -1
- package/dist/landing/assets/index-D8_ggMRE.js +0 -55
- package/dist/landing/assets/index-DZlpyhqd.js +0 -55
- package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
- package/dist/landing/index.html +0 -15
- package/dist/landing/landing.d.ts +0 -3
- package/dist/landing/landing.js +0 -12
- package/dist/landing/landing.js.map +0 -1
- package/docs/commercial/README.md +0 -29
- package/docs/commercial/data-inventory.md +0 -36
- package/docs/commercial/manual-invoicing.md +0 -44
- package/docs/commercial/metrics.md +0 -51
- package/docs/commercial/pricing.md +0 -103
- package/docs/commercial/privacy-policy-draft.md +0 -41
- package/docs/commercial/terms-topics-for-counsel.md +0 -51
- package/docs/design-partners/README.md +0 -52
- package/docs/design-partners/demo-narratives.md +0 -33
- package/docs/design-partners/design-partner-offer.md +0 -39
- package/docs/design-partners/discovery-questions.md +0 -28
- package/docs/design-partners/feedback-capture-template.md +0 -48
- package/docs/design-partners/icp-one-pager.md +0 -43
- package/docs/design-partners/onboarding-checklist.md +0 -33
- package/docs/design-partners/pilot-success-criteria.md +0 -27
- package/docs/design-partners/pricing-interview-script.md +0 -18
- package/docs/design-partners/telemetry-plan.md +0 -48
- package/docs/development/baseline.md +0 -114
- package/docs/development/onboarding-smoke.md +0 -37
- package/docs/development/verification-0.1.8.md +0 -25
- package/docs/enterprise-product-roadmap-ru.md +0 -1879
- package/docs/fundraising/README.md +0 -87
- package/docs/fundraising/data-room-checklist.md +0 -72
- package/docs/fundraising/demo-script.md +0 -52
- package/docs/fundraising/design-partners-50.md +0 -74
- package/docs/fundraising/launch-posts.md +0 -37
- package/docs/fundraising/metrics-and-evidence.md +0 -30
- package/docs/fundraising/roadmap-12-month.md +0 -25
- package/docs/fundraising/technical-due-diligence-index.md +0 -37
- package/docs/fundraising/yc-application.md +0 -25
- package/docs/hosted-pilot.md +0 -111
- package/docs/operations/disaster-recovery-runbook.md +0 -81
- package/docs/operations/kill-switch-runbook.md +0 -32
- package/docs/release-checklist.md +0 -83
- package/docs/release-migration-and-rollback.md +0 -63
- package/docs/release-readiness.md +0 -39
- 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,
|
|
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 = [
|
|
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 =
|
|
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 &&
|
|
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({
|
|
91
|
-
|
|
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({
|
|
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({
|
|
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 {
|
|
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 = {
|
|
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 = {
|
|
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 =
|
|
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
|
|
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 {
|
|
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: {
|
|
215
|
-
|
|
216
|
-
|
|
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 {
|
|
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 &&
|
|
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 = [
|
|
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 {
|
|
360
|
+
return {
|
|
361
|
+
outcome: "committed",
|
|
362
|
+
retrySafe: false,
|
|
363
|
+
reconciledFromProvider: false,
|
|
364
|
+
};
|
|
281
365
|
if (providerPresent === false)
|
|
282
|
-
return {
|
|
283
|
-
|
|
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 {
|
|
290
|
-
|
|
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 {
|
|
393
|
+
return {
|
|
394
|
+
outcome: "drifted",
|
|
395
|
+
retrySafe: false,
|
|
396
|
+
reconciledFromProvider: false,
|
|
397
|
+
};
|
|
294
398
|
if (providerPresent === false)
|
|
295
|
-
return {
|
|
296
|
-
|
|
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 {
|
|
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":
|
|
304
|
-
|
|
305
|
-
case "
|
|
306
|
-
|
|
307
|
-
case "
|
|
308
|
-
|
|
309
|
-
case "
|
|
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
|
|
319
|
-
|
|
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
|
|
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
|
|
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: {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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 = {
|
|
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 {
|
|
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" ||
|
|
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 = {
|
|
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 {
|
|
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, [
|
|
419
|
-
|
|
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" &&
|
|
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
|
|
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" &&
|
|
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 {
|
|
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" ||
|
|
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" ||
|
|
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" ||
|
|
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" ||
|
|
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) ||
|
|
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) {
|