occ-cloudflare 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # occ-cloudflare
2
+
3
+ Cryptographic proof signing for Cloudflare Workers. Every tool/binding call gets an Ed25519-signed proof pair returned alongside the result.
4
+
5
+ Unlike other OCC integrations, proofs are **returned** (not written to disk) since Cloudflare Workers have no filesystem. Store them wherever you like (KV, D1, R2, Durable Objects).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install occ-cloudflare
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Wrap a tool
16
+
17
+ ```typescript
18
+ import { occWrapTool } from "occ-cloudflare";
19
+
20
+ const searchTool = {
21
+ execute: async (args: { query: string }) => {
22
+ return await doSearch(args.query);
23
+ },
24
+ };
25
+
26
+ const wrapped = occWrapTool(searchTool, "search");
27
+ const { result, proofs } = await wrapped.execute({ query: "OCC" });
28
+
29
+ // Store proofs in KV, D1, R2, etc.
30
+ await env.PROOF_LOG.put(`proof-${Date.now()}`, JSON.stringify(proofs));
31
+ ```
32
+
33
+ ### Wrap a binding
34
+
35
+ ```typescript
36
+ import { occWrapBinding } from "occ-cloudflare";
37
+
38
+ export default {
39
+ async fetch(request: Request, env: Env) {
40
+ const kv = occWrapBinding(env.MY_KV, "my-kv");
41
+
42
+ const { result, proofs } = await kv.get("some-key");
43
+ // result = the KV value
44
+ // proofs = pre/post Ed25519-signed proof entries
45
+ },
46
+ };
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ ```typescript
52
+ interface OCCCloudflareOptions {
53
+ measurement?: string; // Default: "occ-cloudflare:stub"
54
+ agentId?: string; // Default: "cloudflare-worker"
55
+ }
56
+ ```
57
+
58
+ ## How it works
59
+
60
+ 1. `occWrapTool()` wraps a tool's `execute` with pre/post proof signing
61
+ 2. `occWrapBinding()` wraps all methods on a Cloudflare binding via Proxy
62
+ 3. An ephemeral Ed25519 key pair is generated in-memory per Worker invocation
63
+ 4. Pre-execution proof: Ed25519 signature over SHA-256 of tool name + arguments
64
+ 5. Post-execution proof: signature over tool name + args + result
65
+ 6. Proofs are chained via `prevB64` for tamper-evident ordering
66
+ 7. No filesystem access — proofs are returned, not written to disk
67
+
68
+ ## Verify
69
+
70
+ Collect proof entries and write them to a `.jsonl` file, then:
71
+
72
+ ```bash
73
+ npx occ-mcp-proxy verify proof.jsonl
74
+ ```
@@ -0,0 +1,75 @@
1
+ /**
2
+ * occ-cloudflare — Cryptographic proof signing for Cloudflare Workers.
3
+ *
4
+ * Wraps tool/binding calls with Ed25519-signed proofs. Since Cloudflare
5
+ * Workers cannot use node:fs, proof entries are returned (not written to
6
+ * disk). The caller is responsible for storing them (e.g. KV, D1, R2).
7
+ *
8
+ * Uses an in-memory signer — no filesystem persistence. Each Worker
9
+ * invocation starts a fresh proof chain.
10
+ *
11
+ * Usage:
12
+ * import { occWrapTool, occWrapBinding } from "occ-cloudflare";
13
+ *
14
+ * const wrapped = occWrapTool(myTool, "my-tool");
15
+ * const result = await wrapped.execute(args);
16
+ * // result.proofs contains the pre/post proof entries
17
+ */
18
+ import { type OCCProof } from "occproof";
19
+ export interface OCCCloudflareOptions {
20
+ /** Measurement string for proof metadata. Default: "occ-cloudflare:stub" */
21
+ measurement?: string;
22
+ /** Agent identifier for metadata. Default: "cloudflare-worker" */
23
+ agentId?: string;
24
+ }
25
+ export interface ProofLogEntry {
26
+ timestamp: string;
27
+ phase: "pre-execution" | "post-execution";
28
+ tool: string;
29
+ agentId: string;
30
+ args?: Record<string, unknown>;
31
+ output?: unknown;
32
+ proofDigestB64: string;
33
+ receipt: OCCProof;
34
+ }
35
+ export interface WrappedResult<T = unknown> {
36
+ /** The original tool result */
37
+ result: T;
38
+ /** Pre and post execution proof entries */
39
+ proofs: ProofLogEntry[];
40
+ }
41
+ /**
42
+ * Wrap a single tool's execute function with Ed25519 proof signing.
43
+ *
44
+ * Since Cloudflare Workers have no filesystem, proofs are returned
45
+ * alongside the result rather than written to disk.
46
+ *
47
+ * @param tool - A tool object with an `execute` method
48
+ * @param name - A human-readable name for this tool (used in proof metadata)
49
+ * @param options - Configuration options
50
+ * @returns A wrapped tool whose execute returns { result, proofs }
51
+ */
52
+ export declare function occWrapTool<T extends {
53
+ execute?: (...args: any[]) => any;
54
+ }>(tool: T, name: string, options?: OCCCloudflareOptions): T & {
55
+ execute: (...args: any[]) => Promise<WrappedResult>;
56
+ };
57
+ /**
58
+ * Wrap a Cloudflare binding (Service Binding, KV namespace, D1, etc.)
59
+ * so that every method call produces pre/post proof entries.
60
+ *
61
+ * Returns a Proxy around the binding. Each method call returns
62
+ * `{ result, proofs }` instead of the raw result.
63
+ *
64
+ * @param binding - A Cloudflare binding object (e.g. env.MY_SERVICE)
65
+ * @param name - A human-readable name for this binding
66
+ * @param options - Configuration options
67
+ * @returns A proxied binding with proof-wrapped methods
68
+ */
69
+ export declare function occWrapBinding<B extends Record<string, any>>(binding: B, name: string, options?: OCCCloudflareOptions): B;
70
+ /**
71
+ * Reset the in-memory signer. Useful for testing or when you want to
72
+ * start a fresh proof chain within the same Worker invocation.
73
+ */
74
+ export declare function resetSigner(): void;
75
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAA6B,KAAK,QAAQ,EAAyB,MAAM,UAAU,CAAC;AAQ3F,MAAM,WAAW,oBAAoB;IACnC,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAAC;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,QAAQ,CAAC;CACnB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,OAAO;IACxC,+BAA+B;IAC/B,MAAM,EAAE,CAAC,CAAC;IACV,2CAA2C;IAC3C,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAyHD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS;IAAE,OAAO,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;CAAE,EACzE,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,oBAAoB,GAC7B,CAAC,GAAG;IAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,aAAa,CAAC,CAAA;CAAE,CA0D7D;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC1D,OAAO,EAAE,CAAC,EACV,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,oBAAoB,GAC7B,CAAC,CAiEH;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAElC"}
package/dist/index.js ADDED
@@ -0,0 +1,243 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2024-2026 Mike Argento
3
+ /**
4
+ * occ-cloudflare — Cryptographic proof signing for Cloudflare Workers.
5
+ *
6
+ * Wraps tool/binding calls with Ed25519-signed proofs. Since Cloudflare
7
+ * Workers cannot use node:fs, proof entries are returned (not written to
8
+ * disk). The caller is responsible for storing them (e.g. KV, D1, R2).
9
+ *
10
+ * Uses an in-memory signer — no filesystem persistence. Each Worker
11
+ * invocation starts a fresh proof chain.
12
+ *
13
+ * Usage:
14
+ * import { occWrapTool, occWrapBinding } from "occ-cloudflare";
15
+ *
16
+ * const wrapped = occWrapTool(myTool, "my-tool");
17
+ * const result = await wrapped.execute(args);
18
+ * // result.proofs contains the pre/post proof entries
19
+ */
20
+ import { Constructor, canonicalize } from "occproof";
21
+ import { sha256 } from "@noble/hashes/sha256";
22
+ import * as ed from "@noble/ed25519";
23
+ class InMemoryHost {
24
+ enforcementTier = "stub";
25
+ #privateKey;
26
+ #publicKey;
27
+ #measurement;
28
+ #counter = 0n;
29
+ constructor(privateKey, publicKey, measurement) {
30
+ this.#privateKey = privateKey;
31
+ this.#publicKey = publicKey;
32
+ this.#measurement = measurement;
33
+ }
34
+ async getMeasurement() {
35
+ return this.#measurement;
36
+ }
37
+ async getFreshNonce() {
38
+ const nonce = new Uint8Array(32);
39
+ crypto.getRandomValues(nonce);
40
+ return nonce;
41
+ }
42
+ async sign(data) {
43
+ return ed.signAsync(data, this.#privateKey);
44
+ }
45
+ async getPublicKey() {
46
+ return this.#publicKey;
47
+ }
48
+ async nextCounter() {
49
+ this.#counter += 1n;
50
+ return this.#counter.toString();
51
+ }
52
+ async secureTime() {
53
+ return Date.now();
54
+ }
55
+ }
56
+ let signerPromise;
57
+ async function getSigner(measurement) {
58
+ if (signerPromise)
59
+ return signerPromise;
60
+ signerPromise = (async () => {
61
+ // Generate ephemeral Ed25519 key pair in memory
62
+ const privateKey = ed.utils.randomPrivateKey();
63
+ const publicKey = await ed.getPublicKeyAsync(privateKey);
64
+ const host = new InMemoryHost(privateKey, publicKey, measurement);
65
+ const constructor = await Constructor.initialize({
66
+ host,
67
+ policy: { requireCounter: true, requireTime: true },
68
+ });
69
+ const publicKeyB64 = uint8ToBase64(publicKey);
70
+ return { constructor, host, publicKeyB64, lastProofHash: undefined };
71
+ })();
72
+ return signerPromise;
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Helpers (no Buffer in Workers — use native btoa/Uint8Array)
76
+ // ---------------------------------------------------------------------------
77
+ function uint8ToBase64(bytes) {
78
+ let binary = "";
79
+ for (let i = 0; i < bytes.length; i++) {
80
+ binary += String.fromCharCode(bytes[i]);
81
+ }
82
+ return btoa(binary);
83
+ }
84
+ function hashPayload(data) {
85
+ const bytes = new TextEncoder().encode(JSON.stringify(data));
86
+ return uint8ToBase64(sha256(bytes));
87
+ }
88
+ async function signDigest(digestB64, metadata, signer) {
89
+ const commitInput = { digestB64, metadata };
90
+ if (signer.lastProofHash)
91
+ commitInput.prevProofHashB64 = signer.lastProofHash;
92
+ const proof = await signer.constructor.commitDigest(commitInput);
93
+ // Chain: store this proof's hash for the next commit
94
+ const proofHash = uint8ToBase64(sha256(canonicalize(proof)));
95
+ signer.lastProofHash = proofHash;
96
+ return proof;
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // Tool wrapper
100
+ // ---------------------------------------------------------------------------
101
+ /**
102
+ * Wrap a single tool's execute function with Ed25519 proof signing.
103
+ *
104
+ * Since Cloudflare Workers have no filesystem, proofs are returned
105
+ * alongside the result rather than written to disk.
106
+ *
107
+ * @param tool - A tool object with an `execute` method
108
+ * @param name - A human-readable name for this tool (used in proof metadata)
109
+ * @param options - Configuration options
110
+ * @returns A wrapped tool whose execute returns { result, proofs }
111
+ */
112
+ export function occWrapTool(tool, name, options) {
113
+ const measurement = options?.measurement ?? "occ-cloudflare:stub";
114
+ const agentId = options?.agentId ?? "cloudflare-worker";
115
+ const originalExecute = tool.execute;
116
+ if (!originalExecute) {
117
+ return { ...tool, execute: async () => ({ result: undefined, proofs: [] }) };
118
+ }
119
+ const wrappedExecute = async (...args) => {
120
+ const toolArgs = args[0] ?? {};
121
+ const signer = await getSigner(measurement);
122
+ const proofs = [];
123
+ // Pre-execution proof
124
+ const preDigest = hashPayload({ tool: name, args: toolArgs });
125
+ const preProof = await signDigest(preDigest, {
126
+ phase: "pre-execution",
127
+ tool: name,
128
+ agentId,
129
+ }, signer);
130
+ proofs.push({
131
+ timestamp: new Date().toISOString(),
132
+ phase: "pre-execution",
133
+ tool: name,
134
+ agentId,
135
+ args: toolArgs,
136
+ proofDigestB64: preDigest,
137
+ receipt: preProof,
138
+ });
139
+ // Execute the real tool
140
+ const result = await originalExecute.apply(tool, args);
141
+ // Post-execution proof
142
+ const postDigest = hashPayload({ tool: name, args: toolArgs, result });
143
+ const postProof = await signDigest(postDigest, {
144
+ phase: "post-execution",
145
+ tool: name,
146
+ agentId,
147
+ }, signer);
148
+ proofs.push({
149
+ timestamp: new Date().toISOString(),
150
+ phase: "post-execution",
151
+ tool: name,
152
+ agentId,
153
+ args: toolArgs,
154
+ output: typeof result === "string" ? result.slice(0, 1000) : result,
155
+ proofDigestB64: postDigest,
156
+ receipt: postProof,
157
+ });
158
+ return { result, proofs };
159
+ };
160
+ return { ...tool, execute: wrappedExecute };
161
+ }
162
+ // ---------------------------------------------------------------------------
163
+ // Binding wrapper (Service Bindings, KV, D1, etc.)
164
+ // ---------------------------------------------------------------------------
165
+ /**
166
+ * Wrap a Cloudflare binding (Service Binding, KV namespace, D1, etc.)
167
+ * so that every method call produces pre/post proof entries.
168
+ *
169
+ * Returns a Proxy around the binding. Each method call returns
170
+ * `{ result, proofs }` instead of the raw result.
171
+ *
172
+ * @param binding - A Cloudflare binding object (e.g. env.MY_SERVICE)
173
+ * @param name - A human-readable name for this binding
174
+ * @param options - Configuration options
175
+ * @returns A proxied binding with proof-wrapped methods
176
+ */
177
+ export function occWrapBinding(binding, name, options) {
178
+ const measurement = options?.measurement ?? "occ-cloudflare:stub";
179
+ const agentId = options?.agentId ?? "cloudflare-worker";
180
+ return new Proxy(binding, {
181
+ get(target, prop) {
182
+ const original = target[prop];
183
+ if (typeof original !== "function")
184
+ return original;
185
+ return async (...args) => {
186
+ const methodName = `${name}.${String(prop)}`;
187
+ const signer = await getSigner(measurement);
188
+ const proofs = [];
189
+ // Pre-execution proof
190
+ const preDigest = hashPayload({
191
+ tool: methodName,
192
+ args: args.length === 1 ? args[0] : args,
193
+ });
194
+ const preProof = await signDigest(preDigest, {
195
+ phase: "pre-execution",
196
+ tool: methodName,
197
+ agentId,
198
+ }, signer);
199
+ proofs.push({
200
+ timestamp: new Date().toISOString(),
201
+ phase: "pre-execution",
202
+ tool: methodName,
203
+ agentId,
204
+ args: args.length === 1 ? args[0] : { _args: args },
205
+ proofDigestB64: preDigest,
206
+ receipt: preProof,
207
+ });
208
+ // Execute the real method
209
+ const result = await original.apply(target, args);
210
+ // Post-execution proof
211
+ const postDigest = hashPayload({
212
+ tool: methodName,
213
+ args: args.length === 1 ? args[0] : args,
214
+ result,
215
+ });
216
+ const postProof = await signDigest(postDigest, {
217
+ phase: "post-execution",
218
+ tool: methodName,
219
+ agentId,
220
+ }, signer);
221
+ proofs.push({
222
+ timestamp: new Date().toISOString(),
223
+ phase: "post-execution",
224
+ tool: methodName,
225
+ agentId,
226
+ args: args.length === 1 ? args[0] : { _args: args },
227
+ output: typeof result === "string" ? result.slice(0, 1000) : result,
228
+ proofDigestB64: postDigest,
229
+ receipt: postProof,
230
+ });
231
+ return { result, proofs };
232
+ };
233
+ },
234
+ });
235
+ }
236
+ /**
237
+ * Reset the in-memory signer. Useful for testing or when you want to
238
+ * start a fresh proof chain within the same Worker invocation.
239
+ */
240
+ export function resetSigner() {
241
+ signerPromise = undefined;
242
+ }
243
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "occ-cloudflare",
3
+ "version": "0.1.0",
4
+ "description": "OCC cryptographic proof signing for Cloudflare Workers tool calls",
5
+ "author": "Mike Argento",
6
+ "license": "Apache-2.0",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "occproof": "^1.0.2",
27
+ "@noble/ed25519": "^2.1.0",
28
+ "@noble/hashes": "^1.4.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0",
32
+ "@cloudflare/workers-types": ">=4.0.0",
33
+ "typescript": "^5.4.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=20.0.0"
37
+ }
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,342 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2024-2026 Mike Argento
3
+
4
+ /**
5
+ * occ-cloudflare — Cryptographic proof signing for Cloudflare Workers.
6
+ *
7
+ * Wraps tool/binding calls with Ed25519-signed proofs. Since Cloudflare
8
+ * Workers cannot use node:fs, proof entries are returned (not written to
9
+ * disk). The caller is responsible for storing them (e.g. KV, D1, R2).
10
+ *
11
+ * Uses an in-memory signer — no filesystem persistence. Each Worker
12
+ * invocation starts a fresh proof chain.
13
+ *
14
+ * Usage:
15
+ * import { occWrapTool, occWrapBinding } from "occ-cloudflare";
16
+ *
17
+ * const wrapped = occWrapTool(myTool, "my-tool");
18
+ * const result = await wrapped.execute(args);
19
+ * // result.proofs contains the pre/post proof entries
20
+ */
21
+
22
+ import { Constructor, canonicalize, type OCCProof, type HostCapabilities } from "occproof";
23
+ import { sha256 } from "@noble/hashes/sha256";
24
+ import * as ed from "@noble/ed25519";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Types
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export interface OCCCloudflareOptions {
31
+ /** Measurement string for proof metadata. Default: "occ-cloudflare:stub" */
32
+ measurement?: string;
33
+ /** Agent identifier for metadata. Default: "cloudflare-worker" */
34
+ agentId?: string;
35
+ }
36
+
37
+ export interface ProofLogEntry {
38
+ timestamp: string;
39
+ phase: "pre-execution" | "post-execution";
40
+ tool: string;
41
+ agentId: string;
42
+ args?: Record<string, unknown>;
43
+ output?: unknown;
44
+ proofDigestB64: string;
45
+ receipt: OCCProof;
46
+ }
47
+
48
+ export interface WrappedResult<T = unknown> {
49
+ /** The original tool result */
50
+ result: T;
51
+ /** Pre and post execution proof entries */
52
+ proofs: ProofLogEntry[];
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // In-memory signer (no filesystem, no occ-stub)
57
+ // ---------------------------------------------------------------------------
58
+
59
+ interface SignerState {
60
+ constructor: Constructor;
61
+ host: InMemoryHost;
62
+ publicKeyB64: string;
63
+ lastProofHash: string | undefined;
64
+ }
65
+
66
+ class InMemoryHost implements HostCapabilities {
67
+ readonly enforcementTier = "stub" as const;
68
+ readonly #privateKey: Uint8Array;
69
+ readonly #publicKey: Uint8Array;
70
+ readonly #measurement: string;
71
+ #counter = 0n;
72
+
73
+ constructor(privateKey: Uint8Array, publicKey: Uint8Array, measurement: string) {
74
+ this.#privateKey = privateKey;
75
+ this.#publicKey = publicKey;
76
+ this.#measurement = measurement;
77
+ }
78
+
79
+ async getMeasurement(): Promise<string> {
80
+ return this.#measurement;
81
+ }
82
+
83
+ async getFreshNonce(): Promise<Uint8Array> {
84
+ const nonce = new Uint8Array(32);
85
+ crypto.getRandomValues(nonce);
86
+ return nonce;
87
+ }
88
+
89
+ async sign(data: Uint8Array): Promise<Uint8Array> {
90
+ return ed.signAsync(data, this.#privateKey);
91
+ }
92
+
93
+ async getPublicKey(): Promise<Uint8Array> {
94
+ return this.#publicKey;
95
+ }
96
+
97
+ async nextCounter(): Promise<string> {
98
+ this.#counter += 1n;
99
+ return this.#counter.toString();
100
+ }
101
+
102
+ async secureTime(): Promise<number> {
103
+ return Date.now();
104
+ }
105
+ }
106
+
107
+ let signerPromise: Promise<SignerState> | undefined;
108
+
109
+ async function getSigner(measurement: string): Promise<SignerState> {
110
+ if (signerPromise) return signerPromise;
111
+
112
+ signerPromise = (async () => {
113
+ // Generate ephemeral Ed25519 key pair in memory
114
+ const privateKey = ed.utils.randomPrivateKey();
115
+ const publicKey = await ed.getPublicKeyAsync(privateKey);
116
+
117
+ const host = new InMemoryHost(privateKey, publicKey, measurement);
118
+
119
+ const constructor = await Constructor.initialize({
120
+ host,
121
+ policy: { requireCounter: true, requireTime: true },
122
+ });
123
+
124
+ const publicKeyB64 = uint8ToBase64(publicKey);
125
+ return { constructor, host, publicKeyB64, lastProofHash: undefined };
126
+ })();
127
+
128
+ return signerPromise;
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Helpers (no Buffer in Workers — use native btoa/Uint8Array)
133
+ // ---------------------------------------------------------------------------
134
+
135
+ function uint8ToBase64(bytes: Uint8Array): string {
136
+ let binary = "";
137
+ for (let i = 0; i < bytes.length; i++) {
138
+ binary += String.fromCharCode(bytes[i]);
139
+ }
140
+ return btoa(binary);
141
+ }
142
+
143
+ function hashPayload(data: unknown): string {
144
+ const bytes = new TextEncoder().encode(JSON.stringify(data));
145
+ return uint8ToBase64(sha256(bytes));
146
+ }
147
+
148
+ async function signDigest(
149
+ digestB64: string,
150
+ metadata: Record<string, unknown>,
151
+ signer: SignerState,
152
+ ): Promise<OCCProof> {
153
+ const commitInput: {
154
+ digestB64: string;
155
+ metadata?: Record<string, unknown>;
156
+ prevProofHashB64?: string;
157
+ } = { digestB64, metadata };
158
+
159
+ if (signer.lastProofHash) commitInput.prevProofHashB64 = signer.lastProofHash;
160
+
161
+ const proof = await signer.constructor.commitDigest(commitInput);
162
+
163
+ // Chain: store this proof's hash for the next commit
164
+ const proofHash = uint8ToBase64(sha256(canonicalize(proof)));
165
+ signer.lastProofHash = proofHash;
166
+
167
+ return proof;
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Tool wrapper
172
+ // ---------------------------------------------------------------------------
173
+
174
+ /**
175
+ * Wrap a single tool's execute function with Ed25519 proof signing.
176
+ *
177
+ * Since Cloudflare Workers have no filesystem, proofs are returned
178
+ * alongside the result rather than written to disk.
179
+ *
180
+ * @param tool - A tool object with an `execute` method
181
+ * @param name - A human-readable name for this tool (used in proof metadata)
182
+ * @param options - Configuration options
183
+ * @returns A wrapped tool whose execute returns { result, proofs }
184
+ */
185
+ export function occWrapTool<T extends { execute?: (...args: any[]) => any }>(
186
+ tool: T,
187
+ name: string,
188
+ options?: OCCCloudflareOptions,
189
+ ): T & { execute: (...args: any[]) => Promise<WrappedResult> } {
190
+ const measurement = options?.measurement ?? "occ-cloudflare:stub";
191
+ const agentId = options?.agentId ?? "cloudflare-worker";
192
+
193
+ const originalExecute = tool.execute;
194
+ if (!originalExecute) {
195
+ return { ...tool, execute: async () => ({ result: undefined, proofs: [] }) } as any;
196
+ }
197
+
198
+ const wrappedExecute = async (...args: any[]): Promise<WrappedResult> => {
199
+ const toolArgs = args[0] ?? {};
200
+ const signer = await getSigner(measurement);
201
+ const proofs: ProofLogEntry[] = [];
202
+
203
+ // Pre-execution proof
204
+ const preDigest = hashPayload({ tool: name, args: toolArgs });
205
+ const preProof = await signDigest(preDigest, {
206
+ phase: "pre-execution",
207
+ tool: name,
208
+ agentId,
209
+ }, signer);
210
+
211
+ proofs.push({
212
+ timestamp: new Date().toISOString(),
213
+ phase: "pre-execution",
214
+ tool: name,
215
+ agentId,
216
+ args: toolArgs,
217
+ proofDigestB64: preDigest,
218
+ receipt: preProof,
219
+ });
220
+
221
+ // Execute the real tool
222
+ const result = await originalExecute.apply(tool, args);
223
+
224
+ // Post-execution proof
225
+ const postDigest = hashPayload({ tool: name, args: toolArgs, result });
226
+ const postProof = await signDigest(postDigest, {
227
+ phase: "post-execution",
228
+ tool: name,
229
+ agentId,
230
+ }, signer);
231
+
232
+ proofs.push({
233
+ timestamp: new Date().toISOString(),
234
+ phase: "post-execution",
235
+ tool: name,
236
+ agentId,
237
+ args: toolArgs,
238
+ output: typeof result === "string" ? result.slice(0, 1000) : result,
239
+ proofDigestB64: postDigest,
240
+ receipt: postProof,
241
+ });
242
+
243
+ return { result, proofs };
244
+ };
245
+
246
+ return { ...tool, execute: wrappedExecute } as any;
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Binding wrapper (Service Bindings, KV, D1, etc.)
251
+ // ---------------------------------------------------------------------------
252
+
253
+ /**
254
+ * Wrap a Cloudflare binding (Service Binding, KV namespace, D1, etc.)
255
+ * so that every method call produces pre/post proof entries.
256
+ *
257
+ * Returns a Proxy around the binding. Each method call returns
258
+ * `{ result, proofs }` instead of the raw result.
259
+ *
260
+ * @param binding - A Cloudflare binding object (e.g. env.MY_SERVICE)
261
+ * @param name - A human-readable name for this binding
262
+ * @param options - Configuration options
263
+ * @returns A proxied binding with proof-wrapped methods
264
+ */
265
+ export function occWrapBinding<B extends Record<string, any>>(
266
+ binding: B,
267
+ name: string,
268
+ options?: OCCCloudflareOptions,
269
+ ): B {
270
+ const measurement = options?.measurement ?? "occ-cloudflare:stub";
271
+ const agentId = options?.agentId ?? "cloudflare-worker";
272
+
273
+ return new Proxy(binding, {
274
+ get(target, prop: string | symbol) {
275
+ const original = target[prop as keyof B];
276
+ if (typeof original !== "function") return original;
277
+
278
+ return async (...args: any[]): Promise<WrappedResult> => {
279
+ const methodName = `${name}.${String(prop)}`;
280
+ const signer = await getSigner(measurement);
281
+ const proofs: ProofLogEntry[] = [];
282
+
283
+ // Pre-execution proof
284
+ const preDigest = hashPayload({
285
+ tool: methodName,
286
+ args: args.length === 1 ? args[0] : args,
287
+ });
288
+ const preProof = await signDigest(preDigest, {
289
+ phase: "pre-execution",
290
+ tool: methodName,
291
+ agentId,
292
+ }, signer);
293
+
294
+ proofs.push({
295
+ timestamp: new Date().toISOString(),
296
+ phase: "pre-execution",
297
+ tool: methodName,
298
+ agentId,
299
+ args: args.length === 1 ? args[0] : { _args: args },
300
+ proofDigestB64: preDigest,
301
+ receipt: preProof,
302
+ });
303
+
304
+ // Execute the real method
305
+ const result = await original.apply(target, args);
306
+
307
+ // Post-execution proof
308
+ const postDigest = hashPayload({
309
+ tool: methodName,
310
+ args: args.length === 1 ? args[0] : args,
311
+ result,
312
+ });
313
+ const postProof = await signDigest(postDigest, {
314
+ phase: "post-execution",
315
+ tool: methodName,
316
+ agentId,
317
+ }, signer);
318
+
319
+ proofs.push({
320
+ timestamp: new Date().toISOString(),
321
+ phase: "post-execution",
322
+ tool: methodName,
323
+ agentId,
324
+ args: args.length === 1 ? args[0] : { _args: args },
325
+ output: typeof result === "string" ? result.slice(0, 1000) : result,
326
+ proofDigestB64: postDigest,
327
+ receipt: postProof,
328
+ });
329
+
330
+ return { result, proofs };
331
+ };
332
+ },
333
+ }) as B;
334
+ }
335
+
336
+ /**
337
+ * Reset the in-memory signer. Useful for testing or when you want to
338
+ * start a fresh proof chain within the same Worker invocation.
339
+ */
340
+ export function resetSigner(): void {
341
+ signerPromise = undefined;
342
+ }