@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
package/dist/actions/gateway.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, KeyObject, randomUUID, sign, verify } from "node:crypto";
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, KeyObject, randomUUID, sign, verify, } 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 { createLocalSafetyController } from "../safety/index.js";
|
|
5
|
+
import { createLocalSafetyController, } from "../safety/index.js";
|
|
6
6
|
import { sanitizeSecretString } from "../security/secrets.js";
|
|
7
|
-
import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
|
|
8
|
-
import { inspectWorld, runSubscriptionFailureWorkflow } from "../worlds/index.js";
|
|
7
|
+
import { atomicWriteJson, ensurePrivateDirectory, withFileLock, } from "../storage/fileStore.js";
|
|
8
|
+
import { inspectWorld, runSubscriptionFailureWorkflow, } from "../worlds/index.js";
|
|
9
9
|
const ACTION_SCHEMA_VERSION = 1;
|
|
10
10
|
const ACTION_KIND = "ghostapi.action";
|
|
11
11
|
const APPROVAL_KIND = "ghostapi.action-approval";
|
|
@@ -31,8 +31,27 @@ export function actionApprovalHash(value) {
|
|
|
31
31
|
}
|
|
32
32
|
export function validateActionEnvelope(value) {
|
|
33
33
|
const action = object(value, "Action envelope must be an object.");
|
|
34
|
-
exactKeys(action, [
|
|
35
|
-
|
|
34
|
+
exactKeys(action, [
|
|
35
|
+
"schemaVersion",
|
|
36
|
+
"kind",
|
|
37
|
+
"actionId",
|
|
38
|
+
"idempotencyKey",
|
|
39
|
+
"actor",
|
|
40
|
+
"project",
|
|
41
|
+
"provider",
|
|
42
|
+
"operation",
|
|
43
|
+
"resource",
|
|
44
|
+
"arguments",
|
|
45
|
+
"expectedSideEffects",
|
|
46
|
+
"riskClass",
|
|
47
|
+
"reversibility",
|
|
48
|
+
"policy",
|
|
49
|
+
"evidence",
|
|
50
|
+
"expiresAt",
|
|
51
|
+
"nonce",
|
|
52
|
+
], "Action envelope");
|
|
53
|
+
if (action.schemaVersion !== ACTION_SCHEMA_VERSION ||
|
|
54
|
+
action.kind !== ACTION_KIND)
|
|
36
55
|
throw new ActionGatewayError("Unsupported action envelope schema.");
|
|
37
56
|
const actor = object(action.actor, "Action actor is invalid.");
|
|
38
57
|
exactKeys(actor, ["id", "workloadId", "type"], "Action actor");
|
|
@@ -52,14 +71,32 @@ export function validateActionEnvelope(value) {
|
|
|
52
71
|
exactKeys(args, ["worldId"], "Action arguments");
|
|
53
72
|
const operation = identifier(action.operation, "Action operation");
|
|
54
73
|
const sideEffects = action.expectedSideEffects;
|
|
55
|
-
const expected = [
|
|
56
|
-
|
|
74
|
+
const expected = [
|
|
75
|
+
"stripe.subscription.past_due",
|
|
76
|
+
"email.subscription_payment_failed",
|
|
77
|
+
"github.recovery_issue",
|
|
78
|
+
"generic_rest.payment_failed",
|
|
79
|
+
];
|
|
80
|
+
if (!Array.isArray(sideEffects) ||
|
|
81
|
+
sideEffects.length > 16 ||
|
|
82
|
+
sideEffects.some((value) => typeof value !== "string" ||
|
|
83
|
+
!IDENTIFIER.test(value) ||
|
|
84
|
+
sanitizeSecretString(value) !== value) ||
|
|
85
|
+
new Set(sideEffects).size !== sideEffects.length)
|
|
57
86
|
throw new ActionGatewayError("Action expected side effects must be a bounded unique list of safe identifiers.");
|
|
58
|
-
if (operation === "synthetic.subscription_failure" &&
|
|
87
|
+
if (operation === "synthetic.subscription_failure" &&
|
|
88
|
+
(sideEffects.length !== expected.length ||
|
|
89
|
+
sideEffects.some((value, index) => value !== expected[index])))
|
|
59
90
|
throw new ActionGatewayError("Action expected side effects are invalid for the synthetic operation.");
|
|
60
|
-
if (action.riskClass !== "read" &&
|
|
91
|
+
if (action.riskClass !== "read" &&
|
|
92
|
+
action.riskClass !== "write" &&
|
|
93
|
+
action.riskClass !== "money_movement" &&
|
|
94
|
+
action.riskClass !== "external_communication" &&
|
|
95
|
+
action.riskClass !== "delete" &&
|
|
96
|
+
action.riskClass !== "deploy")
|
|
61
97
|
throw new ActionGatewayError("Action risk class is invalid.");
|
|
62
|
-
if (action.reversibility !== "none" &&
|
|
98
|
+
if (action.reversibility !== "none" &&
|
|
99
|
+
action.reversibility !== "compensatable")
|
|
63
100
|
throw new ActionGatewayError("Action reversibility is invalid.");
|
|
64
101
|
const policy = object(action.policy, "Action policy reference is invalid.");
|
|
65
102
|
exactKeys(policy, ["version", "hash"], "Action policy reference");
|
|
@@ -73,8 +110,15 @@ export function validateActionEnvelope(value) {
|
|
|
73
110
|
kind: "ghostapi.action",
|
|
74
111
|
actionId: identifier(action.actionId, "Action id"),
|
|
75
112
|
idempotencyKey: identifier(action.idempotencyKey, "Action idempotency key"),
|
|
76
|
-
actor: {
|
|
77
|
-
|
|
113
|
+
actor: {
|
|
114
|
+
id: identifier(actor.id, "Action actor id"),
|
|
115
|
+
workloadId: identifier(actor.workloadId, "Action workload id"),
|
|
116
|
+
type: actor.type,
|
|
117
|
+
},
|
|
118
|
+
project: {
|
|
119
|
+
id: identifier(project.id, "Action project id"),
|
|
120
|
+
environment: "synthetic",
|
|
121
|
+
},
|
|
78
122
|
provider: "ghostapi-synthetic",
|
|
79
123
|
operation,
|
|
80
124
|
resource: { type: "synthetic-world", id: worldId },
|
|
@@ -82,16 +126,30 @@ export function validateActionEnvelope(value) {
|
|
|
82
126
|
expectedSideEffects: [...sideEffects],
|
|
83
127
|
riskClass: action.riskClass,
|
|
84
128
|
reversibility: action.reversibility,
|
|
85
|
-
policy: {
|
|
129
|
+
policy: {
|
|
130
|
+
version: positiveInteger(policy.version, "Action policy version"),
|
|
131
|
+
hash: hash(policy.hash, "Action policy hash"),
|
|
132
|
+
},
|
|
86
133
|
evidence: { hash: hash(evidence.hash, "Action evidence hash") },
|
|
87
134
|
expiresAt: futureTimestamp(action.expiresAt, "Action expiry"),
|
|
88
|
-
nonce: identifier(action.nonce, "Action nonce")
|
|
135
|
+
nonce: identifier(action.nonce, "Action nonce"),
|
|
89
136
|
};
|
|
90
137
|
}
|
|
91
138
|
export function validateActionApproval(value) {
|
|
92
139
|
const approval = object(value, "Action approval must be an object.");
|
|
93
|
-
exactKeys(approval, [
|
|
94
|
-
|
|
140
|
+
exactKeys(approval, [
|
|
141
|
+
"schemaVersion",
|
|
142
|
+
"kind",
|
|
143
|
+
"approvalId",
|
|
144
|
+
"actionHash",
|
|
145
|
+
"approvedBy",
|
|
146
|
+
"approvedAt",
|
|
147
|
+
"expiresAt",
|
|
148
|
+
"nonce",
|
|
149
|
+
"provenance",
|
|
150
|
+
], "Action approval");
|
|
151
|
+
if (approval.schemaVersion !== ACTION_SCHEMA_VERSION ||
|
|
152
|
+
approval.kind !== APPROVAL_KIND)
|
|
95
153
|
throw new ActionGatewayError("Unsupported action approval schema.");
|
|
96
154
|
const approvedAt = timestamp(approval.approvedAt, "Action approval timestamp");
|
|
97
155
|
const expiresAt = futureTimestamp(approval.expiresAt, "Action approval expiry");
|
|
@@ -99,9 +157,25 @@ export function validateActionApproval(value) {
|
|
|
99
157
|
throw new ActionGatewayError("Action approval must expire after it was issued.");
|
|
100
158
|
const provenance = object(approval.provenance, "Action approval provenance is invalid.");
|
|
101
159
|
exactKeys(provenance, ["kind", "keyId", "signature"], "Action approval provenance");
|
|
102
|
-
if (provenance.kind !== "ghostapi.ed25519-signature" ||
|
|
160
|
+
if (provenance.kind !== "ghostapi.ed25519-signature" ||
|
|
161
|
+
typeof provenance.signature !== "string" ||
|
|
162
|
+
!/^[A-Za-z0-9_-]{86}$/.test(provenance.signature))
|
|
103
163
|
throw new ActionGatewayError("Action approval provenance is invalid.");
|
|
104
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
schemaVersion: 1,
|
|
166
|
+
kind: "ghostapi.action-approval",
|
|
167
|
+
approvalId: identifier(approval.approvalId, "Action approval id"),
|
|
168
|
+
actionHash: hash(approval.actionHash, "Action approval hash"),
|
|
169
|
+
approvedBy: identifier(approval.approvedBy, "Action approver id"),
|
|
170
|
+
approvedAt,
|
|
171
|
+
expiresAt,
|
|
172
|
+
nonce: identifier(approval.nonce, "Action approval nonce"),
|
|
173
|
+
provenance: {
|
|
174
|
+
kind: "ghostapi.ed25519-signature",
|
|
175
|
+
keyId: identifier(provenance.keyId, "Action approval provenance key id"),
|
|
176
|
+
signature: provenance.signature,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
105
179
|
}
|
|
106
180
|
export function createSyntheticActionAdapter() {
|
|
107
181
|
return {
|
|
@@ -116,15 +190,31 @@ export function createSyntheticActionAdapter() {
|
|
|
116
190
|
},
|
|
117
191
|
async execute(action, controls) {
|
|
118
192
|
const receipt = await runSubscriptionFailureWorkflow(action.arguments.worldId, action.actionId, controls?.commit);
|
|
119
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
outcome: "committed",
|
|
195
|
+
providerRequestId: `synthetic_${receipt.actionId}`,
|
|
196
|
+
result: {
|
|
197
|
+
worldId: action.arguments.worldId,
|
|
198
|
+
subscriptionId: receipt.subscriptionId,
|
|
199
|
+
syntheticReceiptId: receipt.actionId,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
120
202
|
},
|
|
121
203
|
async verify(action) {
|
|
122
204
|
const world = await inspectWorld(action.arguments.worldId);
|
|
123
205
|
const receipt = world.state.receipts.find((candidate) => candidate.actionId === action.actionId);
|
|
124
206
|
return receipt === undefined
|
|
125
207
|
? { committed: false }
|
|
126
|
-
: {
|
|
127
|
-
|
|
208
|
+
: {
|
|
209
|
+
committed: true,
|
|
210
|
+
providerRequestId: `synthetic_${receipt.actionId}`,
|
|
211
|
+
result: {
|
|
212
|
+
worldId: action.arguments.worldId,
|
|
213
|
+
subscriptionId: receipt.subscriptionId,
|
|
214
|
+
syntheticReceiptId: receipt.actionId,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
},
|
|
128
218
|
};
|
|
129
219
|
}
|
|
130
220
|
export class LocalActionGateway {
|
|
@@ -137,13 +227,18 @@ export class LocalActionGateway {
|
|
|
137
227
|
this.now = options.now ?? (() => new Date());
|
|
138
228
|
this.pathForAction = options.pathForAction ?? getActionPath;
|
|
139
229
|
this.adapter = options.adapter ?? createSyntheticActionAdapter();
|
|
140
|
-
this.safetyController =
|
|
141
|
-
|
|
230
|
+
this.safetyController =
|
|
231
|
+
options.safetyController ??
|
|
232
|
+
createLocalSafetyController({ now: this.now });
|
|
233
|
+
this.approvalVerifier =
|
|
234
|
+
options.approvalVerifier ?? createDisabledActionApprovalVerifier();
|
|
142
235
|
}
|
|
143
236
|
async submit(actionValue, approvalValue, policy) {
|
|
144
237
|
const action = validateActionEnvelope(actionValue);
|
|
145
238
|
const approval = validateActionApproval(approvalValue);
|
|
146
|
-
const verification = await this.approvalVerifier.verify(action, approval, {
|
|
239
|
+
const verification = await this.approvalVerifier.verify(action, approval, {
|
|
240
|
+
phase: "submit",
|
|
241
|
+
});
|
|
147
242
|
requireApprovalInboxVerification(approval, verification);
|
|
148
243
|
const current = this.now().toISOString();
|
|
149
244
|
verifyPolicy(action, policy);
|
|
@@ -152,14 +247,22 @@ export class LocalActionGateway {
|
|
|
152
247
|
return withFileLock(path, async () => {
|
|
153
248
|
const existing = await readStoredAction(path, false);
|
|
154
249
|
if (existing !== null) {
|
|
155
|
-
if (existing.actionHash === actionDigest &&
|
|
250
|
+
if (existing.actionHash === actionDigest &&
|
|
251
|
+
actionApprovalHash(existing.approval) === actionApprovalHash(approval))
|
|
156
252
|
return existing;
|
|
157
253
|
throw new ActionGatewayError("Action id already exists with a different immutable envelope or approval.");
|
|
158
254
|
}
|
|
159
255
|
if (Date.parse(action.expiresAt) <= Date.parse(current))
|
|
160
256
|
throw new ActionGatewayError("Action has expired.");
|
|
161
257
|
verifyApproval(action, approval, current);
|
|
162
|
-
const record = {
|
|
258
|
+
const record = {
|
|
259
|
+
schemaVersion: 1,
|
|
260
|
+
kind: "ghostapi.action-record",
|
|
261
|
+
envelope: action,
|
|
262
|
+
actionHash: actionDigest,
|
|
263
|
+
approval,
|
|
264
|
+
receipts: [],
|
|
265
|
+
};
|
|
163
266
|
record.receipts.push(createReceipt(record, "requested", current));
|
|
164
267
|
await writeStoredAction(path, record);
|
|
165
268
|
return record;
|
|
@@ -178,11 +281,14 @@ export class LocalActionGateway {
|
|
|
178
281
|
const approvalVerification = await this.approvalVerifier.verify(record.envelope, record.approval, { phase: "execute" });
|
|
179
282
|
requireApprovalInboxVerification(record.approval, approvalVerification);
|
|
180
283
|
const current = this.now().toISOString();
|
|
181
|
-
if (actionHash(requested) !== record.actionHash ||
|
|
284
|
+
if (actionHash(requested) !== record.actionHash ||
|
|
285
|
+
canonicalizeActionEnvelope(requested) !==
|
|
286
|
+
canonicalizeActionEnvelope(record.envelope))
|
|
182
287
|
throw new ActionGatewayError("Action envelope changed after approval; execution is blocked.");
|
|
183
288
|
verifyApproval(record.envelope, record.approval, current, false);
|
|
184
289
|
verifyPolicy(record.envelope, policy);
|
|
185
|
-
if (identity.actorId !== record.envelope.actor.id ||
|
|
290
|
+
if (identity.actorId !== record.envelope.actor.id ||
|
|
291
|
+
identity.workloadId !== record.envelope.actor.workloadId)
|
|
186
292
|
throw new ActionGatewayError("Execution identity does not match the approved action.");
|
|
187
293
|
if (!this.adapter.supports(record.envelope.operation))
|
|
188
294
|
throw new ActionGatewayError(`Execution adapter does not support action operation: ${record.envelope.operation}`);
|
|
@@ -192,7 +298,9 @@ export class LocalActionGateway {
|
|
|
192
298
|
const committed = findLatestReceipt(record.receipts, "committed");
|
|
193
299
|
if (committed !== undefined) {
|
|
194
300
|
const reconciliation = await this.adapter.verify(record.envelope);
|
|
195
|
-
if (!reconciliation.committed ||
|
|
301
|
+
if (!reconciliation.committed ||
|
|
302
|
+
reconciliation.providerRequestId === undefined ||
|
|
303
|
+
reconciliation.result === undefined)
|
|
196
304
|
throw new ActionGatewayError("Committed action verification is incomplete; execution will not retry without idempotency proof.");
|
|
197
305
|
const verifiedReceipt = createReceipt(record, "verified", current, reconciliation.providerRequestId, reconciliation.result);
|
|
198
306
|
appendReceipt(record, verifiedReceipt);
|
|
@@ -202,7 +310,9 @@ export class LocalActionGateway {
|
|
|
202
310
|
const attempted = findLatestReceipt(record.receipts, "attempted");
|
|
203
311
|
if (attempted !== undefined) {
|
|
204
312
|
const reconciliation = await this.adapter.verify(record.envelope);
|
|
205
|
-
if (!reconciliation.committed ||
|
|
313
|
+
if (!reconciliation.committed ||
|
|
314
|
+
reconciliation.providerRequestId === undefined ||
|
|
315
|
+
reconciliation.result === undefined)
|
|
206
316
|
throw new ActionGatewayError("Prior action outcome is unknown; execution will not retry without idempotency proof.");
|
|
207
317
|
appendReceipt(record, createReceipt(record, "committed", current, reconciliation.providerRequestId, reconciliation.result));
|
|
208
318
|
const verifiedReceipt = createReceipt(record, "verified", current, reconciliation.providerRequestId, reconciliation.result);
|
|
@@ -222,17 +332,27 @@ export class LocalActionGateway {
|
|
|
222
332
|
await writeStoredAction(path, record);
|
|
223
333
|
let execution;
|
|
224
334
|
try {
|
|
225
|
-
execution = await this.adapter.execute(record.envelope, {
|
|
335
|
+
execution = await this.adapter.execute(record.envelope, {
|
|
336
|
+
commit: safety.commit,
|
|
337
|
+
});
|
|
226
338
|
}
|
|
227
339
|
catch {
|
|
228
|
-
await safety.complete({
|
|
340
|
+
await safety.complete({
|
|
341
|
+
success: false,
|
|
342
|
+
latencyMs: 0,
|
|
343
|
+
reason: "synthetic execution failed or was stopped",
|
|
344
|
+
});
|
|
229
345
|
const failed = createReceipt(record, "failed", this.now().toISOString(), undefined, undefined, { code: "execution_error", retrySafe: false });
|
|
230
346
|
appendReceipt(record, failed);
|
|
231
347
|
await writeStoredAction(path, record);
|
|
232
348
|
throw new ActionGatewayError("Action execution failed; it was not automatically retried.");
|
|
233
349
|
}
|
|
234
350
|
if (execution.outcome === "unknown") {
|
|
235
|
-
await safety.complete({
|
|
351
|
+
await safety.complete({
|
|
352
|
+
success: false,
|
|
353
|
+
latencyMs: 0,
|
|
354
|
+
reason: "synthetic execution outcome is unknown",
|
|
355
|
+
});
|
|
236
356
|
const failed = createReceipt(record, "failed", this.now().toISOString(), undefined, undefined, { code: "unknown_outcome", retrySafe: false });
|
|
237
357
|
appendReceipt(record, failed);
|
|
238
358
|
await writeStoredAction(path, record);
|
|
@@ -241,8 +361,15 @@ export class LocalActionGateway {
|
|
|
241
361
|
appendReceipt(record, createReceipt(record, "committed", this.now().toISOString(), execution.providerRequestId, execution.result));
|
|
242
362
|
await writeStoredAction(path, record);
|
|
243
363
|
const verification = await this.adapter.verify(record.envelope);
|
|
244
|
-
if (!verification.committed ||
|
|
245
|
-
|
|
364
|
+
if (!verification.committed ||
|
|
365
|
+
verification.providerRequestId === undefined ||
|
|
366
|
+
verification.result === undefined) {
|
|
367
|
+
await safety.complete({
|
|
368
|
+
success: false,
|
|
369
|
+
reconciliationMismatch: true,
|
|
370
|
+
latencyMs: 0,
|
|
371
|
+
reason: "synthetic verification did not prove the result",
|
|
372
|
+
});
|
|
246
373
|
const failed = createReceipt(record, "failed", this.now().toISOString(), execution.providerRequestId, execution.result, { code: "verification_failed", retrySafe: false });
|
|
247
374
|
appendReceipt(record, failed);
|
|
248
375
|
await writeStoredAction(path, record);
|
|
@@ -257,7 +384,8 @@ export class LocalActionGateway {
|
|
|
257
384
|
}
|
|
258
385
|
async compensate(actionId) {
|
|
259
386
|
const action = await this.inspect(actionId);
|
|
260
|
-
if (this.adapter.compensate === undefined ||
|
|
387
|
+
if (this.adapter.compensate === undefined ||
|
|
388
|
+
action.envelope.reversibility !== "compensatable")
|
|
261
389
|
throw new ActionGatewayError("Compensation is unsupported for this action; no rollback is claimed.");
|
|
262
390
|
await this.adapter.compensate(action.envelope);
|
|
263
391
|
throw new ActionGatewayError("Compensation adapters must return a durable compensation receipt before this operation can be exposed.");
|
|
@@ -275,32 +403,51 @@ function safetyAction(action, actionDigest) {
|
|
|
275
403
|
riskClass: action.riskClass,
|
|
276
404
|
idempotencyKey: action.idempotencyKey,
|
|
277
405
|
actionHash: actionDigest,
|
|
278
|
-
costs: {
|
|
406
|
+
costs: {
|
|
407
|
+
monetaryAmountMinor: 0,
|
|
408
|
+
requests: 1,
|
|
409
|
+
messages: 1,
|
|
410
|
+
mutations: 1,
|
|
411
|
+
deletes: 0,
|
|
412
|
+
tokenCost: 0,
|
|
413
|
+
},
|
|
279
414
|
};
|
|
280
415
|
}
|
|
281
416
|
export function createLocalActionGateway(options = {}) {
|
|
282
417
|
return new LocalActionGateway(options);
|
|
283
418
|
}
|
|
284
419
|
export function createDisabledActionApprovalVerifier() {
|
|
285
|
-
return {
|
|
420
|
+
return {
|
|
421
|
+
async verify() {
|
|
422
|
+
throw new ActionGatewayError("Action approval authenticity could not be verified.");
|
|
423
|
+
},
|
|
424
|
+
};
|
|
286
425
|
}
|
|
287
426
|
export function createEd25519ActionApprovalSigner(options) {
|
|
288
427
|
const keyId = identifier(options.keyId, "Action approval signing key id");
|
|
289
|
-
const privateKey = options.privateKey instanceof KeyObject
|
|
290
|
-
|
|
428
|
+
const privateKey = options.privateKey instanceof KeyObject
|
|
429
|
+
? options.privateKey
|
|
430
|
+
: createPrivateKey(options.privateKey);
|
|
431
|
+
if (privateKey.type !== "private" ||
|
|
432
|
+
privateKey.asymmetricKeyType !== "ed25519")
|
|
291
433
|
throw new ActionGatewayError("Action approval signing key must be an Ed25519 private key.");
|
|
292
434
|
return {
|
|
293
435
|
issue(value) {
|
|
294
436
|
const approval = validateUnsignedActionApproval(value);
|
|
295
437
|
const signature = sign(null, Buffer.from(canonicalJson(approval), "utf8"), privateKey).toString("base64url");
|
|
296
|
-
return {
|
|
297
|
-
|
|
438
|
+
return {
|
|
439
|
+
...approval,
|
|
440
|
+
provenance: { kind: "ghostapi.ed25519-signature", keyId, signature },
|
|
441
|
+
};
|
|
442
|
+
},
|
|
298
443
|
};
|
|
299
444
|
}
|
|
300
445
|
export function createEd25519ActionApprovalVerifier(options) {
|
|
301
446
|
const keys = new Map(Object.entries(options.trustedKeys).map(([keyId, key]) => {
|
|
302
447
|
const parsedId = identifier(keyId, "Action approval verification key id");
|
|
303
|
-
const publicKey = key instanceof KeyObject && key.type === "public"
|
|
448
|
+
const publicKey = key instanceof KeyObject && key.type === "public"
|
|
449
|
+
? key
|
|
450
|
+
: createPublicKey(key);
|
|
304
451
|
if (publicKey.asymmetricKeyType !== "ed25519")
|
|
305
452
|
throw new ActionGatewayError("Action approval verification key must be Ed25519.");
|
|
306
453
|
return [parsedId, publicKey];
|
|
@@ -309,20 +456,26 @@ export function createEd25519ActionApprovalVerifier(options) {
|
|
|
309
456
|
async verify(action, approval) {
|
|
310
457
|
const parsed = validateActionApproval(approval);
|
|
311
458
|
const publicKey = keys.get(parsed.provenance.keyId);
|
|
312
|
-
const valid = publicKey !== undefined &&
|
|
459
|
+
const valid = publicKey !== undefined &&
|
|
460
|
+
verify(null, Buffer.from(canonicalJson(unsignedActionApproval(parsed)), "utf8"), publicKey, Buffer.from(parsed.provenance.signature, "base64url"));
|
|
313
461
|
if (!valid || parsed.actionHash !== actionHash(action))
|
|
314
462
|
throw new ActionGatewayError("Action approval authenticity could not be verified.");
|
|
315
463
|
return { provenance: "verified" };
|
|
316
|
-
}
|
|
464
|
+
},
|
|
317
465
|
};
|
|
318
466
|
}
|
|
319
467
|
export function createTestActionApprovalVerifier() {
|
|
320
468
|
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
321
|
-
const signer = createEd25519ActionApprovalSigner({
|
|
469
|
+
const signer = createEd25519ActionApprovalSigner({
|
|
470
|
+
keyId: "test-approval-key",
|
|
471
|
+
privateKey,
|
|
472
|
+
});
|
|
322
473
|
return {
|
|
323
|
-
verifier: createEd25519ActionApprovalVerifier({
|
|
474
|
+
verifier: createEd25519ActionApprovalVerifier({
|
|
475
|
+
trustedKeys: { "test-approval-key": publicKey },
|
|
476
|
+
}),
|
|
324
477
|
signer,
|
|
325
|
-
issue: (approval) => signer.issue(approval)
|
|
478
|
+
issue: (approval) => signer.issue(approval),
|
|
326
479
|
};
|
|
327
480
|
}
|
|
328
481
|
export function getActionPath(actionId) {
|
|
@@ -357,7 +510,8 @@ async function writeStoredAction(path, record) {
|
|
|
357
510
|
export function validateStoredAction(value) {
|
|
358
511
|
const record = object(value, "Action record must be an object.");
|
|
359
512
|
exactKeys(record, ["schemaVersion", "kind", "envelope", "actionHash", "approval", "receipts"], "Action record");
|
|
360
|
-
if (record.schemaVersion !== ACTION_SCHEMA_VERSION ||
|
|
513
|
+
if (record.schemaVersion !== ACTION_SCHEMA_VERSION ||
|
|
514
|
+
record.kind !== RECORD_KIND)
|
|
361
515
|
throw new ActionGatewayError("Unsupported action record schema.");
|
|
362
516
|
const envelope = validateActionEnvelope(record.envelope);
|
|
363
517
|
const digest = hash(record.actionHash, "Action record hash");
|
|
@@ -366,16 +520,43 @@ export function validateStoredAction(value) {
|
|
|
366
520
|
const approval = validateActionApproval(record.approval);
|
|
367
521
|
verifyApproval(envelope, approval, new Date(0).toISOString(), false);
|
|
368
522
|
const rawReceipts = record.receipts;
|
|
369
|
-
if (!Array.isArray(rawReceipts) ||
|
|
523
|
+
if (!Array.isArray(rawReceipts) ||
|
|
524
|
+
rawReceipts.length < 1 ||
|
|
525
|
+
rawReceipts.length > MAX_RECEIPTS)
|
|
370
526
|
throw new ActionGatewayError("Action record receipts are invalid.");
|
|
371
|
-
const receipts = rawReceipts.map((receipt, index) => validateReceipt(receipt, envelope.actionId, digest, index === 0
|
|
527
|
+
const receipts = rawReceipts.map((receipt, index) => validateReceipt(receipt, envelope.actionId, digest, index === 0
|
|
528
|
+
? receiptGenesisHash(digest)
|
|
529
|
+
: rawReceipts[index - 1].receiptHash));
|
|
372
530
|
if (receipts[0].status !== "requested")
|
|
373
531
|
throw new ActionGatewayError("Action record must begin with a requested receipt.");
|
|
374
|
-
return {
|
|
532
|
+
return {
|
|
533
|
+
schemaVersion: 1,
|
|
534
|
+
kind: "ghostapi.action-record",
|
|
535
|
+
envelope,
|
|
536
|
+
actionHash: digest,
|
|
537
|
+
approval,
|
|
538
|
+
receipts,
|
|
539
|
+
};
|
|
375
540
|
}
|
|
376
541
|
function createReceipt(record, status, timestamp, providerRequestId, result, failure) {
|
|
377
|
-
const previousReceiptHash = record.receipts.length === 0
|
|
378
|
-
|
|
542
|
+
const previousReceiptHash = record.receipts.length === 0
|
|
543
|
+
? receiptGenesisHash(record.actionHash)
|
|
544
|
+
: record.receipts.at(-1).receiptHash;
|
|
545
|
+
const base = {
|
|
546
|
+
receiptId: identifier(`receipt-${randomUUID().replace(/-/g, "")}`, "Receipt id"),
|
|
547
|
+
actionId: record.envelope.actionId,
|
|
548
|
+
actionHash: record.actionHash,
|
|
549
|
+
status,
|
|
550
|
+
timestamp: timestampValue(timestamp, "Action receipt timestamp"),
|
|
551
|
+
...(providerRequestId === undefined
|
|
552
|
+
? {}
|
|
553
|
+
: {
|
|
554
|
+
providerRequestId: identifier(providerRequestId, "Provider request id"),
|
|
555
|
+
}),
|
|
556
|
+
...(result === undefined ? {} : { result }),
|
|
557
|
+
...(failure === undefined ? {} : { failure }),
|
|
558
|
+
previousReceiptHash,
|
|
559
|
+
};
|
|
379
560
|
return { ...base, receiptHash: sha256(canonicalJson(base)) };
|
|
380
561
|
}
|
|
381
562
|
function appendReceipt(record, receipt) {
|
|
@@ -385,38 +566,80 @@ function appendReceipt(record, receipt) {
|
|
|
385
566
|
}
|
|
386
567
|
function validateReceipt(value, actionId, actionDigest, previousReceiptHash) {
|
|
387
568
|
const receipt = object(value, "Action receipt is invalid.");
|
|
388
|
-
exactKeys(receipt, [
|
|
389
|
-
|
|
569
|
+
exactKeys(receipt, [
|
|
570
|
+
"receiptId",
|
|
571
|
+
"actionId",
|
|
572
|
+
"actionHash",
|
|
573
|
+
"status",
|
|
574
|
+
"timestamp",
|
|
575
|
+
"providerRequestId",
|
|
576
|
+
"result",
|
|
577
|
+
"failure",
|
|
578
|
+
"previousReceiptHash",
|
|
579
|
+
"receiptHash",
|
|
580
|
+
], "Action receipt");
|
|
581
|
+
if (receipt.actionId !== actionId ||
|
|
582
|
+
receipt.actionHash !== actionDigest ||
|
|
583
|
+
receipt.previousReceiptHash !== previousReceiptHash ||
|
|
584
|
+
(receipt.status !== "requested" &&
|
|
585
|
+
receipt.status !== "attempted" &&
|
|
586
|
+
receipt.status !== "committed" &&
|
|
587
|
+
receipt.status !== "verified" &&
|
|
588
|
+
receipt.status !== "failed"))
|
|
390
589
|
throw new ActionGatewayError("Action receipt linkage is invalid.");
|
|
391
|
-
const providerRequestId = receipt.providerRequestId === undefined
|
|
590
|
+
const providerRequestId = receipt.providerRequestId === undefined
|
|
591
|
+
? undefined
|
|
592
|
+
: identifier(receipt.providerRequestId, "Provider request id");
|
|
392
593
|
const result = receipt.result === undefined ? undefined : normalizeResult(receipt.result);
|
|
393
|
-
const failure = receipt.failure === undefined
|
|
394
|
-
|
|
594
|
+
const failure = receipt.failure === undefined
|
|
595
|
+
? undefined
|
|
596
|
+
: normalizeFailure(receipt.failure);
|
|
597
|
+
if ((receipt.status === "committed" || receipt.status === "verified") &&
|
|
598
|
+
(providerRequestId === undefined || result === undefined))
|
|
395
599
|
throw new ActionGatewayError("Committed and verified receipts require a provider result.");
|
|
396
600
|
if (receipt.status === "failed" && failure === undefined)
|
|
397
601
|
throw new ActionGatewayError("Failed receipt requires a failure classification.");
|
|
398
602
|
const status = receipt.status;
|
|
399
|
-
const base = {
|
|
400
|
-
|
|
603
|
+
const base = {
|
|
604
|
+
receiptId: identifier(receipt.receiptId, "Receipt id"),
|
|
605
|
+
actionId,
|
|
606
|
+
actionHash: actionDigest,
|
|
607
|
+
status,
|
|
608
|
+
timestamp: timestampValue(receipt.timestamp, "Action receipt timestamp"),
|
|
609
|
+
...(providerRequestId === undefined ? {} : { providerRequestId }),
|
|
610
|
+
...(result === undefined ? {} : { result }),
|
|
611
|
+
...(failure === undefined ? {} : { failure }),
|
|
612
|
+
previousReceiptHash,
|
|
613
|
+
};
|
|
614
|
+
if (hash(receipt.receiptHash, "Action receipt hash") !==
|
|
615
|
+
sha256(canonicalJson(base)))
|
|
401
616
|
throw new ActionGatewayError("Action receipt hash is invalid.");
|
|
402
617
|
return { ...base, receiptHash: receipt.receiptHash };
|
|
403
618
|
}
|
|
404
619
|
function normalizeResult(value) {
|
|
405
620
|
const result = object(value, "Action receipt result is invalid.");
|
|
406
621
|
exactKeys(result, ["worldId", "subscriptionId", "syntheticReceiptId"], "Action receipt result");
|
|
407
|
-
return {
|
|
622
|
+
return {
|
|
623
|
+
worldId: identifier(result.worldId, "Action receipt world id"),
|
|
624
|
+
subscriptionId: identifier(result.subscriptionId, "Action receipt subscription id"),
|
|
625
|
+
syntheticReceiptId: identifier(result.syntheticReceiptId, "Action receipt synthetic receipt id"),
|
|
626
|
+
};
|
|
408
627
|
}
|
|
409
628
|
function normalizeFailure(value) {
|
|
410
629
|
const failure = object(value, "Action receipt failure is invalid.");
|
|
411
630
|
exactKeys(failure, ["code", "retrySafe"], "Action receipt failure");
|
|
412
|
-
if ((failure.code !== "execution_error" &&
|
|
631
|
+
if ((failure.code !== "execution_error" &&
|
|
632
|
+
failure.code !== "unknown_outcome" &&
|
|
633
|
+
failure.code !== "verification_failed") ||
|
|
634
|
+
failure.retrySafe !== false)
|
|
413
635
|
throw new ActionGatewayError("Action receipt failure is invalid.");
|
|
414
636
|
return { code: failure.code, retrySafe: false };
|
|
415
637
|
}
|
|
416
638
|
function verifyApproval(action, approval, now, enforceExpiry = true) {
|
|
417
639
|
if (approval.actionHash !== actionHash(action))
|
|
418
640
|
throw new ActionGatewayError("Action approval is not bound to the exact canonical action hash.");
|
|
419
|
-
if (approval.approvedBy === action.actor.id ||
|
|
641
|
+
if (approval.approvedBy === action.actor.id ||
|
|
642
|
+
approval.approvedBy === action.actor.workloadId)
|
|
420
643
|
throw new ActionGatewayError("An action actor cannot approve its own high-risk action.");
|
|
421
644
|
if (enforceExpiry && Date.parse(approval.approvedAt) > Date.parse(now))
|
|
422
645
|
throw new ActionGatewayError("Action approval timestamp is in the future.");
|
|
@@ -426,7 +649,8 @@ function verifyApproval(action, approval, now, enforceExpiry = true) {
|
|
|
426
649
|
function verifyPolicy(action, policy) {
|
|
427
650
|
if (!policy.allowed)
|
|
428
651
|
throw new ActionGatewayError("Current policy denies action execution.");
|
|
429
|
-
if (policy.version !== action.policy.version ||
|
|
652
|
+
if (policy.version !== action.policy.version ||
|
|
653
|
+
policy.hash !== action.policy.hash)
|
|
430
654
|
throw new ActionGatewayError("Current policy reference does not match the approved action.");
|
|
431
655
|
}
|
|
432
656
|
function receiptGenesisHash(actionDigest) {
|
|
@@ -451,7 +675,9 @@ function exactKeys(value, keys, label) {
|
|
|
451
675
|
throw new ActionGatewayError(`${label} contains unsupported field: ${key}`);
|
|
452
676
|
}
|
|
453
677
|
function identifier(value, label) {
|
|
454
|
-
if (typeof value !== "string" ||
|
|
678
|
+
if (typeof value !== "string" ||
|
|
679
|
+
!IDENTIFIER.test(value) ||
|
|
680
|
+
sanitizeSecretString(value) !== value)
|
|
455
681
|
throw new ActionGatewayError(`${label} must be a safe stable identifier.`);
|
|
456
682
|
return value;
|
|
457
683
|
}
|
|
@@ -469,7 +695,9 @@ function timestamp(value, label) {
|
|
|
469
695
|
return timestampValue(value, label);
|
|
470
696
|
}
|
|
471
697
|
function timestampValue(value, label) {
|
|
472
|
-
if (typeof value !== "string" ||
|
|
698
|
+
if (typeof value !== "string" ||
|
|
699
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
|
|
700
|
+
!Number.isFinite(Date.parse(value)))
|
|
473
701
|
throw new ActionGatewayError(`${label} must be an ISO UTC timestamp.`);
|
|
474
702
|
return value;
|
|
475
703
|
}
|
|
@@ -488,18 +716,38 @@ function canonicalJson(value) {
|
|
|
488
716
|
return `[${value.map(canonicalJson).join(",")}]`;
|
|
489
717
|
if (typeof value === "object") {
|
|
490
718
|
const record = value;
|
|
491
|
-
return `{${Object.keys(record)
|
|
719
|
+
return `{${Object.keys(record)
|
|
720
|
+
.sort()
|
|
721
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
|
722
|
+
.join(",")}}`;
|
|
492
723
|
}
|
|
493
724
|
throw new ActionGatewayError("Canonical action values must be JSON data.");
|
|
494
725
|
}
|
|
495
726
|
function requireApprovalInboxVerification(approval, verification) {
|
|
496
|
-
if (approval.approvedBy === "approval-inbox" &&
|
|
727
|
+
if (approval.approvedBy === "approval-inbox" &&
|
|
728
|
+
verification.approvalInboxRequestId !== approval.approvalId)
|
|
497
729
|
throw new ActionGatewayError("Approval inbox provenance is not active for execution.");
|
|
498
730
|
}
|
|
499
731
|
function validateUnsignedActionApproval(value) {
|
|
500
732
|
const approval = object(value, "Action approval must be an object.");
|
|
501
|
-
exactKeys(approval, [
|
|
502
|
-
|
|
733
|
+
exactKeys(approval, [
|
|
734
|
+
"schemaVersion",
|
|
735
|
+
"kind",
|
|
736
|
+
"approvalId",
|
|
737
|
+
"actionHash",
|
|
738
|
+
"approvedBy",
|
|
739
|
+
"approvedAt",
|
|
740
|
+
"expiresAt",
|
|
741
|
+
"nonce",
|
|
742
|
+
], "Unsigned action approval");
|
|
743
|
+
return unsignedActionApproval(validateActionApproval({
|
|
744
|
+
...approval,
|
|
745
|
+
provenance: {
|
|
746
|
+
kind: "ghostapi.ed25519-signature",
|
|
747
|
+
keyId: "validation-key",
|
|
748
|
+
signature: "A".repeat(86),
|
|
749
|
+
},
|
|
750
|
+
}));
|
|
503
751
|
}
|
|
504
752
|
function unsignedActionApproval(approval) {
|
|
505
753
|
const { provenance: _provenance, ...unsigned } = approval;
|