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