@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
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { lstat, readFile } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
-
import { actionApprovalHash, actionHash, createLocalActionGateway, createSyntheticActionAdapter, validateActionApproval, validateActionEnvelope } from "../actions/index.js";
|
|
4
|
+
import { actionApprovalHash, actionHash, createLocalActionGateway, createSyntheticActionAdapter, validateActionApproval, validateActionEnvelope, } from "../actions/index.js";
|
|
5
5
|
import { getDataPaths } from "../config/dataPaths.js";
|
|
6
6
|
import { sanitizeSecretString } from "../security/secrets.js";
|
|
7
|
-
import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
|
|
8
|
-
const SCHEMA_VERSION = 1;
|
|
7
|
+
import { atomicWriteJson, ensurePrivateDirectory, withFileLock, } from "../storage/fileStore.js";
|
|
9
8
|
const MAX_STORE_BYTES = 1024 * 1024;
|
|
10
9
|
const MAX_REQUESTS = 1_000;
|
|
11
10
|
const MAX_DECISIONS = 4;
|
|
@@ -13,7 +12,10 @@ const MAX_AUDIT = 10_000;
|
|
|
13
12
|
const IDENTIFIER = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
14
13
|
const HASH = /^[a-f0-9]{64}$/;
|
|
15
14
|
export class ApprovalInboxError extends Error {
|
|
16
|
-
constructor(message) {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "ApprovalInboxError";
|
|
18
|
+
}
|
|
17
19
|
}
|
|
18
20
|
export function createLocalApprovalInbox(options) {
|
|
19
21
|
return new LocalApprovalInbox(options);
|
|
@@ -40,13 +42,24 @@ export class LocalApprovalInbox {
|
|
|
40
42
|
const approval = validateActionApproval(approvalValue);
|
|
41
43
|
const state = await this.read();
|
|
42
44
|
const request = state.requests.find((candidate) => candidate.id === approval.approvalId);
|
|
43
|
-
const active = request?.status === "executing" ||
|
|
44
|
-
|
|
45
|
+
const active = request?.status === "executing" ||
|
|
46
|
+
(request?.status === "executed" &&
|
|
47
|
+
request.executionReceiptHash !== undefined);
|
|
48
|
+
if (request === undefined ||
|
|
49
|
+
!active ||
|
|
50
|
+
request.consumedAt === undefined ||
|
|
51
|
+
request.artifact === undefined ||
|
|
52
|
+
request.actionHash !== actionHash(action) ||
|
|
53
|
+
actionApprovalHash(request.artifact) !== actionApprovalHash(approval))
|
|
45
54
|
throw new ApprovalInboxError("Approval inbox provenance is not active for execution.");
|
|
46
55
|
return { ...verified, approvalInboxRequestId: request.id };
|
|
47
|
-
}
|
|
56
|
+
},
|
|
48
57
|
};
|
|
49
|
-
this.actionGateway = createLocalActionGateway({
|
|
58
|
+
this.actionGateway = createLocalActionGateway({
|
|
59
|
+
...options.actionGatewayOptions,
|
|
60
|
+
now: this.now,
|
|
61
|
+
approvalVerifier: inboxVerifier,
|
|
62
|
+
});
|
|
50
63
|
}
|
|
51
64
|
async request(actionValue, policyValue, contextValue) {
|
|
52
65
|
const action = validateActionEnvelope(actionValue);
|
|
@@ -60,7 +73,8 @@ export class LocalApprovalInbox {
|
|
|
60
73
|
return this.mutate((state, timestamp) => {
|
|
61
74
|
refresh(state, timestamp);
|
|
62
75
|
const actionDigest = actionHash(action);
|
|
63
|
-
const existing = state.requests.find((request) => request.actionHash === actionDigest &&
|
|
76
|
+
const existing = state.requests.find((request) => request.actionHash === actionDigest &&
|
|
77
|
+
request.status !== "superseded");
|
|
64
78
|
if (existing !== undefined)
|
|
65
79
|
return clone(existing);
|
|
66
80
|
const decision = evaluate(policy, action, context, state.requests, timestamp);
|
|
@@ -68,10 +82,23 @@ export class LocalApprovalInbox {
|
|
|
68
82
|
throw new ApprovalInboxError(`Approval policy denies this action: ${decision.reasons.join("; ")}`);
|
|
69
83
|
const expiresAt = new Date(Math.min(Date.parse(action.expiresAt), Date.parse(timestamp) + policy.approvalTtlMs)).toISOString();
|
|
70
84
|
const request = {
|
|
71
|
-
id: identifier(`approval-${randomUUID().replace(/-/g, "")}`, "Approval request id"),
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
id: identifier(`approval-${randomUUID().replace(/-/g, "")}`, "Approval request id"),
|
|
86
|
+
action,
|
|
87
|
+
actionHash: actionDigest,
|
|
88
|
+
risk: classify(action),
|
|
89
|
+
context,
|
|
90
|
+
policy: {
|
|
91
|
+
id: policy.id,
|
|
92
|
+
version: policy.version,
|
|
93
|
+
hash: policyHash(policy),
|
|
94
|
+
requiredApprovals: decision.requiredApprovals,
|
|
95
|
+
},
|
|
96
|
+
display: display(action, context, decision.reasons),
|
|
97
|
+
status: "pending",
|
|
98
|
+
createdAt: timestamp,
|
|
99
|
+
expiresAt,
|
|
100
|
+
escalationAt: new Date(Date.parse(timestamp) + policy.escalationTimeoutMs).toISOString(),
|
|
101
|
+
decisions: [],
|
|
75
102
|
};
|
|
76
103
|
state.requests.push(request);
|
|
77
104
|
appendAudit(state, "request.created", request, action.actor.id, timestamp);
|
|
@@ -86,12 +113,23 @@ export class LocalApprovalInbox {
|
|
|
86
113
|
const request = findRequest(state, requestId);
|
|
87
114
|
requirePending(request);
|
|
88
115
|
assertIndependent(request, approver);
|
|
89
|
-
if (request.decisions.some((decision) => decision.kind === "approve" &&
|
|
116
|
+
if (request.decisions.some((decision) => decision.kind === "approve" &&
|
|
117
|
+
(decision.approverId === approver.id ||
|
|
118
|
+
decision.principalId === approver.principalId ||
|
|
119
|
+
decision.independenceKey === approver.independenceKey)))
|
|
90
120
|
throw new ApprovalInboxError("An independent approver can approve this action only once.");
|
|
91
|
-
const decision = {
|
|
121
|
+
const decision = {
|
|
122
|
+
id: identifier(`decision-${randomUUID().replace(/-/g, "")}`, "Approval decision id"),
|
|
123
|
+
kind: "approve",
|
|
124
|
+
approverId: approver.id,
|
|
125
|
+
principalId: approver.principalId,
|
|
126
|
+
independenceKey: approver.independenceKey,
|
|
127
|
+
decidedAt: now,
|
|
128
|
+
};
|
|
92
129
|
request.decisions.push(decision);
|
|
93
130
|
appendAudit(state, "request.approved", request, approver.id, now);
|
|
94
|
-
if (request.decisions.filter((entry) => entry.kind === "approve").length >=
|
|
131
|
+
if (request.decisions.filter((entry) => entry.kind === "approve").length >=
|
|
132
|
+
request.policy.requiredApprovals) {
|
|
95
133
|
request.status = "approved";
|
|
96
134
|
request.artifact = await createArtifact(request, now, this.approvalSigner);
|
|
97
135
|
appendAudit(state, "artifact.issued", request, "approval-inbox", now);
|
|
@@ -111,7 +149,9 @@ export class LocalApprovalInbox {
|
|
|
111
149
|
const old = await this.mutate((state, now) => {
|
|
112
150
|
refresh(state, now);
|
|
113
151
|
const request = findRequest(state, requestId);
|
|
114
|
-
if (request.status === "executing" ||
|
|
152
|
+
if (request.status === "executing" ||
|
|
153
|
+
request.status === "executed" ||
|
|
154
|
+
request.consumedAt !== undefined)
|
|
115
155
|
throw new ApprovalInboxError("Executed or executing approvals cannot be edited.");
|
|
116
156
|
request.status = "superseded";
|
|
117
157
|
appendAudit(state, "request.superseded", request, approver.id, now);
|
|
@@ -134,18 +174,26 @@ export class LocalApprovalInbox {
|
|
|
134
174
|
const now = this.timestamp();
|
|
135
175
|
refresh(state, now);
|
|
136
176
|
const request = findRequest(state, requestId);
|
|
137
|
-
if ((request.status !== "approved" &&
|
|
177
|
+
if ((request.status !== "approved" &&
|
|
178
|
+
request.status !== "executing" &&
|
|
179
|
+
request.status !== "executed") ||
|
|
180
|
+
request.artifact === undefined)
|
|
138
181
|
throw new ApprovalInboxError("Approval is not active for execution.");
|
|
139
|
-
if (request.policy.id !== policy.id ||
|
|
182
|
+
if (request.policy.id !== policy.id ||
|
|
183
|
+
request.policy.version !== policy.version ||
|
|
184
|
+
request.policy.hash !== policyHash(policy))
|
|
140
185
|
throw new ApprovalInboxError("Approval policy changed after approval.");
|
|
141
186
|
const decision = evaluate(policy, request.action, request.context, state.requests, now, request.id);
|
|
142
|
-
if (!decision.allowed ||
|
|
187
|
+
if (!decision.allowed ||
|
|
188
|
+
decision.requiredApprovals !== request.policy.requiredApprovals)
|
|
143
189
|
throw new ApprovalInboxError("Current approval policy no longer authorizes execution.");
|
|
144
|
-
if (identity.actorId !== request.action.actor.id ||
|
|
190
|
+
if (identity.actorId !== request.action.actor.id ||
|
|
191
|
+
identity.workloadId !== request.action.actor.workloadId)
|
|
145
192
|
throw new ApprovalInboxError("Execution identity does not match the approved action.");
|
|
146
193
|
if (request.status === "executed") {
|
|
147
194
|
const receipt = await this.actionGateway.execute(request.action, identity, actionPolicy);
|
|
148
|
-
if (receipt.status !== "verified" ||
|
|
195
|
+
if (receipt.status !== "verified" ||
|
|
196
|
+
receipt.receiptHash !== request.executionReceiptHash)
|
|
149
197
|
throw new ApprovalInboxError("Executed approval does not match its durable action receipt.");
|
|
150
198
|
return receipt;
|
|
151
199
|
}
|
|
@@ -185,12 +233,22 @@ export class LocalApprovalInbox {
|
|
|
185
233
|
}
|
|
186
234
|
async get(requestIdValue) {
|
|
187
235
|
const requestId = identifier(requestIdValue, "Approval request id");
|
|
188
|
-
return this.mutate((state, now) => {
|
|
236
|
+
return this.mutate((state, now) => {
|
|
237
|
+
refresh(state, now);
|
|
238
|
+
return clone(findRequest(state, requestId));
|
|
239
|
+
});
|
|
189
240
|
}
|
|
190
241
|
async list(status) {
|
|
191
|
-
return this.mutate((state, now) => {
|
|
242
|
+
return this.mutate((state, now) => {
|
|
243
|
+
refresh(state, now);
|
|
244
|
+
return state.requests
|
|
245
|
+
.filter((request) => status === undefined || request.status === status)
|
|
246
|
+
.map(clone);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async readStateForTesting() {
|
|
250
|
+
return this.read();
|
|
192
251
|
}
|
|
193
|
-
async readStateForTesting() { return this.read(); }
|
|
194
252
|
async decideTerminal(requestIdValue, identity, reasonValue, kind) {
|
|
195
253
|
const approver = await this.approverVerifier.authenticate(identity);
|
|
196
254
|
const requestId = identifier(requestIdValue, "Approval request id");
|
|
@@ -198,11 +256,21 @@ export class LocalApprovalInbox {
|
|
|
198
256
|
return this.mutate((state, now) => {
|
|
199
257
|
refresh(state, now);
|
|
200
258
|
const request = findRequest(state, requestId);
|
|
201
|
-
if (request.status === "executing" ||
|
|
259
|
+
if (request.status === "executing" ||
|
|
260
|
+
request.status === "executed" ||
|
|
261
|
+
request.status === "execution_failed")
|
|
202
262
|
throw new ApprovalInboxError("Execution has already started; approval cannot be revoked.");
|
|
203
263
|
if (request.status !== "pending" && request.status !== "approved")
|
|
204
264
|
throw new ApprovalInboxError("Approval is not active.");
|
|
205
|
-
const decision = {
|
|
265
|
+
const decision = {
|
|
266
|
+
id: identifier(`decision-${randomUUID().replace(/-/g, "")}`, "Approval decision id"),
|
|
267
|
+
kind,
|
|
268
|
+
approverId: approver.id,
|
|
269
|
+
principalId: approver.principalId,
|
|
270
|
+
independenceKey: approver.independenceKey,
|
|
271
|
+
reason,
|
|
272
|
+
decidedAt: now,
|
|
273
|
+
};
|
|
206
274
|
request.decisions.push(decision);
|
|
207
275
|
request.status = kind === "reject" ? "rejected" : "revoked";
|
|
208
276
|
appendAudit(state, kind === "reject" ? "request.rejected" : "request.revoked", request, approver.id, now);
|
|
@@ -229,31 +297,67 @@ export class LocalApprovalInbox {
|
|
|
229
297
|
}
|
|
230
298
|
}
|
|
231
299
|
async mutate(operation) {
|
|
232
|
-
return withFileLock(this.path, async () => {
|
|
300
|
+
return withFileLock(this.path, async () => {
|
|
301
|
+
const state = await this.read();
|
|
302
|
+
const result = await operation(state, this.timestamp());
|
|
303
|
+
await this.write(state);
|
|
304
|
+
return result;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
async write(state) {
|
|
308
|
+
await atomicWriteJson(this.path, validateState(state));
|
|
233
309
|
}
|
|
234
|
-
async write(state) { await atomicWriteJson(this.path, validateState(state)); }
|
|
235
310
|
async latestDurableFailedReceipt(actionId) {
|
|
236
311
|
try {
|
|
237
312
|
const record = await this.actionGateway.inspect(actionId);
|
|
238
|
-
return [...record.receipts]
|
|
313
|
+
return [...record.receipts]
|
|
314
|
+
.reverse()
|
|
315
|
+
.find((receipt) => receipt.status === "failed");
|
|
239
316
|
}
|
|
240
317
|
catch {
|
|
241
318
|
return undefined;
|
|
242
319
|
}
|
|
243
320
|
}
|
|
244
|
-
timestamp() {
|
|
245
|
-
|
|
321
|
+
timestamp() {
|
|
322
|
+
const value = this.now();
|
|
323
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
|
|
324
|
+
throw new ApprovalInboxError("Approval inbox clock is invalid.");
|
|
325
|
+
return value.toISOString();
|
|
326
|
+
}
|
|
246
327
|
}
|
|
247
328
|
export function createTestApprovalApproverVerifier() {
|
|
248
329
|
const issued = new WeakSet();
|
|
249
330
|
return {
|
|
250
|
-
verifier: {
|
|
251
|
-
|
|
252
|
-
|
|
331
|
+
verifier: {
|
|
332
|
+
async authenticate(identity) {
|
|
333
|
+
if (identity === null ||
|
|
334
|
+
typeof identity !== "object" ||
|
|
335
|
+
!issued.has(identity))
|
|
336
|
+
throw new ApprovalInboxError("Approver identity is not authenticated.");
|
|
337
|
+
return validateApprover(identity);
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
issue(input) {
|
|
341
|
+
const result = Object.freeze(validateApprover({
|
|
342
|
+
...input,
|
|
343
|
+
principalId: input.principalId ?? input.id,
|
|
344
|
+
}));
|
|
345
|
+
issued.add(result);
|
|
346
|
+
return result;
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
export function approvalPolicyHash(value) {
|
|
351
|
+
return policyHash(validatePolicy(value));
|
|
352
|
+
}
|
|
353
|
+
function emptyState() {
|
|
354
|
+
return {
|
|
355
|
+
schemaVersion: 1,
|
|
356
|
+
requests: [],
|
|
357
|
+
auditAnchor: sha256("ghostapi.approval.audit.v1"),
|
|
358
|
+
audit: [],
|
|
253
359
|
};
|
|
254
360
|
}
|
|
255
|
-
export function approvalPolicyHash(value) { return policyHash(validatePolicy(value)); }
|
|
256
|
-
function emptyState() { return { schemaVersion: 1, requests: [], auditAnchor: sha256("ghostapi.approval.audit.v1"), audit: [] }; }
|
|
257
361
|
function validateState(value) {
|
|
258
362
|
const state = object(value, "Approval inbox store must be an object.");
|
|
259
363
|
exactKeys(state, ["schemaVersion", "requests", "auditAnchor", "audit"], "Approval inbox store");
|
|
@@ -269,10 +373,28 @@ function validateState(value) {
|
|
|
269
373
|
}
|
|
270
374
|
function validateRequest(value) {
|
|
271
375
|
const request = object(value, "Approval request is invalid.");
|
|
272
|
-
exactKeys(request, [
|
|
376
|
+
exactKeys(request, [
|
|
377
|
+
"id",
|
|
378
|
+
"action",
|
|
379
|
+
"actionHash",
|
|
380
|
+
"risk",
|
|
381
|
+
"context",
|
|
382
|
+
"policy",
|
|
383
|
+
"display",
|
|
384
|
+
"status",
|
|
385
|
+
"createdAt",
|
|
386
|
+
"expiresAt",
|
|
387
|
+
"escalationAt",
|
|
388
|
+
"decisions",
|
|
389
|
+
"artifact",
|
|
390
|
+
"consumedAt",
|
|
391
|
+
"executionReceiptHash",
|
|
392
|
+
"supersedesRequestId",
|
|
393
|
+
], "Approval request", ["artifact", "consumedAt", "executionReceiptHash", "supersedesRequestId"]);
|
|
273
394
|
const action = validateActionEnvelope(request.action);
|
|
274
395
|
const actionDigest = hash(request.actionHash, "Approval action hash");
|
|
275
|
-
if (actionHash(action) !== actionDigest ||
|
|
396
|
+
if (actionHash(action) !== actionDigest ||
|
|
397
|
+
classify(action) !== risk(request.risk))
|
|
276
398
|
throw new ApprovalInboxError("Approval request action is invalid.");
|
|
277
399
|
const context = validateContext(request.context);
|
|
278
400
|
const policy = policyRecord(request.policy);
|
|
@@ -281,41 +403,148 @@ function validateRequest(value) {
|
|
|
281
403
|
const createdAt = timestamp(request.createdAt, "Approval creation time");
|
|
282
404
|
const expiresAt = timestamp(request.expiresAt, "Approval expiry");
|
|
283
405
|
const escalationAt = timestamp(request.escalationAt, "Approval escalation time");
|
|
284
|
-
if (!validStatus(request.status) ||
|
|
406
|
+
if (!validStatus(request.status) ||
|
|
407
|
+
Date.parse(expiresAt) <= Date.parse(createdAt) ||
|
|
408
|
+
Date.parse(escalationAt) > Date.parse(expiresAt))
|
|
285
409
|
throw new ApprovalInboxError("Approval request lifecycle is invalid.");
|
|
286
|
-
const artifact = request.artifact === undefined
|
|
287
|
-
|
|
410
|
+
const artifact = request.artifact === undefined
|
|
411
|
+
? undefined
|
|
412
|
+
: validateArtifact(request.artifact, actionDigest);
|
|
413
|
+
if ((request.status === "approved" ||
|
|
414
|
+
request.status === "executing" ||
|
|
415
|
+
request.status === "executed" ||
|
|
416
|
+
request.status === "execution_failed") &&
|
|
417
|
+
artifact === undefined)
|
|
288
418
|
throw new ApprovalInboxError("Approved requests require an approval artifact.");
|
|
289
|
-
const consumedAt = request.consumedAt === undefined
|
|
290
|
-
|
|
419
|
+
const consumedAt = request.consumedAt === undefined
|
|
420
|
+
? undefined
|
|
421
|
+
: timestamp(request.consumedAt, "Approval consumption time");
|
|
422
|
+
if ((request.status === "executing" ||
|
|
423
|
+
request.status === "executed" ||
|
|
424
|
+
request.status === "execution_failed") &&
|
|
425
|
+
consumedAt === undefined)
|
|
291
426
|
throw new ApprovalInboxError("Executed approvals require consumption metadata.");
|
|
292
|
-
const executionReceiptHash = request.executionReceiptHash === undefined
|
|
427
|
+
const executionReceiptHash = request.executionReceiptHash === undefined
|
|
428
|
+
? undefined
|
|
429
|
+
: hash(request.executionReceiptHash, "Approval execution receipt hash");
|
|
293
430
|
if (request.status === "executed" && executionReceiptHash === undefined)
|
|
294
431
|
throw new ApprovalInboxError("Executed approvals require an action receipt hash.");
|
|
295
|
-
return {
|
|
432
|
+
return {
|
|
433
|
+
id: identifier(request.id, "Approval request id"),
|
|
434
|
+
action,
|
|
435
|
+
actionHash: actionDigest,
|
|
436
|
+
risk: risk(request.risk),
|
|
437
|
+
context,
|
|
438
|
+
policy,
|
|
439
|
+
display: displayValue,
|
|
440
|
+
status: request.status,
|
|
441
|
+
createdAt,
|
|
442
|
+
expiresAt,
|
|
443
|
+
escalationAt,
|
|
444
|
+
decisions,
|
|
445
|
+
...(artifact === undefined ? {} : { artifact }),
|
|
446
|
+
...(consumedAt === undefined ? {} : { consumedAt }),
|
|
447
|
+
...(executionReceiptHash === undefined ? {} : { executionReceiptHash }),
|
|
448
|
+
...(request.supersedesRequestId === undefined
|
|
449
|
+
? {}
|
|
450
|
+
: {
|
|
451
|
+
supersedesRequestId: identifier(request.supersedesRequestId, "Superseded approval request id"),
|
|
452
|
+
}),
|
|
453
|
+
};
|
|
296
454
|
}
|
|
297
455
|
function validateAudit(value) {
|
|
298
456
|
const record = object(value, "Approval audit record is invalid.");
|
|
299
|
-
exactKeys(record, [
|
|
300
|
-
|
|
301
|
-
|
|
457
|
+
exactKeys(record, [
|
|
458
|
+
"sequence",
|
|
459
|
+
"event",
|
|
460
|
+
"requestId",
|
|
461
|
+
"actionHash",
|
|
462
|
+
"actionReceiptHash",
|
|
463
|
+
"actorId",
|
|
464
|
+
"timestamp",
|
|
465
|
+
"previousHash",
|
|
466
|
+
"recordHash",
|
|
467
|
+
], "Approval audit record", ["actionReceiptHash"]);
|
|
468
|
+
const base = {
|
|
469
|
+
sequence: positive(record.sequence, "Approval audit sequence"),
|
|
470
|
+
event: text(record.event, "Approval audit event", 80),
|
|
471
|
+
requestId: identifier(record.requestId, "Approval audit request id"),
|
|
472
|
+
actionHash: hash(record.actionHash, "Approval audit action hash"),
|
|
473
|
+
...(record.actionReceiptHash === undefined
|
|
474
|
+
? {}
|
|
475
|
+
: {
|
|
476
|
+
actionReceiptHash: hash(record.actionReceiptHash, "Approval audit receipt hash"),
|
|
477
|
+
}),
|
|
478
|
+
actorId: identifier(record.actorId, "Approval audit actor id"),
|
|
479
|
+
timestamp: timestamp(record.timestamp, "Approval audit timestamp"),
|
|
480
|
+
previousHash: hash(record.previousHash, "Approval audit previous hash"),
|
|
481
|
+
};
|
|
482
|
+
if (hash(record.recordHash, "Approval audit record hash") !==
|
|
483
|
+
sha256(canonical(base)))
|
|
302
484
|
throw new ApprovalInboxError("Approval audit record hash is invalid.");
|
|
303
485
|
return { ...base, recordHash: record.recordHash };
|
|
304
486
|
}
|
|
305
|
-
function validateAuditChain(anchor, audit) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
487
|
+
function validateAuditChain(anchor, audit) {
|
|
488
|
+
let previous = anchor;
|
|
489
|
+
let sequence = 1;
|
|
490
|
+
for (const record of audit) {
|
|
491
|
+
if (record.sequence !== sequence || record.previousHash !== previous)
|
|
492
|
+
throw new ApprovalInboxError("Approval audit chain is invalid.");
|
|
493
|
+
previous = record.recordHash;
|
|
494
|
+
sequence += 1;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
311
497
|
function validatePolicy(value) {
|
|
312
498
|
const policy = object(value, "Approval policy is invalid.");
|
|
313
|
-
exactKeys(policy, [
|
|
499
|
+
exactKeys(policy, [
|
|
500
|
+
"schemaVersion",
|
|
501
|
+
"kind",
|
|
502
|
+
"id",
|
|
503
|
+
"version",
|
|
504
|
+
"allowedEnvironments",
|
|
505
|
+
"allowedActors",
|
|
506
|
+
"allowedResources",
|
|
507
|
+
"maxAmountMinor",
|
|
508
|
+
"minimumConfidence",
|
|
509
|
+
"criticalRisks",
|
|
510
|
+
"velocity",
|
|
511
|
+
"approvalTtlMs",
|
|
512
|
+
"escalationTimeoutMs",
|
|
513
|
+
], "Approval policy", ["allowedActors", "allowedResources", "maxAmountMinor"]);
|
|
314
514
|
if (policy.schemaVersion !== 1 || policy.kind !== "ghostapi.approval-policy")
|
|
315
515
|
throw new ApprovalInboxError("Unsupported approval policy schema.");
|
|
316
516
|
const velocity = object(policy.velocity, "Approval velocity policy is invalid.");
|
|
317
517
|
exactKeys(velocity, ["maxActions", "windowMs"], "Approval velocity policy");
|
|
318
|
-
const result = {
|
|
518
|
+
const result = {
|
|
519
|
+
schemaVersion: 1,
|
|
520
|
+
kind: "ghostapi.approval-policy",
|
|
521
|
+
id: identifier(policy.id, "Approval policy id"),
|
|
522
|
+
version: positive(policy.version, "Approval policy version"),
|
|
523
|
+
allowedEnvironments: identifiers(policy.allowedEnvironments, "Approval allowed environments"),
|
|
524
|
+
...(policy.allowedActors === undefined
|
|
525
|
+
? {}
|
|
526
|
+
: {
|
|
527
|
+
allowedActors: identifiers(policy.allowedActors, "Approval allowed actors"),
|
|
528
|
+
}),
|
|
529
|
+
...(policy.allowedResources === undefined
|
|
530
|
+
? {}
|
|
531
|
+
: {
|
|
532
|
+
allowedResources: identifiers(policy.allowedResources, "Approval allowed resources"),
|
|
533
|
+
}),
|
|
534
|
+
...(policy.maxAmountMinor === undefined
|
|
535
|
+
? {}
|
|
536
|
+
: {
|
|
537
|
+
maxAmountMinor: nonNegative(policy.maxAmountMinor, "Approval max amount"),
|
|
538
|
+
}),
|
|
539
|
+
minimumConfidence: percentage(policy.minimumConfidence, "Approval minimum confidence"),
|
|
540
|
+
criticalRisks: risks(policy.criticalRisks),
|
|
541
|
+
velocity: {
|
|
542
|
+
maxActions: positive(velocity.maxActions, "Approval velocity maximum"),
|
|
543
|
+
windowMs: boundedMs(velocity.windowMs, "Approval velocity window"),
|
|
544
|
+
},
|
|
545
|
+
approvalTtlMs: boundedMs(policy.approvalTtlMs, "Approval TTL"),
|
|
546
|
+
escalationTimeoutMs: boundedMs(policy.escalationTimeoutMs, "Approval escalation timeout"),
|
|
547
|
+
};
|
|
319
548
|
if (result.escalationTimeoutMs > result.approvalTtlMs)
|
|
320
549
|
throw new ApprovalInboxError("Approval escalation timeout cannot exceed approval TTL.");
|
|
321
550
|
return result;
|
|
@@ -325,88 +554,375 @@ function evaluate(policy, action, context, history, now, currentId) {
|
|
|
325
554
|
const actionRisk = classify(action);
|
|
326
555
|
if (!policy.allowedEnvironments.includes(action.project.environment))
|
|
327
556
|
reasons.push("environment is not approved");
|
|
328
|
-
if (policy.allowedActors !== undefined &&
|
|
557
|
+
if (policy.allowedActors !== undefined &&
|
|
558
|
+
!policy.allowedActors.includes(action.actor.id))
|
|
329
559
|
reasons.push("actor is not approved");
|
|
330
|
-
if (policy.allowedResources !== undefined &&
|
|
560
|
+
if (policy.allowedResources !== undefined &&
|
|
561
|
+
!policy.allowedResources.includes(action.resource.id))
|
|
331
562
|
reasons.push("resource is not approved");
|
|
332
|
-
if (policy.maxAmountMinor !== undefined &&
|
|
563
|
+
if (policy.maxAmountMinor !== undefined &&
|
|
564
|
+
(context.amountMinor === undefined ||
|
|
565
|
+
context.amountMinor > policy.maxAmountMinor))
|
|
333
566
|
reasons.push("amount is missing or exceeds the policy limit");
|
|
334
567
|
const cutoff = Date.parse(now) - policy.velocity.windowMs;
|
|
335
|
-
if (history.filter((request) => request.id !== currentId &&
|
|
568
|
+
if (history.filter((request) => request.id !== currentId &&
|
|
569
|
+
request.action.actor.id === action.actor.id &&
|
|
570
|
+
Date.parse(request.createdAt) >= cutoff &&
|
|
571
|
+
request.status !== "superseded").length >= policy.velocity.maxActions)
|
|
336
572
|
reasons.push("action velocity limit was reached");
|
|
337
|
-
const requiredApprovals = policy.criticalRisks.includes(actionRisk) ||
|
|
338
|
-
|
|
573
|
+
const requiredApprovals = policy.criticalRisks.includes(actionRisk) ||
|
|
574
|
+
context.confidence < policy.minimumConfidence
|
|
575
|
+
? 2
|
|
576
|
+
: 1;
|
|
577
|
+
reasons.push(requiredApprovals === 2
|
|
578
|
+
? "independent two-person approval is required"
|
|
579
|
+
: "independent human approval is required");
|
|
339
580
|
return { allowed: reasons.length === 1, requiredApprovals, reasons };
|
|
340
581
|
}
|
|
341
|
-
function display(action, context, policyReason) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
582
|
+
function display(action, context, policyReason) {
|
|
583
|
+
return {
|
|
584
|
+
intent: "Update a local synthetic subscription failure workflow.",
|
|
585
|
+
target: `${action.resource.type}/${action.resource.id}`,
|
|
586
|
+
normalizedArgumentDiff: { before: {}, after: clone(action.arguments) },
|
|
587
|
+
expectedSideEffects: [...action.expectedSideEffects],
|
|
588
|
+
reversibility: action.reversibility,
|
|
589
|
+
impact: {
|
|
590
|
+
amountMinor: context.amountMinor ?? null,
|
|
591
|
+
amountKnown: context.amountMinor !== undefined,
|
|
592
|
+
irreversible: action.reversibility === "none",
|
|
593
|
+
},
|
|
594
|
+
policyReason,
|
|
595
|
+
evidenceHash: action.evidence.hash,
|
|
596
|
+
simulation: {
|
|
597
|
+
status: "passed",
|
|
598
|
+
summary: "Synthetic adapter preflight found the local target world.",
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function displayRecord(value, action, context) {
|
|
603
|
+
const candidate = object(value, "Approval display is invalid.");
|
|
604
|
+
exactKeys(candidate, [
|
|
605
|
+
"intent",
|
|
606
|
+
"target",
|
|
607
|
+
"normalizedArgumentDiff",
|
|
608
|
+
"expectedSideEffects",
|
|
609
|
+
"reversibility",
|
|
610
|
+
"impact",
|
|
611
|
+
"policyReason",
|
|
612
|
+
"evidenceHash",
|
|
613
|
+
"simulation",
|
|
614
|
+
], "Approval display");
|
|
615
|
+
const generated = display(action, context, array(candidate.policyReason, "Approval policy reason", 16).map((item) => text(item, "Approval policy reason", 200)));
|
|
616
|
+
if (canonical(candidate) !== canonical(generated))
|
|
617
|
+
throw new ApprovalInboxError("Approval display does not match normalized action data.");
|
|
618
|
+
return generated;
|
|
619
|
+
}
|
|
620
|
+
async function createArtifact(request, now, signer) {
|
|
621
|
+
return signer.issue({
|
|
622
|
+
schemaVersion: 1,
|
|
623
|
+
kind: "ghostapi.action-approval",
|
|
624
|
+
approvalId: request.id,
|
|
625
|
+
actionHash: request.actionHash,
|
|
626
|
+
approvedBy: "approval-inbox",
|
|
627
|
+
approvedAt: now,
|
|
628
|
+
expiresAt: request.expiresAt,
|
|
629
|
+
nonce: `inbox-${request.actionHash.slice(0, 32)}`,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
function validateArtifact(value, actionDigest) {
|
|
633
|
+
const artifact = object(value, "Approval artifact is invalid.");
|
|
634
|
+
exactKeys(artifact, [
|
|
635
|
+
"schemaVersion",
|
|
636
|
+
"kind",
|
|
637
|
+
"approvalId",
|
|
638
|
+
"actionHash",
|
|
639
|
+
"approvedBy",
|
|
640
|
+
"approvedAt",
|
|
641
|
+
"expiresAt",
|
|
642
|
+
"nonce",
|
|
643
|
+
"provenance",
|
|
644
|
+
], "Approval artifact");
|
|
645
|
+
if (artifact.schemaVersion !== 1 ||
|
|
646
|
+
artifact.kind !== "ghostapi.action-approval" ||
|
|
647
|
+
artifact.actionHash !== actionDigest ||
|
|
648
|
+
artifact.approvedBy !== "approval-inbox")
|
|
649
|
+
throw new ApprovalInboxError("Approval artifact is invalid.");
|
|
650
|
+
const provenance = object(artifact.provenance, "Approval artifact provenance is invalid.");
|
|
651
|
+
exactKeys(provenance, ["kind", "keyId", "signature"], "Approval artifact provenance");
|
|
652
|
+
if (provenance.kind !== "ghostapi.ed25519-signature" ||
|
|
653
|
+
typeof provenance.signature !== "string" ||
|
|
654
|
+
!/^[A-Za-z0-9_-]{86}$/.test(provenance.signature))
|
|
655
|
+
throw new ApprovalInboxError("Approval artifact provenance is invalid.");
|
|
656
|
+
return {
|
|
657
|
+
schemaVersion: 1,
|
|
658
|
+
kind: "ghostapi.action-approval",
|
|
659
|
+
approvalId: identifier(artifact.approvalId, "Approval artifact id"),
|
|
660
|
+
actionHash: actionDigest,
|
|
661
|
+
approvedBy: "approval-inbox",
|
|
662
|
+
approvedAt: timestamp(artifact.approvedAt, "Approval artifact issue time"),
|
|
663
|
+
expiresAt: timestamp(artifact.expiresAt, "Approval artifact expiry"),
|
|
664
|
+
nonce: identifier(artifact.nonce, "Approval artifact nonce"),
|
|
665
|
+
provenance: {
|
|
666
|
+
kind: "ghostapi.ed25519-signature",
|
|
667
|
+
keyId: identifier(provenance.keyId, "Approval artifact key id"),
|
|
668
|
+
signature: provenance.signature,
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function policyRecord(value) {
|
|
673
|
+
const policy = object(value, "Approval policy record is invalid.");
|
|
674
|
+
exactKeys(policy, ["id", "version", "hash", "requiredApprovals"], "Approval policy record");
|
|
675
|
+
const requiredApprovals = policy.requiredApprovals;
|
|
676
|
+
if (requiredApprovals !== 1 && requiredApprovals !== 2)
|
|
677
|
+
throw new ApprovalInboxError("Approval requirement is invalid.");
|
|
678
|
+
return {
|
|
679
|
+
id: identifier(policy.id, "Approval policy id"),
|
|
680
|
+
version: positive(policy.version, "Approval policy version"),
|
|
681
|
+
hash: hash(policy.hash, "Approval policy hash"),
|
|
682
|
+
requiredApprovals,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function validateContext(value) {
|
|
686
|
+
const context = object(value, "Approval context is invalid.");
|
|
687
|
+
exactKeys(context, ["confidence", "amountMinor"], "Approval context", [
|
|
688
|
+
"amountMinor",
|
|
689
|
+
]);
|
|
690
|
+
return {
|
|
691
|
+
confidence: percentage(context.confidence, "Approval confidence"),
|
|
692
|
+
...(context.amountMinor === undefined
|
|
693
|
+
? {}
|
|
694
|
+
: { amountMinor: nonNegative(context.amountMinor, "Approval amount") }),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function validateApprover(value) {
|
|
698
|
+
const approver = object(value, "Approver identity is invalid.");
|
|
699
|
+
exactKeys(approver, ["id", "principalId", "independenceKey"], "Approver identity");
|
|
700
|
+
return {
|
|
701
|
+
id: identifier(approver.id, "Approver id"),
|
|
702
|
+
principalId: identifier(approver.principalId, "Approver principal id"),
|
|
703
|
+
independenceKey: identifier(approver.independenceKey, "Approver independence key"),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function validateDecision(value) {
|
|
707
|
+
const decision = object(value, "Approval decision is invalid.");
|
|
708
|
+
exactKeys(decision, [
|
|
709
|
+
"id",
|
|
710
|
+
"kind",
|
|
711
|
+
"approverId",
|
|
712
|
+
"principalId",
|
|
713
|
+
"independenceKey",
|
|
714
|
+
"reason",
|
|
715
|
+
"decidedAt",
|
|
716
|
+
], "Approval decision", ["reason"]);
|
|
717
|
+
if (decision.kind !== "approve" &&
|
|
718
|
+
decision.kind !== "reject" &&
|
|
719
|
+
decision.kind !== "revoke")
|
|
720
|
+
throw new ApprovalInboxError("Approval decision kind is invalid.");
|
|
721
|
+
return {
|
|
722
|
+
id: identifier(decision.id, "Approval decision id"),
|
|
723
|
+
kind: decision.kind,
|
|
724
|
+
approverId: identifier(decision.approverId, "Approver id"),
|
|
725
|
+
principalId: identifier(decision.principalId, "Approver principal id"),
|
|
726
|
+
independenceKey: identifier(decision.independenceKey, "Approver independence key"),
|
|
727
|
+
...(decision.reason === undefined
|
|
728
|
+
? {}
|
|
729
|
+
: { reason: text(decision.reason, "Approval decision reason", 200) }),
|
|
730
|
+
decidedAt: timestamp(decision.decidedAt, "Approval decision time"),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function appendAudit(state, event, request, actorId, timestampValue, actionReceiptHash) {
|
|
734
|
+
const previous = state.audit.at(-1)?.recordHash ?? state.auditAnchor;
|
|
735
|
+
const base = {
|
|
736
|
+
sequence: state.audit.length + 1,
|
|
737
|
+
event,
|
|
738
|
+
requestId: request.id,
|
|
739
|
+
actionHash: request.actionHash,
|
|
740
|
+
...(actionReceiptHash === undefined ? {} : { actionReceiptHash }),
|
|
741
|
+
actorId: identifier(actorId, "Approval audit actor id"),
|
|
742
|
+
timestamp: timestampValue,
|
|
743
|
+
previousHash: previous,
|
|
744
|
+
};
|
|
745
|
+
state.audit.push({ ...base, recordHash: sha256(canonical(base)) });
|
|
746
|
+
}
|
|
747
|
+
function refresh(state, now) {
|
|
748
|
+
for (const request of state.requests) {
|
|
749
|
+
if (request.status === "pending" &&
|
|
750
|
+
Date.parse(now) >= Date.parse(request.escalationAt)) {
|
|
751
|
+
request.status = "timed_out";
|
|
752
|
+
appendAudit(state, "request.timed_out", request, "approval-inbox", now);
|
|
753
|
+
}
|
|
754
|
+
else if ((request.status === "pending" || request.status === "approved") &&
|
|
755
|
+
Date.parse(now) >= Date.parse(request.expiresAt)) {
|
|
756
|
+
request.status = "expired";
|
|
757
|
+
appendAudit(state, "request.expired", request, "approval-inbox", now);
|
|
758
|
+
}
|
|
363
759
|
}
|
|
364
|
-
}
|
|
365
|
-
function classify(action) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return
|
|
382
|
-
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
760
|
+
}
|
|
761
|
+
function classify(action) {
|
|
762
|
+
if (action.provider === "ghostapi-synthetic" &&
|
|
763
|
+
action.operation === "synthetic.subscription_failure")
|
|
764
|
+
return "update";
|
|
765
|
+
throw new ApprovalInboxError("No approval risk taxonomy rule exists for this action.");
|
|
766
|
+
}
|
|
767
|
+
function risk(value) {
|
|
768
|
+
if (value !== "read" &&
|
|
769
|
+
value !== "create" &&
|
|
770
|
+
value !== "update" &&
|
|
771
|
+
value !== "communicate" &&
|
|
772
|
+
value !== "money_movement" &&
|
|
773
|
+
value !== "delete" &&
|
|
774
|
+
value !== "permission_change" &&
|
|
775
|
+
value !== "deployment")
|
|
776
|
+
throw new ApprovalInboxError("Approval risk is invalid.");
|
|
777
|
+
return value;
|
|
778
|
+
}
|
|
779
|
+
function risks(value) {
|
|
780
|
+
const parsed = array(value, "Approval critical risks", 8).map(risk);
|
|
781
|
+
unique(parsed, "Approval critical risks");
|
|
782
|
+
return parsed.sort();
|
|
783
|
+
}
|
|
784
|
+
function requirePending(request) {
|
|
785
|
+
if (request.status !== "pending")
|
|
786
|
+
throw new ApprovalInboxError("Approval is not pending.");
|
|
787
|
+
}
|
|
788
|
+
function assertIndependent(request, approver) {
|
|
789
|
+
if (approver.id === request.action.actor.id ||
|
|
790
|
+
approver.principalId === request.action.actor.id ||
|
|
791
|
+
approver.principalId === request.action.actor.workloadId ||
|
|
792
|
+
approver.independenceKey === request.action.actor.id ||
|
|
793
|
+
approver.independenceKey === request.action.actor.workloadId)
|
|
794
|
+
throw new ApprovalInboxError("Action workload cannot approve its own action.");
|
|
795
|
+
}
|
|
796
|
+
function findRequest(state, requestId) {
|
|
797
|
+
const request = state.requests.find((candidate) => candidate.id === requestId);
|
|
798
|
+
if (request === undefined)
|
|
799
|
+
throw new ApprovalInboxError("Approval request was not found.");
|
|
800
|
+
return request;
|
|
801
|
+
}
|
|
802
|
+
function validStatus(value) {
|
|
803
|
+
return (value === "pending" ||
|
|
804
|
+
value === "approved" ||
|
|
805
|
+
value === "rejected" ||
|
|
806
|
+
value === "revoked" ||
|
|
807
|
+
value === "timed_out" ||
|
|
808
|
+
value === "expired" ||
|
|
809
|
+
value === "executing" ||
|
|
810
|
+
value === "executed" ||
|
|
811
|
+
value === "execution_failed" ||
|
|
812
|
+
value === "superseded");
|
|
813
|
+
}
|
|
814
|
+
function policyHash(policy) {
|
|
815
|
+
return sha256(canonical(policy));
|
|
816
|
+
}
|
|
817
|
+
function canonical(value) {
|
|
818
|
+
if (value === null ||
|
|
819
|
+
typeof value === "boolean" ||
|
|
820
|
+
typeof value === "string" ||
|
|
821
|
+
typeof value === "number")
|
|
822
|
+
return JSON.stringify(value);
|
|
823
|
+
if (Array.isArray(value))
|
|
824
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
825
|
+
if (typeof value === "object")
|
|
826
|
+
return `{${Object.keys(value)
|
|
827
|
+
.sort()
|
|
828
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
829
|
+
.join(",")}}`;
|
|
830
|
+
throw new ApprovalInboxError("Approval data must be JSON.");
|
|
831
|
+
}
|
|
832
|
+
function sha256(value) {
|
|
833
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
834
|
+
}
|
|
835
|
+
function identifier(value, label) {
|
|
836
|
+
if (typeof value !== "string" ||
|
|
837
|
+
!IDENTIFIER.test(value) ||
|
|
838
|
+
sanitizeSecretString(value) !== value)
|
|
839
|
+
throw new ApprovalInboxError(`${label} must be a safe identifier.`);
|
|
840
|
+
return value;
|
|
841
|
+
}
|
|
842
|
+
function hash(value, label) {
|
|
843
|
+
if (typeof value !== "string" || !HASH.test(value))
|
|
844
|
+
throw new ApprovalInboxError(`${label} must be a SHA-256 hash.`);
|
|
845
|
+
return value;
|
|
846
|
+
}
|
|
847
|
+
function timestamp(value, label) {
|
|
848
|
+
if (typeof value !== "string" ||
|
|
849
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
|
|
850
|
+
!Number.isFinite(Date.parse(value)))
|
|
851
|
+
throw new ApprovalInboxError(`${label} must be an ISO UTC timestamp.`);
|
|
852
|
+
return value;
|
|
853
|
+
}
|
|
854
|
+
function positive(value, label) {
|
|
855
|
+
if (typeof value !== "number" ||
|
|
856
|
+
!Number.isInteger(value) ||
|
|
857
|
+
value < 1 ||
|
|
858
|
+
value > 100_000)
|
|
859
|
+
throw new ApprovalInboxError(`${label} is invalid.`);
|
|
860
|
+
return value;
|
|
861
|
+
}
|
|
862
|
+
function nonNegative(value, label) {
|
|
863
|
+
if (typeof value !== "number" ||
|
|
864
|
+
!Number.isInteger(value) ||
|
|
865
|
+
value < 0 ||
|
|
866
|
+
value > Number.MAX_SAFE_INTEGER)
|
|
867
|
+
throw new ApprovalInboxError(`${label} is invalid.`);
|
|
868
|
+
return value;
|
|
869
|
+
}
|
|
870
|
+
function percentage(value, label) {
|
|
871
|
+
if (typeof value !== "number" ||
|
|
872
|
+
!Number.isInteger(value) ||
|
|
873
|
+
value < 0 ||
|
|
874
|
+
value > 100)
|
|
875
|
+
throw new ApprovalInboxError(`${label} must be an integer from 0 to 100.`);
|
|
876
|
+
return value;
|
|
877
|
+
}
|
|
878
|
+
function boundedMs(value, label) {
|
|
879
|
+
if (typeof value !== "number" ||
|
|
880
|
+
!Number.isInteger(value) ||
|
|
881
|
+
value < 1_000 ||
|
|
882
|
+
value > 24 * 60 * 60 * 1000)
|
|
883
|
+
throw new ApprovalInboxError(`${label} is invalid.`);
|
|
884
|
+
return value;
|
|
885
|
+
}
|
|
886
|
+
function text(value, label, max) {
|
|
887
|
+
if (typeof value !== "string" ||
|
|
888
|
+
value.trim() === "" ||
|
|
889
|
+
value.length > max ||
|
|
890
|
+
/[\u0000-\u001f]/.test(value) ||
|
|
891
|
+
sanitizeSecretString(value) !== value)
|
|
892
|
+
throw new ApprovalInboxError(`${label} is invalid.`);
|
|
893
|
+
return value.trim();
|
|
894
|
+
}
|
|
895
|
+
function identifiers(value, label) {
|
|
896
|
+
const result = array(value, label, 100).map((entry) => identifier(entry, label));
|
|
897
|
+
if (result.length === 0)
|
|
898
|
+
throw new ApprovalInboxError(`${label} must not be empty.`);
|
|
899
|
+
unique(result, label);
|
|
900
|
+
return result.sort();
|
|
901
|
+
}
|
|
902
|
+
function object(value, message) {
|
|
903
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
904
|
+
throw new ApprovalInboxError(message);
|
|
905
|
+
return value;
|
|
906
|
+
}
|
|
907
|
+
function array(value, label, max) {
|
|
908
|
+
if (!Array.isArray(value) || value.length > max)
|
|
909
|
+
throw new ApprovalInboxError(`${label} is invalid.`);
|
|
910
|
+
return value;
|
|
911
|
+
}
|
|
912
|
+
function exactKeys(value, keys, label, optional = []) {
|
|
913
|
+
for (const key of Object.keys(value))
|
|
914
|
+
if (!keys.includes(key) ||
|
|
915
|
+
(value[key] === undefined && !optional.includes(key)))
|
|
916
|
+
throw new ApprovalInboxError(`${label} contains unsupported field: ${key}`);
|
|
917
|
+
}
|
|
918
|
+
function unique(values, label) {
|
|
919
|
+
if (new Set(values).size !== values.length)
|
|
920
|
+
throw new ApprovalInboxError(`${label} must be unique.`);
|
|
921
|
+
}
|
|
922
|
+
function clone(value) {
|
|
923
|
+
return structuredClone(value);
|
|
924
|
+
}
|
|
925
|
+
function isErrorCode(error, code) {
|
|
926
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
927
|
+
}
|
|
412
928
|
//# sourceMappingURL=approvalInbox.js.map
|