open-multi-agent-kit 0.95.1 → 0.95.2
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 +17 -0
- package/README.md +1 -1
- package/dist/approvals/approval-receipt-store.d.ts +18 -0
- package/dist/approvals/approval-receipt-store.d.ts.map +1 -0
- package/dist/approvals/approval-receipt-store.js +169 -0
- package/dist/approvals/approval-receipt-store.js.map +1 -0
- package/dist/approvals/approval-receipt.d.ts +43 -0
- package/dist/approvals/approval-receipt.d.ts.map +1 -0
- package/dist/approvals/approval-receipt.js +208 -0
- package/dist/approvals/approval-receipt.js.map +1 -0
- package/dist/approvals/approval-verifier.d.ts +17 -0
- package/dist/approvals/approval-verifier.d.ts.map +1 -0
- package/dist/approvals/approval-verifier.js +23 -0
- package/dist/approvals/approval-verifier.js.map +1 -0
- package/dist/approvals/plannotator-approval-bridge.d.ts +58 -0
- package/dist/approvals/plannotator-approval-bridge.d.ts.map +1 -0
- package/dist/approvals/plannotator-approval-bridge.js +169 -0
- package/dist/approvals/plannotator-approval-bridge.js.map +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/package-doctor-cli.d.ts +13 -0
- package/dist/commands/package-doctor-cli.d.ts.map +1 -0
- package/dist/commands/package-doctor-cli.js +80 -0
- package/dist/commands/package-doctor-cli.js.map +1 -0
- package/dist/commands/router-feedback-cli.d.ts +11 -0
- package/dist/commands/router-feedback-cli.d.ts.map +1 -0
- package/dist/commands/router-feedback-cli.js +99 -0
- package/dist/commands/router-feedback-cli.js.map +1 -0
- package/dist/commands/stats-cli.d.ts +20 -0
- package/dist/commands/stats-cli.d.ts.map +1 -0
- package/dist/commands/stats-cli.js +99 -0
- package/dist/commands/stats-cli.js.map +1 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +4 -1
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +97 -29
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +583 -199
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +8 -2
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +9 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -1
- package/dist/core/compaction/transaction.js +17 -2
- package/dist/core/compaction/transaction.js.map +1 -1
- package/dist/core/context-budget-compressors.d.ts +50 -0
- package/dist/core/context-budget-compressors.d.ts.map +1 -0
- package/dist/core/context-budget-compressors.js +166 -0
- package/dist/core/context-budget-compressors.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +1 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +1 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt.js +18 -2
- package/dist/core/context-budget-system-prompt.js.map +1 -1
- package/dist/core/context-budget-v2-cache-disk.d.ts +109 -0
- package/dist/core/context-budget-v2-cache-disk.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-disk.js +343 -0
- package/dist/core/context-budget-v2-cache-disk.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +22 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +4 -4
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache.d.ts +1 -0
- package/dist/core/context-budget-v2-cache.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache.js +1 -0
- package/dist/core/context-budget-v2-cache.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +17 -14
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +1 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +45 -22
- package/dist/core/extensions/loader.js.bak-sharedjiti-20260812-111246 +553 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +4 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/mcp/client.d.ts +105 -0
- package/dist/core/mcp/client.d.ts.map +1 -0
- package/dist/core/mcp/client.js +198 -0
- package/dist/core/mcp/client.js.map +1 -0
- package/dist/core/mcp/config.d.ts +23 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +89 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/index.d.ts +15 -0
- package/dist/core/mcp/index.d.ts.map +1 -0
- package/dist/core/mcp/index.js +15 -0
- package/dist/core/mcp/index.js.map +1 -0
- package/dist/core/mcp/manager.d.ts +84 -0
- package/dist/core/mcp/manager.d.ts.map +1 -0
- package/dist/core/mcp/manager.js +173 -0
- package/dist/core/mcp/manager.js.map +1 -0
- package/dist/core/mcp/protocol.d.ts +59 -0
- package/dist/core/mcp/protocol.d.ts.map +1 -0
- package/dist/core/mcp/protocol.js +103 -0
- package/dist/core/mcp/protocol.js.map +1 -0
- package/dist/core/mcp/stdio-transport.d.ts +63 -0
- package/dist/core/mcp/stdio-transport.d.ts.map +1 -0
- package/dist/core/mcp/stdio-transport.js +132 -0
- package/dist/core/mcp/stdio-transport.js.map +1 -0
- package/dist/core/mcp/tools.d.ts +47 -0
- package/dist/core/mcp/tools.d.ts.map +1 -0
- package/dist/core/mcp/tools.js +102 -0
- package/dist/core/mcp/tools.js.map +1 -0
- package/dist/core/package-archive.d.ts +3 -0
- package/dist/core/package-archive.d.ts.map +1 -0
- package/dist/core/package-archive.js +161 -0
- package/dist/core/package-archive.js.map +1 -0
- package/dist/core/package-doctor-source-scan.d.ts +13 -0
- package/dist/core/package-doctor-source-scan.d.ts.map +1 -0
- package/dist/core/package-doctor-source-scan.js +124 -0
- package/dist/core/package-doctor-source-scan.js.map +1 -0
- package/dist/core/package-doctor.d.ts +41 -0
- package/dist/core/package-doctor.d.ts.map +1 -0
- package/dist/core/package-doctor.js +145 -0
- package/dist/core/package-doctor.js.map +1 -0
- package/dist/core/package-manager.d.ts +10 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +175 -42
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/package-manifest.d.ts +26 -0
- package/dist/core/package-manifest.d.ts.map +1 -0
- package/dist/core/package-manifest.js +85 -0
- package/dist/core/package-manifest.js.map +1 -0
- package/dist/core/pi-compat.d.ts +5 -0
- package/dist/core/pi-compat.d.ts.map +1 -0
- package/dist/core/pi-compat.js +18 -0
- package/dist/core/pi-compat.js.map +1 -0
- package/dist/core/reasoning-router-bias.d.ts +10 -14
- package/dist/core/reasoning-router-bias.d.ts.map +1 -1
- package/dist/core/reasoning-router-bias.js +22 -15
- package/dist/core/reasoning-router-bias.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts +9 -1
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +20 -5
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/redaction.d.ts +2 -5
- package/dist/core/redaction.d.ts.map +1 -1
- package/dist/core/redaction.js +14 -1
- package/dist/core/redaction.js.map +1 -1
- package/dist/core/repository-learning-scope.d.ts +10 -0
- package/dist/core/repository-learning-scope.d.ts.map +1 -0
- package/dist/core/repository-learning-scope.js +42 -0
- package/dist/core/repository-learning-scope.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +16 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/router-feedback-collector.d.ts +2 -1
- package/dist/core/router-feedback-collector.d.ts.map +1 -1
- package/dist/core/router-feedback-collector.js +2 -2
- package/dist/core/router-feedback-collector.js.map +1 -1
- package/dist/core/runtime-provenance.d.ts +54 -0
- package/dist/core/runtime-provenance.d.ts.map +1 -0
- package/dist/core/runtime-provenance.js +115 -0
- package/dist/core/runtime-provenance.js.map +1 -0
- package/dist/core/sandbox/backend.d.ts.map +1 -1
- package/dist/core/sandbox/backend.js +39 -21
- package/dist/core/sandbox/backend.js.map +1 -1
- package/dist/core/sandbox/default-policy.d.ts +4 -5
- package/dist/core/sandbox/default-policy.d.ts.map +1 -1
- package/dist/core/sandbox/default-policy.js +9 -11
- package/dist/core/sandbox/default-policy.js.map +1 -1
- package/dist/core/session-bash-runtime.d.ts +4 -4
- package/dist/core/session-bash-runtime.d.ts.map +1 -1
- package/dist/core/session-bash-runtime.js +6 -5
- package/dist/core/session-bash-runtime.js.map +1 -1
- package/dist/core/session-compaction-service.d.ts.map +1 -1
- package/dist/core/session-compaction-service.js +7 -7
- package/dist/core/session-compaction-service.js.map +1 -1
- package/dist/core/session-manager.d.ts +4 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +47 -7
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -4
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +11 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +9 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +10 -4
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +2 -2
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +2 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +15 -0
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/core/turn-metrics.d.ts +135 -0
- package/dist/core/turn-metrics.d.ts.map +1 -0
- package/dist/core/turn-metrics.js +261 -0
- package/dist/core/turn-metrics.js.map +1 -0
- package/dist/core/verified-bash-runtime.d.ts.map +1 -1
- package/dist/core/verified-bash-runtime.js +10 -4
- package/dist/core/verified-bash-runtime.js.map +1 -1
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +7 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -1
- package/dist/guardrails/replay-ledger-lock-owner.js +39 -0
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +18 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.js +2 -2
- package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +3 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +40 -12
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.d.ts +14 -2
- package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.js +106 -19
- package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +10 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +104 -17
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +1 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/compaction.md +25 -3
- package/docs/containerization.md +14 -2
- package/docs/docs.json +8 -0
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +75 -16
- package/docs/mcp.md +97 -0
- package/docs/metrics.md +87 -0
- package/docs/packages.md +25 -4
- package/docs/providers.md +1 -1
- package/docs/sdk.md +25 -4
- package/docs/settings.md +8 -7
- package/docs/usage.md +13 -3
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/plannotator-approval-bridge.ts +83 -0
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/README.md +21 -11
- package/examples/extensions/subagent/adaptive-agent-runtime.ts +9 -3
- package/examples/extensions/subagent/agents/planner.md +0 -2
- package/examples/extensions/subagent/agents/reviewer.md +0 -2
- package/examples/extensions/subagent/agents/scout.md +0 -2
- package/examples/extensions/subagent/agents/worker.md +0 -1
- package/examples/extensions/subagent/agents.test.ts +9 -0
- package/examples/extensions/subagent/agents.ts +11 -4
- package/examples/extensions/subagent/deadline-budget.test.ts +32 -0
- package/examples/extensions/subagent/deadline-budget.ts +25 -1
- package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +10 -0
- package/examples/extensions/subagent/index.ts +234 -55
- package/examples/extensions/subagent/subagent-execution-policy.test.ts +27 -0
- package/examples/extensions/subagent/subagent-extension-smoke.test.ts +167 -2
- package/examples/extensions/subagent/workflow-graph.test.ts +80 -0
- package/examples/extensions/subagent/workflow-graph.ts +89 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +253 -439
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.95.2] - 2026-08-15
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- macOS sessions now derive replay-ledger process identity from bounded BSD `ps -o lstart=` output, so replay-lock acquisition no longer fails closed during AgentSession startup on Darwin while Linux `/proc` behavior remains unchanged.
|
|
10
|
+
- The status rail now verifies connected MCP servers with protocol pings, marks dead processes as failed, and retries failed servers on a bounded slower cadence without spawning idle servers.
|
|
11
|
+
- Interactive UX now includes an empty-editor affordance hint, elapsed-time and interrupt details on the working indicator, transient information notices, width-safe editor scroll borders, and opt-in footer CPU/memory metrics.
|
|
12
|
+
- Extension tools can resolve timeouts from current context. The subagent extension uses this to remove task-count, concurrency, execution-budget, attempt, and outer tool-timeout caps in Ultra while preserving explicit cancellation and non-Ultra limits.
|
|
13
|
+
- The startup control panel now identifies the product with `WELCOME TO OMK` instead of legacy Pi agent branding.
|
|
14
|
+
- Context-budget prompts use compact metadata for valid zero-resource plans while retaining cache-state and legacy optimizer compatibility telemetry; diagnostic and non-empty plans keep full observability, and invalid budgets bypass the plan cache so diagnostics cannot be hidden by a prior hit.
|
|
15
|
+
- The workspace context-budget cache scans every persisted key/value, keeps credential-shaped entries in memory only, and removes unsafe legacy snapshots instead of persisting secret-shaped text.
|
|
16
|
+
- Exact context representations now reuse content-addressed cache entries across different queries and budget sizes instead of forcing avoidable misses.
|
|
17
|
+
- Opt-in reasoning-router learning now isolates default ledgers and compiled bias snapshots per repository or git worktree, captures explicit manual thinking-level overrides as bounded feedback, and keeps updates behind the shipped, deterministic `omk router-feedback compile-bias` between-session command. Snapshot loading rejects nonzero biases below the strong-evidence threshold, unsafe or inconsistent counts, and duplicate cells; compilation uses an exclusive randomized temporary file before atomic rename.
|
|
18
|
+
- Session bash now defaults to OS sandbox enforcement instead of ledger-only audit mode. macOS `sandbox-exec` and Linux `bwrap` restrict writes to the workspace/temp directories and disable network access; missing backends fail closed. Unwrapped `audit` and disabled `off` modes now require an explicit `OMK_BASH_SANDBOX` value, and unknown values resolve to `enforce`.
|
|
19
|
+
|
|
3
20
|
## [0.95.1] - 2026-08-01
|
|
4
21
|
|
|
5
22
|
### Added
|
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ npm test
|
|
|
170
170
|
|
|
171
171
|
See [Development](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/development.md) and [CONTRIBUTING.md](https://github.com/dmae97/omk/blob/main/CONTRIBUTING.md).
|
|
172
172
|
|
|
173
|
-
Release notes: [v0.95.
|
|
173
|
+
Release notes: [v0.95.2](https://github.com/dmae97/omk/blob/main/.github/RELEASE_NOTES_v0.95.2.md).
|
|
174
174
|
|
|
175
175
|
## License
|
|
176
176
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ApprovalReceipt } from "./approval-receipt.ts";
|
|
2
|
+
export interface ApprovalReceiptWriteResult {
|
|
3
|
+
path: string;
|
|
4
|
+
created: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare class ApprovalReceiptStore {
|
|
7
|
+
readonly rootDir: string;
|
|
8
|
+
constructor(rootDir: string);
|
|
9
|
+
private ensurePrivateDirectory;
|
|
10
|
+
private assertRootUnchanged;
|
|
11
|
+
private pathFor;
|
|
12
|
+
private openReceiptForRead;
|
|
13
|
+
write(receipt: ApprovalReceipt): ApprovalReceiptWriteResult;
|
|
14
|
+
has(receiptId: string): boolean;
|
|
15
|
+
read(receiptId: string): ApprovalReceipt;
|
|
16
|
+
listReceiptIds(): string[];
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=approval-receipt-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-receipt-store.d.ts","sourceRoot":"","sources":["../../src/approvals/approval-receipt-store.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,eAAe,EAAqD,MAAM,uBAAuB,CAAC;AAKhH,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CACjB;AAsDD,qBAAa,oBAAoB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,YAAY,OAAO,EAAE,MAAM,EAG1B;IAED,OAAO,CAAC,sBAAsB;IAa9B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,kBAAkB;IAW1B,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,0BAA0B,CAkC1D;IAED,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAe9B;IAED,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,CAUvC;IAED,cAAc,IAAI,MAAM,EAAE,CASzB;CACD","sourcesContent":["import {\n\tcloseSync,\n\tconstants,\n\tfchmodSync,\n\tfstatSync,\n\tfsyncSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\treaddirSync,\n\treadSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { type ApprovalReceipt, serializeApprovalReceipt, validateApprovalReceipt } from \"./approval-receipt.ts\";\n\nconst MAX_RECEIPT_BYTES = 1024 * 1024;\nconst NOFOLLOW = constants.O_NOFOLLOW ?? 0;\n\nexport interface ApprovalReceiptWriteResult {\n\tpath: string;\n\tcreated: boolean;\n}\n\ninterface RootIdentity {\n\tdev: number;\n\tino: number;\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n\treturn error instanceof Error && \"code\" in error && error.code === code;\n}\n\nfunction assertOwnedByCurrentUser(uid: number, label: string): void {\n\tconst getuid = process.getuid;\n\tif (typeof getuid === \"function\" && uid !== getuid()) {\n\t\tthrow new Error(`${label} is not owned by the current user`);\n\t}\n}\n\nfunction receiptFileName(receiptId: string): string {\n\tif (!/^approval-[a-f0-9]{32}$/u.test(receiptId)) throw new Error(\"Approval receipt id is invalid\");\n\treturn `${receiptId}.json`;\n}\n\nfunction assertPrivateReceiptFd(fd: number, filePath: string): void {\n\tconst stat = fstatSync(fd);\n\tif (!stat.isFile()) throw new Error(`Approval receipt is not a regular file: ${filePath}`);\n\tif (stat.nlink !== 1) throw new Error(`Approval receipt must not be hard-linked: ${filePath}`);\n\tassertOwnedByCurrentUser(stat.uid, `Approval receipt ${filePath}`);\n\tif ((stat.mode & 0o777) !== 0o600) {\n\t\tthrow new Error(`Approval receipt permissions must be 0600: ${filePath}`);\n\t}\n\tif (stat.size > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n}\n\nfunction readReceiptFromFd(fd: number, filePath: string): ApprovalReceipt {\n\tassertPrivateReceiptFd(fd, filePath);\n\tconst buffer = Buffer.alloc(MAX_RECEIPT_BYTES + 1);\n\tlet offset = 0;\n\twhile (offset < buffer.length) {\n\t\tconst read = readSync(fd, buffer, offset, buffer.length - offset, offset);\n\t\tif (read === 0) break;\n\t\toffset += read;\n\t}\n\tif (offset > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n\tassertPrivateReceiptFd(fd, filePath);\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(buffer.subarray(0, offset).toString(\"utf8\")) as unknown;\n\t} catch {\n\t\tthrow new Error(`Approval receipt is not valid JSON: ${filePath}`);\n\t}\n\treturn validateApprovalReceipt(value);\n}\n\nexport class ApprovalReceiptStore {\n\treadonly rootDir: string;\n\n\tconstructor(rootDir: string) {\n\t\tthis.rootDir = resolve(rootDir);\n\t\tthis.ensurePrivateDirectory();\n\t}\n\n\tprivate ensurePrivateDirectory(): RootIdentity {\n\t\tmkdirSync(this.rootDir, { recursive: true, mode: 0o700 });\n\t\tconst stat = lstatSync(this.rootDir);\n\t\tif (stat.isSymbolicLink() || !stat.isDirectory()) {\n\t\t\tthrow new Error(`Approval receipt root must be a real directory: ${this.rootDir}`);\n\t\t}\n\t\tassertOwnedByCurrentUser(stat.uid, `Approval receipt root ${this.rootDir}`);\n\t\tif ((stat.mode & 0o777) !== 0o700) {\n\t\t\tthrow new Error(`Approval receipt root permissions must be 0700: ${this.rootDir}`);\n\t\t}\n\t\treturn { dev: stat.dev, ino: stat.ino };\n\t}\n\n\tprivate assertRootUnchanged(expected: RootIdentity): void {\n\t\tconst current = this.ensurePrivateDirectory();\n\t\tif (current.dev !== expected.dev || current.ino !== expected.ino) {\n\t\t\tthrow new Error(\"Approval receipt root changed during the operation\");\n\t\t}\n\t}\n\n\tprivate pathFor(receiptId: string): string {\n\t\treturn join(this.rootDir, receiptFileName(receiptId));\n\t}\n\n\tprivate openReceiptForRead(filePath: string): number {\n\t\tconst fd = openSync(filePath, constants.O_RDONLY | NOFOLLOW);\n\t\ttry {\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t\treturn fd;\n\t\t} catch (error) {\n\t\t\tcloseSync(fd);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\twrite(receipt: ApprovalReceipt): ApprovalReceiptWriteResult {\n\t\tconst validated = validateApprovalReceipt(receipt);\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst filePath = this.pathFor(validated.core.receiptId);\n\t\tconst canonical = serializeApprovalReceipt(validated);\n\t\tconst bytes = Buffer.from(canonical, \"utf8\");\n\t\tif (bytes.byteLength > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o600);\n\t\t} catch (error) {\n\t\t\tif (!isErrno(error, \"EEXIST\")) throw error;\n\t\t\tconst existing = this.read(validated.core.receiptId);\n\t\t\tif (serializeApprovalReceipt(existing) !== canonical) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Approval receipt already exists with different content and is never overwritten: ${validated.core.receiptId}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t\treturn { path: filePath, created: false };\n\t\t}\n\n\t\ttry {\n\t\t\tfchmodSync(fd, 0o600);\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t\twriteFileSync(fd, bytes);\n\t\t\tfsyncSync(fd);\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn { path: filePath, created: true };\n\t}\n\n\thas(receiptId: string): boolean {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = this.openReceiptForRead(this.pathFor(receiptId));\n\t\t} catch (error) {\n\t\t\tif (isErrno(error, \"ENOENT\")) {\n\t\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tcloseSync(fd);\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn true;\n\t}\n\n\tread(receiptId: string): ApprovalReceipt {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst filePath = this.pathFor(receiptId);\n\t\tconst fd = this.openReceiptForRead(filePath);\n\t\ttry {\n\t\t\treturn readReceiptFromFd(fd, filePath);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t}\n\t}\n\n\tlistReceiptIds(): string[] {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst ids = readdirSync(this.rootDir)\n\t\t\t.map((name) => /^(approval-[a-f0-9]{32})\\.json$/u.exec(name)?.[1])\n\t\t\t.filter((id): id is string => id !== undefined)\n\t\t\t.sort((left, right) => left.localeCompare(right));\n\t\tfor (const id of ids) this.has(id);\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn ids;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { serializeApprovalReceipt, validateApprovalReceipt } from "./approval-receipt.js";
|
|
4
|
+
const MAX_RECEIPT_BYTES = 1024 * 1024;
|
|
5
|
+
const NOFOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
6
|
+
function isErrno(error, code) {
|
|
7
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
8
|
+
}
|
|
9
|
+
function assertOwnedByCurrentUser(uid, label) {
|
|
10
|
+
const getuid = process.getuid;
|
|
11
|
+
if (typeof getuid === "function" && uid !== getuid()) {
|
|
12
|
+
throw new Error(`${label} is not owned by the current user`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function receiptFileName(receiptId) {
|
|
16
|
+
if (!/^approval-[a-f0-9]{32}$/u.test(receiptId))
|
|
17
|
+
throw new Error("Approval receipt id is invalid");
|
|
18
|
+
return `${receiptId}.json`;
|
|
19
|
+
}
|
|
20
|
+
function assertPrivateReceiptFd(fd, filePath) {
|
|
21
|
+
const stat = fstatSync(fd);
|
|
22
|
+
if (!stat.isFile())
|
|
23
|
+
throw new Error(`Approval receipt is not a regular file: ${filePath}`);
|
|
24
|
+
if (stat.nlink !== 1)
|
|
25
|
+
throw new Error(`Approval receipt must not be hard-linked: ${filePath}`);
|
|
26
|
+
assertOwnedByCurrentUser(stat.uid, `Approval receipt ${filePath}`);
|
|
27
|
+
if ((stat.mode & 0o777) !== 0o600) {
|
|
28
|
+
throw new Error(`Approval receipt permissions must be 0600: ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
if (stat.size > MAX_RECEIPT_BYTES)
|
|
31
|
+
throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);
|
|
32
|
+
}
|
|
33
|
+
function readReceiptFromFd(fd, filePath) {
|
|
34
|
+
assertPrivateReceiptFd(fd, filePath);
|
|
35
|
+
const buffer = Buffer.alloc(MAX_RECEIPT_BYTES + 1);
|
|
36
|
+
let offset = 0;
|
|
37
|
+
while (offset < buffer.length) {
|
|
38
|
+
const read = readSync(fd, buffer, offset, buffer.length - offset, offset);
|
|
39
|
+
if (read === 0)
|
|
40
|
+
break;
|
|
41
|
+
offset += read;
|
|
42
|
+
}
|
|
43
|
+
if (offset > MAX_RECEIPT_BYTES)
|
|
44
|
+
throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);
|
|
45
|
+
assertPrivateReceiptFd(fd, filePath);
|
|
46
|
+
let value;
|
|
47
|
+
try {
|
|
48
|
+
value = JSON.parse(buffer.subarray(0, offset).toString("utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new Error(`Approval receipt is not valid JSON: ${filePath}`);
|
|
52
|
+
}
|
|
53
|
+
return validateApprovalReceipt(value);
|
|
54
|
+
}
|
|
55
|
+
export class ApprovalReceiptStore {
|
|
56
|
+
rootDir;
|
|
57
|
+
constructor(rootDir) {
|
|
58
|
+
this.rootDir = resolve(rootDir);
|
|
59
|
+
this.ensurePrivateDirectory();
|
|
60
|
+
}
|
|
61
|
+
ensurePrivateDirectory() {
|
|
62
|
+
mkdirSync(this.rootDir, { recursive: true, mode: 0o700 });
|
|
63
|
+
const stat = lstatSync(this.rootDir);
|
|
64
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
65
|
+
throw new Error(`Approval receipt root must be a real directory: ${this.rootDir}`);
|
|
66
|
+
}
|
|
67
|
+
assertOwnedByCurrentUser(stat.uid, `Approval receipt root ${this.rootDir}`);
|
|
68
|
+
if ((stat.mode & 0o777) !== 0o700) {
|
|
69
|
+
throw new Error(`Approval receipt root permissions must be 0700: ${this.rootDir}`);
|
|
70
|
+
}
|
|
71
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
72
|
+
}
|
|
73
|
+
assertRootUnchanged(expected) {
|
|
74
|
+
const current = this.ensurePrivateDirectory();
|
|
75
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
76
|
+
throw new Error("Approval receipt root changed during the operation");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
pathFor(receiptId) {
|
|
80
|
+
return join(this.rootDir, receiptFileName(receiptId));
|
|
81
|
+
}
|
|
82
|
+
openReceiptForRead(filePath) {
|
|
83
|
+
const fd = openSync(filePath, constants.O_RDONLY | NOFOLLOW);
|
|
84
|
+
try {
|
|
85
|
+
assertPrivateReceiptFd(fd, filePath);
|
|
86
|
+
return fd;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
closeSync(fd);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
write(receipt) {
|
|
94
|
+
const validated = validateApprovalReceipt(receipt);
|
|
95
|
+
const rootIdentity = this.ensurePrivateDirectory();
|
|
96
|
+
const filePath = this.pathFor(validated.core.receiptId);
|
|
97
|
+
const canonical = serializeApprovalReceipt(validated);
|
|
98
|
+
const bytes = Buffer.from(canonical, "utf8");
|
|
99
|
+
if (bytes.byteLength > MAX_RECEIPT_BYTES)
|
|
100
|
+
throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);
|
|
101
|
+
let fd;
|
|
102
|
+
try {
|
|
103
|
+
fd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o600);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (!isErrno(error, "EEXIST"))
|
|
107
|
+
throw error;
|
|
108
|
+
const existing = this.read(validated.core.receiptId);
|
|
109
|
+
if (serializeApprovalReceipt(existing) !== canonical) {
|
|
110
|
+
throw new Error(`Approval receipt already exists with different content and is never overwritten: ${validated.core.receiptId}`);
|
|
111
|
+
}
|
|
112
|
+
this.assertRootUnchanged(rootIdentity);
|
|
113
|
+
return { path: filePath, created: false };
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
fchmodSync(fd, 0o600);
|
|
117
|
+
assertPrivateReceiptFd(fd, filePath);
|
|
118
|
+
writeFileSync(fd, bytes);
|
|
119
|
+
fsyncSync(fd);
|
|
120
|
+
assertPrivateReceiptFd(fd, filePath);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
closeSync(fd);
|
|
124
|
+
}
|
|
125
|
+
this.assertRootUnchanged(rootIdentity);
|
|
126
|
+
return { path: filePath, created: true };
|
|
127
|
+
}
|
|
128
|
+
has(receiptId) {
|
|
129
|
+
const rootIdentity = this.ensurePrivateDirectory();
|
|
130
|
+
let fd;
|
|
131
|
+
try {
|
|
132
|
+
fd = this.openReceiptForRead(this.pathFor(receiptId));
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (isErrno(error, "ENOENT")) {
|
|
136
|
+
this.assertRootUnchanged(rootIdentity);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
closeSync(fd);
|
|
142
|
+
this.assertRootUnchanged(rootIdentity);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
read(receiptId) {
|
|
146
|
+
const rootIdentity = this.ensurePrivateDirectory();
|
|
147
|
+
const filePath = this.pathFor(receiptId);
|
|
148
|
+
const fd = this.openReceiptForRead(filePath);
|
|
149
|
+
try {
|
|
150
|
+
return readReceiptFromFd(fd, filePath);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
closeSync(fd);
|
|
154
|
+
this.assertRootUnchanged(rootIdentity);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
listReceiptIds() {
|
|
158
|
+
const rootIdentity = this.ensurePrivateDirectory();
|
|
159
|
+
const ids = readdirSync(this.rootDir)
|
|
160
|
+
.map((name) => /^(approval-[a-f0-9]{32})\.json$/u.exec(name)?.[1])
|
|
161
|
+
.filter((id) => id !== undefined)
|
|
162
|
+
.sort((left, right) => left.localeCompare(right));
|
|
163
|
+
for (const id of ids)
|
|
164
|
+
this.has(id);
|
|
165
|
+
this.assertRootUnchanged(rootIdentity);
|
|
166
|
+
return ids;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=approval-receipt-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-receipt-store.js","sourceRoot":"","sources":["../../src/approvals/approval-receipt-store.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tcloseSync,\n\tconstants,\n\tfchmodSync,\n\tfstatSync,\n\tfsyncSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\treaddirSync,\n\treadSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { type ApprovalReceipt, serializeApprovalReceipt, validateApprovalReceipt } from \"./approval-receipt.ts\";\n\nconst MAX_RECEIPT_BYTES = 1024 * 1024;\nconst NOFOLLOW = constants.O_NOFOLLOW ?? 0;\n\nexport interface ApprovalReceiptWriteResult {\n\tpath: string;\n\tcreated: boolean;\n}\n\ninterface RootIdentity {\n\tdev: number;\n\tino: number;\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n\treturn error instanceof Error && \"code\" in error && error.code === code;\n}\n\nfunction assertOwnedByCurrentUser(uid: number, label: string): void {\n\tconst getuid = process.getuid;\n\tif (typeof getuid === \"function\" && uid !== getuid()) {\n\t\tthrow new Error(`${label} is not owned by the current user`);\n\t}\n}\n\nfunction receiptFileName(receiptId: string): string {\n\tif (!/^approval-[a-f0-9]{32}$/u.test(receiptId)) throw new Error(\"Approval receipt id is invalid\");\n\treturn `${receiptId}.json`;\n}\n\nfunction assertPrivateReceiptFd(fd: number, filePath: string): void {\n\tconst stat = fstatSync(fd);\n\tif (!stat.isFile()) throw new Error(`Approval receipt is not a regular file: ${filePath}`);\n\tif (stat.nlink !== 1) throw new Error(`Approval receipt must not be hard-linked: ${filePath}`);\n\tassertOwnedByCurrentUser(stat.uid, `Approval receipt ${filePath}`);\n\tif ((stat.mode & 0o777) !== 0o600) {\n\t\tthrow new Error(`Approval receipt permissions must be 0600: ${filePath}`);\n\t}\n\tif (stat.size > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n}\n\nfunction readReceiptFromFd(fd: number, filePath: string): ApprovalReceipt {\n\tassertPrivateReceiptFd(fd, filePath);\n\tconst buffer = Buffer.alloc(MAX_RECEIPT_BYTES + 1);\n\tlet offset = 0;\n\twhile (offset < buffer.length) {\n\t\tconst read = readSync(fd, buffer, offset, buffer.length - offset, offset);\n\t\tif (read === 0) break;\n\t\toffset += read;\n\t}\n\tif (offset > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n\tassertPrivateReceiptFd(fd, filePath);\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(buffer.subarray(0, offset).toString(\"utf8\")) as unknown;\n\t} catch {\n\t\tthrow new Error(`Approval receipt is not valid JSON: ${filePath}`);\n\t}\n\treturn validateApprovalReceipt(value);\n}\n\nexport class ApprovalReceiptStore {\n\treadonly rootDir: string;\n\n\tconstructor(rootDir: string) {\n\t\tthis.rootDir = resolve(rootDir);\n\t\tthis.ensurePrivateDirectory();\n\t}\n\n\tprivate ensurePrivateDirectory(): RootIdentity {\n\t\tmkdirSync(this.rootDir, { recursive: true, mode: 0o700 });\n\t\tconst stat = lstatSync(this.rootDir);\n\t\tif (stat.isSymbolicLink() || !stat.isDirectory()) {\n\t\t\tthrow new Error(`Approval receipt root must be a real directory: ${this.rootDir}`);\n\t\t}\n\t\tassertOwnedByCurrentUser(stat.uid, `Approval receipt root ${this.rootDir}`);\n\t\tif ((stat.mode & 0o777) !== 0o700) {\n\t\t\tthrow new Error(`Approval receipt root permissions must be 0700: ${this.rootDir}`);\n\t\t}\n\t\treturn { dev: stat.dev, ino: stat.ino };\n\t}\n\n\tprivate assertRootUnchanged(expected: RootIdentity): void {\n\t\tconst current = this.ensurePrivateDirectory();\n\t\tif (current.dev !== expected.dev || current.ino !== expected.ino) {\n\t\t\tthrow new Error(\"Approval receipt root changed during the operation\");\n\t\t}\n\t}\n\n\tprivate pathFor(receiptId: string): string {\n\t\treturn join(this.rootDir, receiptFileName(receiptId));\n\t}\n\n\tprivate openReceiptForRead(filePath: string): number {\n\t\tconst fd = openSync(filePath, constants.O_RDONLY | NOFOLLOW);\n\t\ttry {\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t\treturn fd;\n\t\t} catch (error) {\n\t\t\tcloseSync(fd);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\twrite(receipt: ApprovalReceipt): ApprovalReceiptWriteResult {\n\t\tconst validated = validateApprovalReceipt(receipt);\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst filePath = this.pathFor(validated.core.receiptId);\n\t\tconst canonical = serializeApprovalReceipt(validated);\n\t\tconst bytes = Buffer.from(canonical, \"utf8\");\n\t\tif (bytes.byteLength > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o600);\n\t\t} catch (error) {\n\t\t\tif (!isErrno(error, \"EEXIST\")) throw error;\n\t\t\tconst existing = this.read(validated.core.receiptId);\n\t\t\tif (serializeApprovalReceipt(existing) !== canonical) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Approval receipt already exists with different content and is never overwritten: ${validated.core.receiptId}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t\treturn { path: filePath, created: false };\n\t\t}\n\n\t\ttry {\n\t\t\tfchmodSync(fd, 0o600);\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t\twriteFileSync(fd, bytes);\n\t\t\tfsyncSync(fd);\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn { path: filePath, created: true };\n\t}\n\n\thas(receiptId: string): boolean {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = this.openReceiptForRead(this.pathFor(receiptId));\n\t\t} catch (error) {\n\t\t\tif (isErrno(error, \"ENOENT\")) {\n\t\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tcloseSync(fd);\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn true;\n\t}\n\n\tread(receiptId: string): ApprovalReceipt {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst filePath = this.pathFor(receiptId);\n\t\tconst fd = this.openReceiptForRead(filePath);\n\t\ttry {\n\t\t\treturn readReceiptFromFd(fd, filePath);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t}\n\t}\n\n\tlistReceiptIds(): string[] {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst ids = readdirSync(this.rootDir)\n\t\t\t.map((name) => /^(approval-[a-f0-9]{32})\\.json$/u.exec(name)?.[1])\n\t\t\t.filter((id): id is string => id !== undefined)\n\t\t\t.sort((left, right) => left.localeCompare(right));\n\t\tfor (const id of ids) this.has(id);\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn ids;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type ApprovalDecision = "approved" | "rejected" | "invalidated";
|
|
2
|
+
export interface ApprovalContentDigest {
|
|
3
|
+
sha256: string;
|
|
4
|
+
bytes: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ApprovalPlanDigest extends ApprovalContentDigest {
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ApprovalReceiptCore {
|
|
10
|
+
schemaVersion: 1;
|
|
11
|
+
receiptId: string;
|
|
12
|
+
provider: "plannotator";
|
|
13
|
+
trust: "external-extension-event";
|
|
14
|
+
requestId: string;
|
|
15
|
+
reviewId: string;
|
|
16
|
+
decision: ApprovalDecision;
|
|
17
|
+
decidedAt: string;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
plan: ApprovalPlanDigest;
|
|
20
|
+
feedback?: ApprovalContentDigest;
|
|
21
|
+
}
|
|
22
|
+
export interface ApprovalReceipt {
|
|
23
|
+
core: ApprovalReceiptCore;
|
|
24
|
+
coreSha256: string;
|
|
25
|
+
}
|
|
26
|
+
export interface CreateApprovalReceiptInput {
|
|
27
|
+
provider: "plannotator";
|
|
28
|
+
requestId: string;
|
|
29
|
+
reviewId: string;
|
|
30
|
+
decision: ApprovalDecision;
|
|
31
|
+
decidedAt: string;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
planPath: string;
|
|
34
|
+
planContent: string;
|
|
35
|
+
feedback?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function computeApprovalContentDigest(value: string): ApprovalContentDigest;
|
|
38
|
+
export declare function computeApprovalReceiptCoreSha256(core: ApprovalReceiptCore): string;
|
|
39
|
+
export declare function validateApprovalReceipt(value: unknown): ApprovalReceipt;
|
|
40
|
+
export declare function createApprovalReceipt(input: CreateApprovalReceiptInput): ApprovalReceipt;
|
|
41
|
+
export declare function parseApprovalReceipt(serialized: string): ApprovalReceipt;
|
|
42
|
+
export declare function serializeApprovalReceipt(receipt: ApprovalReceipt): string;
|
|
43
|
+
//# sourceMappingURL=approval-receipt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-receipt.d.ts","sourceRoot":"","sources":["../../src/approvals/approval-receipt.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,UAAU,GAAG,aAAa,CAAC;AAEvE,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAmB,SAAQ,qBAAqB;IAChE,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,mBAAmB;IACnC,aAAa,EAAE,CAAC,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,EAAE,0BAA0B,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,EAAE,aAAa,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAMD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,CAEjF;AAyFD,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAElF;AAOD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CA8CvE;AAiBD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,eAAe,CAwBxF;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,CAQxE;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAEzE","sourcesContent":["import { createHash, timingSafeEqual } from \"node:crypto\";\nimport { isAbsolute } from \"node:path\";\n\nconst SHA256_PATTERN = /^[a-f0-9]{64}$/u;\nconst SAFE_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/u;\nconst CORE_DOMAIN = \"omk.approval-receipt.core.v1\\0\";\nconst ID_DOMAIN = \"omk.approval-receipt.id.v1\\0\";\n\nexport type ApprovalDecision = \"approved\" | \"rejected\" | \"invalidated\";\n\nexport interface ApprovalContentDigest {\n\tsha256: string;\n\tbytes: number;\n}\n\nexport interface ApprovalPlanDigest extends ApprovalContentDigest {\n\tpath: string;\n}\n\nexport interface ApprovalReceiptCore {\n\tschemaVersion: 1;\n\treceiptId: string;\n\tprovider: \"plannotator\";\n\ttrust: \"external-extension-event\";\n\trequestId: string;\n\treviewId: string;\n\tdecision: ApprovalDecision;\n\tdecidedAt: string;\n\tsessionId: string;\n\tplan: ApprovalPlanDigest;\n\tfeedback?: ApprovalContentDigest;\n}\n\nexport interface ApprovalReceipt {\n\tcore: ApprovalReceiptCore;\n\tcoreSha256: string;\n}\n\nexport interface CreateApprovalReceiptInput {\n\tprovider: \"plannotator\";\n\trequestId: string;\n\treviewId: string;\n\tdecision: ApprovalDecision;\n\tdecidedAt: string;\n\tsessionId: string;\n\tplanPath: string;\n\tplanContent: string;\n\tfeedback?: string;\n}\n\nfunction sha256(domain: string, value: string): string {\n\treturn createHash(\"sha256\").update(domain).update(value, \"utf8\").digest(\"hex\");\n}\n\nexport function computeApprovalContentDigest(value: string): ApprovalContentDigest {\n\treturn { sha256: sha256(\"omk.approval-content.v1\\0\", value), bytes: Buffer.byteLength(value, \"utf8\") };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasOwn(value: Record<string, unknown>, key: string): boolean {\n\treturn Object.getOwnPropertyDescriptor(value, key) !== undefined;\n}\n\nfunction exactKeys(value: Record<string, unknown>, required: string[], optional: string[] = []): void {\n\tconst allowed = new Set([...required, ...optional]);\n\tfor (const key of Object.keys(value)) {\n\t\tif (!allowed.has(key)) throw new Error(`Unexpected approval receipt field: ${key}`);\n\t}\n\tfor (const key of required) {\n\t\tif (!hasOwn(value, key)) throw new Error(`Missing approval receipt field: ${key}`);\n\t}\n}\n\nfunction safeId(value: unknown, label: string): string {\n\tif (typeof value !== \"string\" || !SAFE_ID_PATTERN.test(value)) throw new Error(`${label} is not a safe identifier`);\n\treturn value;\n}\n\nfunction sha256Hex(value: unknown, label: string): string {\n\tif (typeof value !== \"string\" || !SHA256_PATTERN.test(value)) throw new Error(`${label} must be lowercase SHA-256`);\n\treturn value;\n}\n\nfunction nonNegativeInteger(value: unknown, label: string): number {\n\tif (!Number.isSafeInteger(value) || (value as number) < 0)\n\t\tthrow new Error(`${label} must be a non-negative safe integer`);\n\treturn value as number;\n}\n\nfunction workspaceRelativePath(value: unknown): string {\n\tif (typeof value !== \"string\" || !value || value.includes(\"\\\\\") || value.includes(\"\\0\") || isAbsolute(value)) {\n\t\tthrow new Error(\"plan.path must be workspace-relative\");\n\t}\n\tconst segments = value.split(\"/\");\n\tif (segments.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\")) {\n\t\tthrow new Error(\"plan.path must be workspace-relative\");\n\t}\n\treturn value;\n}\n\nfunction timestamp(value: unknown): string {\n\tif (typeof value !== \"string\") throw new Error(\"decidedAt must be an ISO timestamp\");\n\tconst parsed = new Date(value);\n\tif (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) {\n\t\tthrow new Error(\"decidedAt must be a canonical ISO timestamp\");\n\t}\n\treturn value;\n}\n\nfunction parseContentDigest(value: unknown, label: string): ApprovalContentDigest {\n\tif (!isRecord(value)) throw new Error(`${label} must be an object`);\n\texactKeys(value, [\"sha256\", \"bytes\"]);\n\treturn {\n\t\tsha256: sha256Hex(value.sha256, `${label}.sha256`),\n\t\tbytes: nonNegativeInteger(value.bytes, `${label}.bytes`),\n\t};\n}\n\nfunction parsePlan(value: unknown): ApprovalPlanDigest {\n\tif (!isRecord(value)) throw new Error(\"plan must be an object\");\n\texactKeys(value, [\"path\", \"sha256\", \"bytes\"]);\n\treturn {\n\t\tpath: workspaceRelativePath(value.path),\n\t\tsha256: sha256Hex(value.sha256, \"plan.sha256\"),\n\t\tbytes: nonNegativeInteger(value.bytes, \"plan.bytes\"),\n\t};\n}\n\nfunction canonicalJson(value: unknown): string {\n\tif (value === null || typeof value === \"boolean\" || typeof value === \"string\") return JSON.stringify(value);\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) throw new Error(\"Approval receipt contains a non-finite number\");\n\t\treturn JSON.stringify(value);\n\t}\n\tif (Array.isArray(value)) return `[${value.map(canonicalJson).join(\",\")}]`;\n\tif (!isRecord(value)) throw new Error(\"Approval receipt contains an unsupported value\");\n\treturn `{${Object.keys(value)\n\t\t.sort((left, right) => left.localeCompare(right))\n\t\t.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)\n\t\t.join(\",\")}}`;\n}\n\nexport function computeApprovalReceiptCoreSha256(core: ApprovalReceiptCore): string {\n\treturn sha256(CORE_DOMAIN, canonicalJson(core));\n}\n\nfunction constantTimeEqual(left: string, right: string): boolean {\n\tif (!SHA256_PATTERN.test(left) || !SHA256_PATTERN.test(right)) return false;\n\treturn timingSafeEqual(Buffer.from(left, \"hex\"), Buffer.from(right, \"hex\"));\n}\n\nexport function validateApprovalReceipt(value: unknown): ApprovalReceipt {\n\tif (!isRecord(value)) throw new Error(\"Approval receipt must be an object\");\n\texactKeys(value, [\"core\", \"coreSha256\"]);\n\tif (!isRecord(value.core)) throw new Error(\"Approval receipt core must be an object\");\n\tconst coreValue = value.core;\n\texactKeys(\n\t\tcoreValue,\n\t\t[\n\t\t\t\"schemaVersion\",\n\t\t\t\"receiptId\",\n\t\t\t\"provider\",\n\t\t\t\"trust\",\n\t\t\t\"requestId\",\n\t\t\t\"reviewId\",\n\t\t\t\"decision\",\n\t\t\t\"decidedAt\",\n\t\t\t\"sessionId\",\n\t\t\t\"plan\",\n\t\t],\n\t\t[\"feedback\"],\n\t);\n\tif (coreValue.schemaVersion !== 1) throw new Error(\"Unsupported approval receipt schemaVersion\");\n\tif (coreValue.provider !== \"plannotator\") throw new Error(\"Unsupported approval provider\");\n\tif (coreValue.trust !== \"external-extension-event\") throw new Error(\"Invalid approval trust classification\");\n\tif (coreValue.decision !== \"approved\" && coreValue.decision !== \"rejected\" && coreValue.decision !== \"invalidated\") {\n\t\tthrow new Error(\"Invalid approval decision\");\n\t}\n\tconst core: ApprovalReceiptCore = {\n\t\tschemaVersion: 1,\n\t\treceiptId: safeId(coreValue.receiptId, \"receiptId\"),\n\t\tprovider: \"plannotator\",\n\t\ttrust: \"external-extension-event\",\n\t\trequestId: safeId(coreValue.requestId, \"requestId\"),\n\t\treviewId: safeId(coreValue.reviewId, \"reviewId\"),\n\t\tdecision: coreValue.decision,\n\t\tdecidedAt: timestamp(coreValue.decidedAt),\n\t\tsessionId: safeId(coreValue.sessionId, \"sessionId\"),\n\t\tplan: parsePlan(coreValue.plan),\n\t\t...(hasOwn(coreValue, \"feedback\") ? { feedback: parseContentDigest(coreValue.feedback, \"feedback\") } : {}),\n\t};\n\tconst expectedReceiptId = computeApprovalReceiptId(core);\n\tif (core.receiptId !== expectedReceiptId) throw new Error(\"Approval receipt id mismatch\");\n\tconst coreSha256 = sha256Hex(value.coreSha256, \"coreSha256\");\n\tconst expectedDigest = computeApprovalReceiptCoreSha256(core);\n\tif (!constantTimeEqual(coreSha256, expectedDigest)) throw new Error(\"Approval receipt digest mismatch\");\n\treturn { core, coreSha256 };\n}\n\nfunction computeApprovalReceiptId(\n\tcore: Omit<ApprovalReceiptCore, \"receiptId\" | \"decidedAt\" | \"decision\" | \"feedback\">,\n): string {\n\tconst identity = {\n\t\tschemaVersion: core.schemaVersion,\n\t\tprovider: core.provider,\n\t\ttrust: core.trust,\n\t\trequestId: core.requestId,\n\t\treviewId: core.reviewId,\n\t\tsessionId: core.sessionId,\n\t\tplan: core.plan,\n\t};\n\treturn `approval-${sha256(ID_DOMAIN, canonicalJson(identity)).slice(0, 32)}`;\n}\n\nexport function createApprovalReceipt(input: CreateApprovalReceiptInput): ApprovalReceipt {\n\tconst plan: ApprovalPlanDigest = {\n\t\tpath: workspaceRelativePath(input.planPath),\n\t\t...computeApprovalContentDigest(input.planContent),\n\t};\n\tconst coreWithoutId = {\n\t\tschemaVersion: 1 as const,\n\t\tprovider: input.provider,\n\t\ttrust: \"external-extension-event\" as const,\n\t\trequestId: safeId(input.requestId, \"requestId\"),\n\t\treviewId: safeId(input.reviewId, \"reviewId\"),\n\t\tdecision: input.decision,\n\t\tdecidedAt: timestamp(input.decidedAt),\n\t\tsessionId: safeId(input.sessionId, \"sessionId\"),\n\t\tplan,\n\t\t...(input.feedback !== undefined && input.feedback.length > 0\n\t\t\t? { feedback: computeApprovalContentDigest(input.feedback) }\n\t\t\t: {}),\n\t};\n\tconst core: ApprovalReceiptCore = {\n\t\t...coreWithoutId,\n\t\treceiptId: computeApprovalReceiptId(coreWithoutId),\n\t};\n\treturn validateApprovalReceipt({ core, coreSha256: computeApprovalReceiptCoreSha256(core) });\n}\n\nexport function parseApprovalReceipt(serialized: string): ApprovalReceipt {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(serialized) as unknown;\n\t} catch {\n\t\tthrow new Error(\"Approval receipt is not valid JSON\");\n\t}\n\treturn validateApprovalReceipt(parsed);\n}\n\nexport function serializeApprovalReceipt(receipt: ApprovalReceipt): string {\n\treturn `${JSON.stringify(validateApprovalReceipt(receipt), null, 2)}\\n`;\n}\n"]}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
|
|
4
|
+
const SAFE_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/u;
|
|
5
|
+
const CORE_DOMAIN = "omk.approval-receipt.core.v1\0";
|
|
6
|
+
const ID_DOMAIN = "omk.approval-receipt.id.v1\0";
|
|
7
|
+
function sha256(domain, value) {
|
|
8
|
+
return createHash("sha256").update(domain).update(value, "utf8").digest("hex");
|
|
9
|
+
}
|
|
10
|
+
export function computeApprovalContentDigest(value) {
|
|
11
|
+
return { sha256: sha256("omk.approval-content.v1\0", value), bytes: Buffer.byteLength(value, "utf8") };
|
|
12
|
+
}
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function hasOwn(value, key) {
|
|
17
|
+
return Object.getOwnPropertyDescriptor(value, key) !== undefined;
|
|
18
|
+
}
|
|
19
|
+
function exactKeys(value, required, optional = []) {
|
|
20
|
+
const allowed = new Set([...required, ...optional]);
|
|
21
|
+
for (const key of Object.keys(value)) {
|
|
22
|
+
if (!allowed.has(key))
|
|
23
|
+
throw new Error(`Unexpected approval receipt field: ${key}`);
|
|
24
|
+
}
|
|
25
|
+
for (const key of required) {
|
|
26
|
+
if (!hasOwn(value, key))
|
|
27
|
+
throw new Error(`Missing approval receipt field: ${key}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function safeId(value, label) {
|
|
31
|
+
if (typeof value !== "string" || !SAFE_ID_PATTERN.test(value))
|
|
32
|
+
throw new Error(`${label} is not a safe identifier`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function sha256Hex(value, label) {
|
|
36
|
+
if (typeof value !== "string" || !SHA256_PATTERN.test(value))
|
|
37
|
+
throw new Error(`${label} must be lowercase SHA-256`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function nonNegativeInteger(value, label) {
|
|
41
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
42
|
+
throw new Error(`${label} must be a non-negative safe integer`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function workspaceRelativePath(value) {
|
|
46
|
+
if (typeof value !== "string" || !value || value.includes("\\") || value.includes("\0") || isAbsolute(value)) {
|
|
47
|
+
throw new Error("plan.path must be workspace-relative");
|
|
48
|
+
}
|
|
49
|
+
const segments = value.split("/");
|
|
50
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
51
|
+
throw new Error("plan.path must be workspace-relative");
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function timestamp(value) {
|
|
56
|
+
if (typeof value !== "string")
|
|
57
|
+
throw new Error("decidedAt must be an ISO timestamp");
|
|
58
|
+
const parsed = new Date(value);
|
|
59
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) {
|
|
60
|
+
throw new Error("decidedAt must be a canonical ISO timestamp");
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function parseContentDigest(value, label) {
|
|
65
|
+
if (!isRecord(value))
|
|
66
|
+
throw new Error(`${label} must be an object`);
|
|
67
|
+
exactKeys(value, ["sha256", "bytes"]);
|
|
68
|
+
return {
|
|
69
|
+
sha256: sha256Hex(value.sha256, `${label}.sha256`),
|
|
70
|
+
bytes: nonNegativeInteger(value.bytes, `${label}.bytes`),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function parsePlan(value) {
|
|
74
|
+
if (!isRecord(value))
|
|
75
|
+
throw new Error("plan must be an object");
|
|
76
|
+
exactKeys(value, ["path", "sha256", "bytes"]);
|
|
77
|
+
return {
|
|
78
|
+
path: workspaceRelativePath(value.path),
|
|
79
|
+
sha256: sha256Hex(value.sha256, "plan.sha256"),
|
|
80
|
+
bytes: nonNegativeInteger(value.bytes, "plan.bytes"),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function canonicalJson(value) {
|
|
84
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
85
|
+
return JSON.stringify(value);
|
|
86
|
+
if (typeof value === "number") {
|
|
87
|
+
if (!Number.isFinite(value))
|
|
88
|
+
throw new Error("Approval receipt contains a non-finite number");
|
|
89
|
+
return JSON.stringify(value);
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(value))
|
|
92
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
93
|
+
if (!isRecord(value))
|
|
94
|
+
throw new Error("Approval receipt contains an unsupported value");
|
|
95
|
+
return `{${Object.keys(value)
|
|
96
|
+
.sort((left, right) => left.localeCompare(right))
|
|
97
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
98
|
+
.join(",")}}`;
|
|
99
|
+
}
|
|
100
|
+
export function computeApprovalReceiptCoreSha256(core) {
|
|
101
|
+
return sha256(CORE_DOMAIN, canonicalJson(core));
|
|
102
|
+
}
|
|
103
|
+
function constantTimeEqual(left, right) {
|
|
104
|
+
if (!SHA256_PATTERN.test(left) || !SHA256_PATTERN.test(right))
|
|
105
|
+
return false;
|
|
106
|
+
return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
|
|
107
|
+
}
|
|
108
|
+
export function validateApprovalReceipt(value) {
|
|
109
|
+
if (!isRecord(value))
|
|
110
|
+
throw new Error("Approval receipt must be an object");
|
|
111
|
+
exactKeys(value, ["core", "coreSha256"]);
|
|
112
|
+
if (!isRecord(value.core))
|
|
113
|
+
throw new Error("Approval receipt core must be an object");
|
|
114
|
+
const coreValue = value.core;
|
|
115
|
+
exactKeys(coreValue, [
|
|
116
|
+
"schemaVersion",
|
|
117
|
+
"receiptId",
|
|
118
|
+
"provider",
|
|
119
|
+
"trust",
|
|
120
|
+
"requestId",
|
|
121
|
+
"reviewId",
|
|
122
|
+
"decision",
|
|
123
|
+
"decidedAt",
|
|
124
|
+
"sessionId",
|
|
125
|
+
"plan",
|
|
126
|
+
], ["feedback"]);
|
|
127
|
+
if (coreValue.schemaVersion !== 1)
|
|
128
|
+
throw new Error("Unsupported approval receipt schemaVersion");
|
|
129
|
+
if (coreValue.provider !== "plannotator")
|
|
130
|
+
throw new Error("Unsupported approval provider");
|
|
131
|
+
if (coreValue.trust !== "external-extension-event")
|
|
132
|
+
throw new Error("Invalid approval trust classification");
|
|
133
|
+
if (coreValue.decision !== "approved" && coreValue.decision !== "rejected" && coreValue.decision !== "invalidated") {
|
|
134
|
+
throw new Error("Invalid approval decision");
|
|
135
|
+
}
|
|
136
|
+
const core = {
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
receiptId: safeId(coreValue.receiptId, "receiptId"),
|
|
139
|
+
provider: "plannotator",
|
|
140
|
+
trust: "external-extension-event",
|
|
141
|
+
requestId: safeId(coreValue.requestId, "requestId"),
|
|
142
|
+
reviewId: safeId(coreValue.reviewId, "reviewId"),
|
|
143
|
+
decision: coreValue.decision,
|
|
144
|
+
decidedAt: timestamp(coreValue.decidedAt),
|
|
145
|
+
sessionId: safeId(coreValue.sessionId, "sessionId"),
|
|
146
|
+
plan: parsePlan(coreValue.plan),
|
|
147
|
+
...(hasOwn(coreValue, "feedback") ? { feedback: parseContentDigest(coreValue.feedback, "feedback") } : {}),
|
|
148
|
+
};
|
|
149
|
+
const expectedReceiptId = computeApprovalReceiptId(core);
|
|
150
|
+
if (core.receiptId !== expectedReceiptId)
|
|
151
|
+
throw new Error("Approval receipt id mismatch");
|
|
152
|
+
const coreSha256 = sha256Hex(value.coreSha256, "coreSha256");
|
|
153
|
+
const expectedDigest = computeApprovalReceiptCoreSha256(core);
|
|
154
|
+
if (!constantTimeEqual(coreSha256, expectedDigest))
|
|
155
|
+
throw new Error("Approval receipt digest mismatch");
|
|
156
|
+
return { core, coreSha256 };
|
|
157
|
+
}
|
|
158
|
+
function computeApprovalReceiptId(core) {
|
|
159
|
+
const identity = {
|
|
160
|
+
schemaVersion: core.schemaVersion,
|
|
161
|
+
provider: core.provider,
|
|
162
|
+
trust: core.trust,
|
|
163
|
+
requestId: core.requestId,
|
|
164
|
+
reviewId: core.reviewId,
|
|
165
|
+
sessionId: core.sessionId,
|
|
166
|
+
plan: core.plan,
|
|
167
|
+
};
|
|
168
|
+
return `approval-${sha256(ID_DOMAIN, canonicalJson(identity)).slice(0, 32)}`;
|
|
169
|
+
}
|
|
170
|
+
export function createApprovalReceipt(input) {
|
|
171
|
+
const plan = {
|
|
172
|
+
path: workspaceRelativePath(input.planPath),
|
|
173
|
+
...computeApprovalContentDigest(input.planContent),
|
|
174
|
+
};
|
|
175
|
+
const coreWithoutId = {
|
|
176
|
+
schemaVersion: 1,
|
|
177
|
+
provider: input.provider,
|
|
178
|
+
trust: "external-extension-event",
|
|
179
|
+
requestId: safeId(input.requestId, "requestId"),
|
|
180
|
+
reviewId: safeId(input.reviewId, "reviewId"),
|
|
181
|
+
decision: input.decision,
|
|
182
|
+
decidedAt: timestamp(input.decidedAt),
|
|
183
|
+
sessionId: safeId(input.sessionId, "sessionId"),
|
|
184
|
+
plan,
|
|
185
|
+
...(input.feedback !== undefined && input.feedback.length > 0
|
|
186
|
+
? { feedback: computeApprovalContentDigest(input.feedback) }
|
|
187
|
+
: {}),
|
|
188
|
+
};
|
|
189
|
+
const core = {
|
|
190
|
+
...coreWithoutId,
|
|
191
|
+
receiptId: computeApprovalReceiptId(coreWithoutId),
|
|
192
|
+
};
|
|
193
|
+
return validateApprovalReceipt({ core, coreSha256: computeApprovalReceiptCoreSha256(core) });
|
|
194
|
+
}
|
|
195
|
+
export function parseApprovalReceipt(serialized) {
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(serialized);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
throw new Error("Approval receipt is not valid JSON");
|
|
202
|
+
}
|
|
203
|
+
return validateApprovalReceipt(parsed);
|
|
204
|
+
}
|
|
205
|
+
export function serializeApprovalReceipt(receipt) {
|
|
206
|
+
return `${JSON.stringify(validateApprovalReceipt(receipt), null, 2)}\n`;
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=approval-receipt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-receipt.js","sourceRoot":"","sources":["../../src/approvals/approval-receipt.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, timingSafeEqual } from \"node:crypto\";\nimport { isAbsolute } from \"node:path\";\n\nconst SHA256_PATTERN = /^[a-f0-9]{64}$/u;\nconst SAFE_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/u;\nconst CORE_DOMAIN = \"omk.approval-receipt.core.v1\\0\";\nconst ID_DOMAIN = \"omk.approval-receipt.id.v1\\0\";\n\nexport type ApprovalDecision = \"approved\" | \"rejected\" | \"invalidated\";\n\nexport interface ApprovalContentDigest {\n\tsha256: string;\n\tbytes: number;\n}\n\nexport interface ApprovalPlanDigest extends ApprovalContentDigest {\n\tpath: string;\n}\n\nexport interface ApprovalReceiptCore {\n\tschemaVersion: 1;\n\treceiptId: string;\n\tprovider: \"plannotator\";\n\ttrust: \"external-extension-event\";\n\trequestId: string;\n\treviewId: string;\n\tdecision: ApprovalDecision;\n\tdecidedAt: string;\n\tsessionId: string;\n\tplan: ApprovalPlanDigest;\n\tfeedback?: ApprovalContentDigest;\n}\n\nexport interface ApprovalReceipt {\n\tcore: ApprovalReceiptCore;\n\tcoreSha256: string;\n}\n\nexport interface CreateApprovalReceiptInput {\n\tprovider: \"plannotator\";\n\trequestId: string;\n\treviewId: string;\n\tdecision: ApprovalDecision;\n\tdecidedAt: string;\n\tsessionId: string;\n\tplanPath: string;\n\tplanContent: string;\n\tfeedback?: string;\n}\n\nfunction sha256(domain: string, value: string): string {\n\treturn createHash(\"sha256\").update(domain).update(value, \"utf8\").digest(\"hex\");\n}\n\nexport function computeApprovalContentDigest(value: string): ApprovalContentDigest {\n\treturn { sha256: sha256(\"omk.approval-content.v1\\0\", value), bytes: Buffer.byteLength(value, \"utf8\") };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasOwn(value: Record<string, unknown>, key: string): boolean {\n\treturn Object.getOwnPropertyDescriptor(value, key) !== undefined;\n}\n\nfunction exactKeys(value: Record<string, unknown>, required: string[], optional: string[] = []): void {\n\tconst allowed = new Set([...required, ...optional]);\n\tfor (const key of Object.keys(value)) {\n\t\tif (!allowed.has(key)) throw new Error(`Unexpected approval receipt field: ${key}`);\n\t}\n\tfor (const key of required) {\n\t\tif (!hasOwn(value, key)) throw new Error(`Missing approval receipt field: ${key}`);\n\t}\n}\n\nfunction safeId(value: unknown, label: string): string {\n\tif (typeof value !== \"string\" || !SAFE_ID_PATTERN.test(value)) throw new Error(`${label} is not a safe identifier`);\n\treturn value;\n}\n\nfunction sha256Hex(value: unknown, label: string): string {\n\tif (typeof value !== \"string\" || !SHA256_PATTERN.test(value)) throw new Error(`${label} must be lowercase SHA-256`);\n\treturn value;\n}\n\nfunction nonNegativeInteger(value: unknown, label: string): number {\n\tif (!Number.isSafeInteger(value) || (value as number) < 0)\n\t\tthrow new Error(`${label} must be a non-negative safe integer`);\n\treturn value as number;\n}\n\nfunction workspaceRelativePath(value: unknown): string {\n\tif (typeof value !== \"string\" || !value || value.includes(\"\\\\\") || value.includes(\"\\0\") || isAbsolute(value)) {\n\t\tthrow new Error(\"plan.path must be workspace-relative\");\n\t}\n\tconst segments = value.split(\"/\");\n\tif (segments.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\")) {\n\t\tthrow new Error(\"plan.path must be workspace-relative\");\n\t}\n\treturn value;\n}\n\nfunction timestamp(value: unknown): string {\n\tif (typeof value !== \"string\") throw new Error(\"decidedAt must be an ISO timestamp\");\n\tconst parsed = new Date(value);\n\tif (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) {\n\t\tthrow new Error(\"decidedAt must be a canonical ISO timestamp\");\n\t}\n\treturn value;\n}\n\nfunction parseContentDigest(value: unknown, label: string): ApprovalContentDigest {\n\tif (!isRecord(value)) throw new Error(`${label} must be an object`);\n\texactKeys(value, [\"sha256\", \"bytes\"]);\n\treturn {\n\t\tsha256: sha256Hex(value.sha256, `${label}.sha256`),\n\t\tbytes: nonNegativeInteger(value.bytes, `${label}.bytes`),\n\t};\n}\n\nfunction parsePlan(value: unknown): ApprovalPlanDigest {\n\tif (!isRecord(value)) throw new Error(\"plan must be an object\");\n\texactKeys(value, [\"path\", \"sha256\", \"bytes\"]);\n\treturn {\n\t\tpath: workspaceRelativePath(value.path),\n\t\tsha256: sha256Hex(value.sha256, \"plan.sha256\"),\n\t\tbytes: nonNegativeInteger(value.bytes, \"plan.bytes\"),\n\t};\n}\n\nfunction canonicalJson(value: unknown): string {\n\tif (value === null || typeof value === \"boolean\" || typeof value === \"string\") return JSON.stringify(value);\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) throw new Error(\"Approval receipt contains a non-finite number\");\n\t\treturn JSON.stringify(value);\n\t}\n\tif (Array.isArray(value)) return `[${value.map(canonicalJson).join(\",\")}]`;\n\tif (!isRecord(value)) throw new Error(\"Approval receipt contains an unsupported value\");\n\treturn `{${Object.keys(value)\n\t\t.sort((left, right) => left.localeCompare(right))\n\t\t.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)\n\t\t.join(\",\")}}`;\n}\n\nexport function computeApprovalReceiptCoreSha256(core: ApprovalReceiptCore): string {\n\treturn sha256(CORE_DOMAIN, canonicalJson(core));\n}\n\nfunction constantTimeEqual(left: string, right: string): boolean {\n\tif (!SHA256_PATTERN.test(left) || !SHA256_PATTERN.test(right)) return false;\n\treturn timingSafeEqual(Buffer.from(left, \"hex\"), Buffer.from(right, \"hex\"));\n}\n\nexport function validateApprovalReceipt(value: unknown): ApprovalReceipt {\n\tif (!isRecord(value)) throw new Error(\"Approval receipt must be an object\");\n\texactKeys(value, [\"core\", \"coreSha256\"]);\n\tif (!isRecord(value.core)) throw new Error(\"Approval receipt core must be an object\");\n\tconst coreValue = value.core;\n\texactKeys(\n\t\tcoreValue,\n\t\t[\n\t\t\t\"schemaVersion\",\n\t\t\t\"receiptId\",\n\t\t\t\"provider\",\n\t\t\t\"trust\",\n\t\t\t\"requestId\",\n\t\t\t\"reviewId\",\n\t\t\t\"decision\",\n\t\t\t\"decidedAt\",\n\t\t\t\"sessionId\",\n\t\t\t\"plan\",\n\t\t],\n\t\t[\"feedback\"],\n\t);\n\tif (coreValue.schemaVersion !== 1) throw new Error(\"Unsupported approval receipt schemaVersion\");\n\tif (coreValue.provider !== \"plannotator\") throw new Error(\"Unsupported approval provider\");\n\tif (coreValue.trust !== \"external-extension-event\") throw new Error(\"Invalid approval trust classification\");\n\tif (coreValue.decision !== \"approved\" && coreValue.decision !== \"rejected\" && coreValue.decision !== \"invalidated\") {\n\t\tthrow new Error(\"Invalid approval decision\");\n\t}\n\tconst core: ApprovalReceiptCore = {\n\t\tschemaVersion: 1,\n\t\treceiptId: safeId(coreValue.receiptId, \"receiptId\"),\n\t\tprovider: \"plannotator\",\n\t\ttrust: \"external-extension-event\",\n\t\trequestId: safeId(coreValue.requestId, \"requestId\"),\n\t\treviewId: safeId(coreValue.reviewId, \"reviewId\"),\n\t\tdecision: coreValue.decision,\n\t\tdecidedAt: timestamp(coreValue.decidedAt),\n\t\tsessionId: safeId(coreValue.sessionId, \"sessionId\"),\n\t\tplan: parsePlan(coreValue.plan),\n\t\t...(hasOwn(coreValue, \"feedback\") ? { feedback: parseContentDigest(coreValue.feedback, \"feedback\") } : {}),\n\t};\n\tconst expectedReceiptId = computeApprovalReceiptId(core);\n\tif (core.receiptId !== expectedReceiptId) throw new Error(\"Approval receipt id mismatch\");\n\tconst coreSha256 = sha256Hex(value.coreSha256, \"coreSha256\");\n\tconst expectedDigest = computeApprovalReceiptCoreSha256(core);\n\tif (!constantTimeEqual(coreSha256, expectedDigest)) throw new Error(\"Approval receipt digest mismatch\");\n\treturn { core, coreSha256 };\n}\n\nfunction computeApprovalReceiptId(\n\tcore: Omit<ApprovalReceiptCore, \"receiptId\" | \"decidedAt\" | \"decision\" | \"feedback\">,\n): string {\n\tconst identity = {\n\t\tschemaVersion: core.schemaVersion,\n\t\tprovider: core.provider,\n\t\ttrust: core.trust,\n\t\trequestId: core.requestId,\n\t\treviewId: core.reviewId,\n\t\tsessionId: core.sessionId,\n\t\tplan: core.plan,\n\t};\n\treturn `approval-${sha256(ID_DOMAIN, canonicalJson(identity)).slice(0, 32)}`;\n}\n\nexport function createApprovalReceipt(input: CreateApprovalReceiptInput): ApprovalReceipt {\n\tconst plan: ApprovalPlanDigest = {\n\t\tpath: workspaceRelativePath(input.planPath),\n\t\t...computeApprovalContentDigest(input.planContent),\n\t};\n\tconst coreWithoutId = {\n\t\tschemaVersion: 1 as const,\n\t\tprovider: input.provider,\n\t\ttrust: \"external-extension-event\" as const,\n\t\trequestId: safeId(input.requestId, \"requestId\"),\n\t\treviewId: safeId(input.reviewId, \"reviewId\"),\n\t\tdecision: input.decision,\n\t\tdecidedAt: timestamp(input.decidedAt),\n\t\tsessionId: safeId(input.sessionId, \"sessionId\"),\n\t\tplan,\n\t\t...(input.feedback !== undefined && input.feedback.length > 0\n\t\t\t? { feedback: computeApprovalContentDigest(input.feedback) }\n\t\t\t: {}),\n\t};\n\tconst core: ApprovalReceiptCore = {\n\t\t...coreWithoutId,\n\t\treceiptId: computeApprovalReceiptId(coreWithoutId),\n\t};\n\treturn validateApprovalReceipt({ core, coreSha256: computeApprovalReceiptCoreSha256(core) });\n}\n\nexport function parseApprovalReceipt(serialized: string): ApprovalReceipt {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(serialized) as unknown;\n\t} catch {\n\t\tthrow new Error(\"Approval receipt is not valid JSON\");\n\t}\n\treturn validateApprovalReceipt(parsed);\n}\n\nexport function serializeApprovalReceipt(receipt: ApprovalReceipt): string {\n\treturn `${JSON.stringify(validateApprovalReceipt(receipt), null, 2)}\\n`;\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ApprovalReceipt } from "./approval-receipt.ts";
|
|
2
|
+
export type ApprovalVerificationFailure = "invalid-receipt" | "not-approved" | "session-mismatch" | "plan-path-mismatch" | "plan-content-mismatch";
|
|
3
|
+
export type ApprovalVerificationResult = {
|
|
4
|
+
valid: true;
|
|
5
|
+
receipt: ApprovalReceipt;
|
|
6
|
+
} | {
|
|
7
|
+
valid: false;
|
|
8
|
+
reason: ApprovalVerificationFailure;
|
|
9
|
+
};
|
|
10
|
+
export interface ApprovalExecutionBinding {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
planPath: string;
|
|
13
|
+
planContent: string;
|
|
14
|
+
}
|
|
15
|
+
/** Verify immediately before execution; a persisted approval never authorizes changed plan bytes. */
|
|
16
|
+
export declare function verifyApprovalForExecution(value: unknown, binding: ApprovalExecutionBinding): ApprovalVerificationResult;
|
|
17
|
+
//# sourceMappingURL=approval-verifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-verifier.d.ts","sourceRoot":"","sources":["../../src/approvals/approval-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAyD,MAAM,uBAAuB,CAAC;AAEpH,MAAM,MAAM,2BAA2B,GACpC,iBAAiB,GACjB,cAAc,GACd,kBAAkB,GAClB,oBAAoB,GACpB,uBAAuB,CAAC;AAE3B,MAAM,MAAM,0BAA0B,GACnC;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GACzC;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAA;CAAE,CAAC;AAEzD,MAAM,WAAW,wBAAwB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,qGAAqG;AACrG,wBAAgB,0BAA0B,CACzC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,wBAAwB,GAC/B,0BAA0B,CAe5B","sourcesContent":["import { type ApprovalReceipt, computeApprovalContentDigest, validateApprovalReceipt } from \"./approval-receipt.ts\";\n\nexport type ApprovalVerificationFailure =\n\t| \"invalid-receipt\"\n\t| \"not-approved\"\n\t| \"session-mismatch\"\n\t| \"plan-path-mismatch\"\n\t| \"plan-content-mismatch\";\n\nexport type ApprovalVerificationResult =\n\t| { valid: true; receipt: ApprovalReceipt }\n\t| { valid: false; reason: ApprovalVerificationFailure };\n\nexport interface ApprovalExecutionBinding {\n\tsessionId: string;\n\tplanPath: string;\n\tplanContent: string;\n}\n\n/** Verify immediately before execution; a persisted approval never authorizes changed plan bytes. */\nexport function verifyApprovalForExecution(\n\tvalue: unknown,\n\tbinding: ApprovalExecutionBinding,\n): ApprovalVerificationResult {\n\tlet receipt: ApprovalReceipt;\n\ttry {\n\t\treceipt = validateApprovalReceipt(value);\n\t} catch {\n\t\treturn { valid: false, reason: \"invalid-receipt\" };\n\t}\n\tif (receipt.core.decision !== \"approved\") return { valid: false, reason: \"not-approved\" };\n\tif (receipt.core.sessionId !== binding.sessionId) return { valid: false, reason: \"session-mismatch\" };\n\tif (receipt.core.plan.path !== binding.planPath) return { valid: false, reason: \"plan-path-mismatch\" };\n\tconst digest = computeApprovalContentDigest(binding.planContent);\n\tif (receipt.core.plan.sha256 !== digest.sha256 || receipt.core.plan.bytes !== digest.bytes) {\n\t\treturn { valid: false, reason: \"plan-content-mismatch\" };\n\t}\n\treturn { valid: true, receipt };\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { computeApprovalContentDigest, validateApprovalReceipt } from "./approval-receipt.js";
|
|
2
|
+
/** Verify immediately before execution; a persisted approval never authorizes changed plan bytes. */
|
|
3
|
+
export function verifyApprovalForExecution(value, binding) {
|
|
4
|
+
let receipt;
|
|
5
|
+
try {
|
|
6
|
+
receipt = validateApprovalReceipt(value);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return { valid: false, reason: "invalid-receipt" };
|
|
10
|
+
}
|
|
11
|
+
if (receipt.core.decision !== "approved")
|
|
12
|
+
return { valid: false, reason: "not-approved" };
|
|
13
|
+
if (receipt.core.sessionId !== binding.sessionId)
|
|
14
|
+
return { valid: false, reason: "session-mismatch" };
|
|
15
|
+
if (receipt.core.plan.path !== binding.planPath)
|
|
16
|
+
return { valid: false, reason: "plan-path-mismatch" };
|
|
17
|
+
const digest = computeApprovalContentDigest(binding.planContent);
|
|
18
|
+
if (receipt.core.plan.sha256 !== digest.sha256 || receipt.core.plan.bytes !== digest.bytes) {
|
|
19
|
+
return { valid: false, reason: "plan-content-mismatch" };
|
|
20
|
+
}
|
|
21
|
+
return { valid: true, receipt };
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=approval-verifier.js.map
|