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 +74 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +243 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/index.ts +342 -0
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|