@occ-core/stub 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.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @occ/stub public API
3
+ */
4
+ export { StubHost } from "./stub-host.js";
5
+ export type { StubHostOptions, PersistentStubHostOptions } from "./stub-host.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @occ/stub public API
3
+ */
4
+ export { StubHost } from "./stub-host.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @occ/stub — StubHost
3
+ *
4
+ * An in-process implementation of HostCapabilities for development and
5
+ * testing. All TEE-specific operations are performed in software using
6
+ * Node.js built-ins and @noble/ed25519.
7
+ *
8
+ * SECURITY WARNING: This implementation provides NO security guarantees.
9
+ * Keys are held in process memory (or a plaintext JSON file for persistence),
10
+ * the counter is software-only, and there is no hardware-backed attestation.
11
+ *
12
+ * DO NOT use StubHost in production or in any security-sensitive context.
13
+ * Its sole purpose is to allow developers to run, test, and explore
14
+ * occ-core locally without a real TEE.
15
+ */
16
+ import type { HostCapabilities } from "@occ/core";
17
+ export interface StubHostOptions {
18
+ /**
19
+ * Ed25519 private key (32 bytes).
20
+ * If omitted, a fresh random key is generated.
21
+ * Providing a fixed key is useful for deterministic tests.
22
+ */
23
+ privateKey?: Uint8Array;
24
+ /**
25
+ * Measurement string returned by getMeasurement().
26
+ * Defaults to a fixed placeholder string.
27
+ */
28
+ measurement?: string;
29
+ /**
30
+ * Initial counter value. Defaults to 0.
31
+ */
32
+ initialCounter?: bigint;
33
+ /**
34
+ * If true, secureTime() is available and returns Date.now().
35
+ * Defaults to true.
36
+ */
37
+ enableTime?: boolean;
38
+ /**
39
+ * If true, nextCounter() is available.
40
+ * Defaults to true.
41
+ */
42
+ enableCounter?: boolean;
43
+ }
44
+ export interface PersistentStubHostOptions {
45
+ /**
46
+ * Path to a JSON file where identity and counter state is persisted.
47
+ * If the file does not exist, it is created with a fresh keypair and
48
+ * counter starting at 0. If it exists, the stored key and counter
49
+ * are loaded and resumed.
50
+ */
51
+ statePath: string;
52
+ /**
53
+ * Measurement string returned by getMeasurement().
54
+ * Defaults to "stub:measurement:not-a-real-tee".
55
+ */
56
+ measurement?: string;
57
+ /**
58
+ * If true, secureTime() is available and returns Date.now().
59
+ * Defaults to true.
60
+ */
61
+ enableTime?: boolean;
62
+ /**
63
+ * If true, nextCounter() is available and persists across restarts.
64
+ * Defaults to true.
65
+ */
66
+ enableCounter?: boolean;
67
+ }
68
+ /**
69
+ * StubHost wraps a HostCapabilities object built from in-process crypto.
70
+ *
71
+ * Optional capabilities are conditionally attached to the plain object
72
+ * returned by `.host` — this avoids TypeScript exactOptionalPropertyTypes
73
+ * conflicts that arise when implementing optional interface methods as class
74
+ * fields that could be undefined.
75
+ */
76
+ export declare class StubHost {
77
+ #private;
78
+ private constructor();
79
+ /**
80
+ * Record the hash of the last produced proof into persistent state.
81
+ * Call this after each successful commit when using persistent mode to
82
+ * enable automatic proof chaining across restarts.
83
+ */
84
+ setLastProofHash(hashB64: string): void;
85
+ /**
86
+ * Read the last proof hash from persistent state (for chaining).
87
+ * Returns undefined if no proof has been committed yet.
88
+ */
89
+ getLastProofHash(): string | undefined;
90
+ /**
91
+ * Create a StubHost with ephemeral (in-memory) state.
92
+ * Counter resets to 0 on every process restart.
93
+ * Async because key derivation is async in @noble/ed25519.
94
+ */
95
+ static create(opts?: StubHostOptions): Promise<StubHost>;
96
+ /**
97
+ * Create a StubHost that persists identity and counter to a JSON file.
98
+ *
99
+ * On first call: generates a fresh keypair and writes it to `statePath`.
100
+ * On subsequent calls: loads the existing keypair and counter and resumes.
101
+ *
102
+ * This lets a local demo service maintain a stable identity and
103
+ * monotonically increasing counter across restarts.
104
+ *
105
+ * The state file is plaintext JSON — not for production use.
106
+ */
107
+ static createPersistent(opts: PersistentStubHostOptions): Promise<StubHost>;
108
+ /**
109
+ * Return the HostCapabilities object to pass to Constructor.initialize().
110
+ */
111
+ get host(): HostCapabilities;
112
+ /** Return the raw private key bytes (test use only). */
113
+ get privateKeyBytes(): Uint8Array;
114
+ /** Return the raw public key bytes. */
115
+ get publicKeyBytes(): Uint8Array;
116
+ /** Return the current counter value without advancing it. */
117
+ get currentCounter(): bigint;
118
+ }
119
+ //# sourceMappingURL=stub-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stub-host.d.ts","sourceRoot":"","sources":["../src/stub-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAMlD,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IAExB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAMD,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAmBD;;;;;;;GAOG;AACH,qBAAa,QAAQ;;IAQnB,OAAO;IAoDP;;;;OAIG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAevC;;;OAGG;IACH,gBAAgB,IAAI,MAAM,GAAG,SAAS;IActC;;;;OAIG;WACU,MAAM,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAkBlE;;;;;;;;;;OAUG;WACU,gBAAgB,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAmDjF;;OAEG;IACH,IAAI,IAAI,IAAI,gBAAgB,CAE3B;IAED,wDAAwD;IACxD,IAAI,eAAe,IAAI,UAAU,CAEhC;IAED,uCAAuC;IACvC,IAAI,cAAc,IAAI,UAAU,CAE/B;IAED,6DAA6D;IAC7D,IAAI,cAAc,IAAI,MAAM,CAE3B;CACF"}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * @occ/stub — StubHost
3
+ *
4
+ * An in-process implementation of HostCapabilities for development and
5
+ * testing. All TEE-specific operations are performed in software using
6
+ * Node.js built-ins and @noble/ed25519.
7
+ *
8
+ * SECURITY WARNING: This implementation provides NO security guarantees.
9
+ * Keys are held in process memory (or a plaintext JSON file for persistence),
10
+ * the counter is software-only, and there is no hardware-backed attestation.
11
+ *
12
+ * DO NOT use StubHost in production or in any security-sensitive context.
13
+ * Its sole purpose is to allow developers to run, test, and explore
14
+ * occ-core locally without a real TEE.
15
+ */
16
+ import { getPublicKeyAsync, signAsync, utils } from "@noble/ed25519";
17
+ import { randomBytes } from "crypto";
18
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
19
+ import { dirname } from "path";
20
+ // ---------------------------------------------------------------------------
21
+ // StubHost
22
+ // ---------------------------------------------------------------------------
23
+ /**
24
+ * StubHost wraps a HostCapabilities object built from in-process crypto.
25
+ *
26
+ * Optional capabilities are conditionally attached to the plain object
27
+ * returned by `.host` — this avoids TypeScript exactOptionalPropertyTypes
28
+ * conflicts that arise when implementing optional interface methods as class
29
+ * fields that could be undefined.
30
+ */
31
+ export class StubHost {
32
+ #host;
33
+ #privateKey;
34
+ #publicKey;
35
+ #measurement;
36
+ #counter;
37
+ #statePath;
38
+ constructor(privateKey, publicKey, opts, statePath) {
39
+ this.#privateKey = privateKey;
40
+ this.#publicKey = publicKey;
41
+ this.#measurement = opts.measurement;
42
+ this.#counter = opts.initialCounter;
43
+ this.#statePath = statePath;
44
+ // Build the base host with required capabilities
45
+ const base = {
46
+ enforcementTier: "stub",
47
+ getMeasurement: async () => this.#measurement,
48
+ getFreshNonce: async () => new Uint8Array(randomBytes(32)),
49
+ sign: async (data) => signAsync(data, this.#privateKey),
50
+ getPublicKey: async () => this.#publicKey,
51
+ };
52
+ // Conditionally attach optional capabilities
53
+ if (opts.enableCounter) {
54
+ base.nextCounter = async () => {
55
+ this.#counter += 1n;
56
+ if (this.#statePath !== undefined) {
57
+ this.#persistState();
58
+ }
59
+ return String(this.#counter);
60
+ };
61
+ }
62
+ if (opts.enableTime) {
63
+ base.secureTime = async () => Date.now();
64
+ }
65
+ this.#host = base;
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // Persistence helpers
69
+ // ---------------------------------------------------------------------------
70
+ #persistState() {
71
+ if (this.#statePath === undefined)
72
+ return;
73
+ const state = {
74
+ privateKeyB64: Buffer.from(this.#privateKey).toString("base64"),
75
+ counter: String(this.#counter),
76
+ };
77
+ writeFileSync(this.#statePath, JSON.stringify(state, null, 2), "utf8");
78
+ }
79
+ /**
80
+ * Record the hash of the last produced proof into persistent state.
81
+ * Call this after each successful commit when using persistent mode to
82
+ * enable automatic proof chaining across restarts.
83
+ */
84
+ setLastProofHash(hashB64) {
85
+ if (this.#statePath === undefined)
86
+ return;
87
+ let existing;
88
+ try {
89
+ existing = JSON.parse(readFileSync(this.#statePath, "utf8"));
90
+ }
91
+ catch {
92
+ existing = {
93
+ privateKeyB64: Buffer.from(this.#privateKey).toString("base64"),
94
+ counter: String(this.#counter),
95
+ };
96
+ }
97
+ existing.lastProofHashB64 = hashB64;
98
+ writeFileSync(this.#statePath, JSON.stringify(existing, null, 2), "utf8");
99
+ }
100
+ /**
101
+ * Read the last proof hash from persistent state (for chaining).
102
+ * Returns undefined if no proof has been committed yet.
103
+ */
104
+ getLastProofHash() {
105
+ if (this.#statePath === undefined)
106
+ return undefined;
107
+ try {
108
+ const raw = JSON.parse(readFileSync(this.#statePath, "utf8"));
109
+ return raw.lastProofHashB64;
110
+ }
111
+ catch {
112
+ return undefined;
113
+ }
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // Factory methods
117
+ // ---------------------------------------------------------------------------
118
+ /**
119
+ * Create a StubHost with ephemeral (in-memory) state.
120
+ * Counter resets to 0 on every process restart.
121
+ * Async because key derivation is async in @noble/ed25519.
122
+ */
123
+ static async create(opts = {}) {
124
+ const privateKey = opts.privateKey ?? utils.randomPrivateKey();
125
+ if (privateKey.length !== 32) {
126
+ throw new RangeError("StubHost: privateKey must be 32 bytes");
127
+ }
128
+ const publicKey = await getPublicKeyAsync(privateKey);
129
+ const resolved = {
130
+ privateKey,
131
+ measurement: opts.measurement ?? "stub:measurement:not-a-real-tee",
132
+ initialCounter: opts.initialCounter ?? 0n,
133
+ enableTime: opts.enableTime ?? true,
134
+ enableCounter: opts.enableCounter ?? true,
135
+ };
136
+ return new StubHost(privateKey, publicKey, resolved);
137
+ }
138
+ /**
139
+ * Create a StubHost that persists identity and counter to a JSON file.
140
+ *
141
+ * On first call: generates a fresh keypair and writes it to `statePath`.
142
+ * On subsequent calls: loads the existing keypair and counter and resumes.
143
+ *
144
+ * This lets a local demo service maintain a stable identity and
145
+ * monotonically increasing counter across restarts.
146
+ *
147
+ * The state file is plaintext JSON — not for production use.
148
+ */
149
+ static async createPersistent(opts) {
150
+ const { statePath, measurement, enableTime, enableCounter } = opts;
151
+ let privateKey;
152
+ let initialCounter;
153
+ // Try to load existing state
154
+ let existingState;
155
+ try {
156
+ existingState = JSON.parse(readFileSync(statePath, "utf8"));
157
+ }
158
+ catch {
159
+ // File doesn't exist or is corrupt — start fresh
160
+ }
161
+ if (existingState !== undefined) {
162
+ privateKey = new Uint8Array(Buffer.from(existingState.privateKeyB64, "base64"));
163
+ initialCounter = BigInt(existingState.counter);
164
+ }
165
+ else {
166
+ // First run — generate a fresh key
167
+ privateKey = utils.randomPrivateKey();
168
+ initialCounter = 0n;
169
+ // Ensure the directory exists
170
+ mkdirSync(dirname(statePath), { recursive: true });
171
+ const initialState = {
172
+ privateKeyB64: Buffer.from(privateKey).toString("base64"),
173
+ counter: String(initialCounter),
174
+ };
175
+ writeFileSync(statePath, JSON.stringify(initialState, null, 2), "utf8");
176
+ }
177
+ if (privateKey.length !== 32) {
178
+ throw new RangeError("StubHost.createPersistent: loaded key is not 32 bytes");
179
+ }
180
+ const publicKey = await getPublicKeyAsync(privateKey);
181
+ const resolved = {
182
+ privateKey,
183
+ measurement: measurement ?? "stub:measurement:not-a-real-tee",
184
+ initialCounter,
185
+ enableTime: enableTime ?? true,
186
+ enableCounter: enableCounter ?? true,
187
+ };
188
+ return new StubHost(privateKey, publicKey, resolved, statePath);
189
+ }
190
+ // ---------------------------------------------------------------------------
191
+ // Accessors
192
+ // ---------------------------------------------------------------------------
193
+ /**
194
+ * Return the HostCapabilities object to pass to Constructor.initialize().
195
+ */
196
+ get host() {
197
+ return this.#host;
198
+ }
199
+ /** Return the raw private key bytes (test use only). */
200
+ get privateKeyBytes() {
201
+ return this.#privateKey;
202
+ }
203
+ /** Return the raw public key bytes. */
204
+ get publicKeyBytes() {
205
+ return this.#publicKey;
206
+ }
207
+ /** Return the current counter value without advancing it. */
208
+ get currentCounter() {
209
+ return this.#counter;
210
+ }
211
+ }
212
+ //# sourceMappingURL=stub-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stub-host.js","sourceRoot":"","sources":["../src/stub-host.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@occ-core/stub",
3
+ "version": "0.1.0",
4
+ "description": "In-process HostCapabilities stub for occ-core — development and testing only",
5
+ "keywords": [
6
+ "occ",
7
+ "origin-controlled-computing",
8
+ "tee",
9
+ "stub",
10
+ "dev",
11
+ "ed25519",
12
+ "tamper-evident"
13
+ ],
14
+ "author": "Mike Argento",
15
+ "license": "Apache-2.0",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/mikeargento/occ-core.git",
19
+ "directory": "packages/occ-core-stub"
20
+ },
21
+ "homepage": "https://github.com/mikeargento/occ-core#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/mikeargento/occ-core/issues"
24
+ },
25
+ "type": "module",
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "sideEffects": false,
29
+ "exports": {
30
+ ".": {
31
+ "import": "./dist/index.js",
32
+ "types": "./dist/index.d.ts"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "!dist/__tests__",
38
+ "src",
39
+ "!src/__tests__"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "npm run build && node --test dist/__tests__/*.test.js",
45
+ "prepublishOnly": "npm run build && npm test"
46
+ },
47
+ "dependencies": {
48
+ "@noble/ed25519": "^2.1.0",
49
+ "@occ-core/core": "*"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^20.0.0",
53
+ "typescript": "^5.4.0"
54
+ },
55
+ "engines": {
56
+ "node": ">=18.0.0"
57
+ }
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @occ/stub public API
3
+ */
4
+ export { StubHost } from "./stub-host.js";
5
+ export type { StubHostOptions, PersistentStubHostOptions } from "./stub-host.js";
@@ -0,0 +1,320 @@
1
+ /**
2
+ * @occ/stub — StubHost
3
+ *
4
+ * An in-process implementation of HostCapabilities for development and
5
+ * testing. All TEE-specific operations are performed in software using
6
+ * Node.js built-ins and @noble/ed25519.
7
+ *
8
+ * SECURITY WARNING: This implementation provides NO security guarantees.
9
+ * Keys are held in process memory (or a plaintext JSON file for persistence),
10
+ * the counter is software-only, and there is no hardware-backed attestation.
11
+ *
12
+ * DO NOT use StubHost in production or in any security-sensitive context.
13
+ * Its sole purpose is to allow developers to run, test, and explore
14
+ * occ-core locally without a real TEE.
15
+ */
16
+
17
+ import { getPublicKeyAsync, signAsync, utils } from "@noble/ed25519";
18
+ import { randomBytes } from "crypto";
19
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
20
+ import { dirname } from "path";
21
+ import type { HostCapabilities } from "@occ/core";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // StubHost options
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface StubHostOptions {
28
+ /**
29
+ * Ed25519 private key (32 bytes).
30
+ * If omitted, a fresh random key is generated.
31
+ * Providing a fixed key is useful for deterministic tests.
32
+ */
33
+ privateKey?: Uint8Array;
34
+
35
+ /**
36
+ * Measurement string returned by getMeasurement().
37
+ * Defaults to a fixed placeholder string.
38
+ */
39
+ measurement?: string;
40
+
41
+ /**
42
+ * Initial counter value. Defaults to 0.
43
+ */
44
+ initialCounter?: bigint;
45
+
46
+ /**
47
+ * If true, secureTime() is available and returns Date.now().
48
+ * Defaults to true.
49
+ */
50
+ enableTime?: boolean;
51
+
52
+ /**
53
+ * If true, nextCounter() is available.
54
+ * Defaults to true.
55
+ */
56
+ enableCounter?: boolean;
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Persistent StubHost options
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export interface PersistentStubHostOptions {
64
+ /**
65
+ * Path to a JSON file where identity and counter state is persisted.
66
+ * If the file does not exist, it is created with a fresh keypair and
67
+ * counter starting at 0. If it exists, the stored key and counter
68
+ * are loaded and resumed.
69
+ */
70
+ statePath: string;
71
+
72
+ /**
73
+ * Measurement string returned by getMeasurement().
74
+ * Defaults to "stub:measurement:not-a-real-tee".
75
+ */
76
+ measurement?: string;
77
+
78
+ /**
79
+ * If true, secureTime() is available and returns Date.now().
80
+ * Defaults to true.
81
+ */
82
+ enableTime?: boolean;
83
+
84
+ /**
85
+ * If true, nextCounter() is available and persists across restarts.
86
+ * Defaults to true.
87
+ */
88
+ enableCounter?: boolean;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Persisted state shape (written as JSON)
93
+ // ---------------------------------------------------------------------------
94
+
95
+ interface PersistedState {
96
+ /** Base64-encoded Ed25519 private key (32 bytes). */
97
+ privateKeyB64: string;
98
+ /** Current counter value as a decimal string (survives BigInt round-trip). */
99
+ counter: string;
100
+ /** Base64 of the last proof's canonical hash — populated by the service layer. */
101
+ lastProofHashB64?: string;
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // StubHost
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * StubHost wraps a HostCapabilities object built from in-process crypto.
110
+ *
111
+ * Optional capabilities are conditionally attached to the plain object
112
+ * returned by `.host` — this avoids TypeScript exactOptionalPropertyTypes
113
+ * conflicts that arise when implementing optional interface methods as class
114
+ * fields that could be undefined.
115
+ */
116
+ export class StubHost {
117
+ readonly #host: HostCapabilities;
118
+ readonly #privateKey: Uint8Array;
119
+ readonly #publicKey: Uint8Array;
120
+ readonly #measurement: string;
121
+ #counter: bigint;
122
+ readonly #statePath: string | undefined;
123
+
124
+ private constructor(
125
+ privateKey: Uint8Array,
126
+ publicKey: Uint8Array,
127
+ opts: Required<StubHostOptions>,
128
+ statePath?: string
129
+ ) {
130
+ this.#privateKey = privateKey;
131
+ this.#publicKey = publicKey;
132
+ this.#measurement = opts.measurement;
133
+ this.#counter = opts.initialCounter;
134
+ this.#statePath = statePath;
135
+
136
+ // Build the base host with required capabilities
137
+ const base: HostCapabilities = {
138
+ enforcementTier: "stub",
139
+ getMeasurement: async () => this.#measurement,
140
+ getFreshNonce: async () => new Uint8Array(randomBytes(32)),
141
+ sign: async (data: Uint8Array) => signAsync(data, this.#privateKey),
142
+ getPublicKey: async () => this.#publicKey,
143
+ };
144
+
145
+ // Conditionally attach optional capabilities
146
+ if (opts.enableCounter) {
147
+ base.nextCounter = async (): Promise<string> => {
148
+ this.#counter += 1n;
149
+ if (this.#statePath !== undefined) {
150
+ this.#persistState();
151
+ }
152
+ return String(this.#counter);
153
+ };
154
+ }
155
+
156
+ if (opts.enableTime) {
157
+ base.secureTime = async (): Promise<number> => Date.now();
158
+ }
159
+
160
+ this.#host = base;
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Persistence helpers
165
+ // ---------------------------------------------------------------------------
166
+
167
+ #persistState(): void {
168
+ if (this.#statePath === undefined) return;
169
+ const state: PersistedState = {
170
+ privateKeyB64: Buffer.from(this.#privateKey).toString("base64"),
171
+ counter: String(this.#counter),
172
+ };
173
+ writeFileSync(this.#statePath, JSON.stringify(state, null, 2), "utf8");
174
+ }
175
+
176
+ /**
177
+ * Record the hash of the last produced proof into persistent state.
178
+ * Call this after each successful commit when using persistent mode to
179
+ * enable automatic proof chaining across restarts.
180
+ */
181
+ setLastProofHash(hashB64: string): void {
182
+ if (this.#statePath === undefined) return;
183
+ let existing: PersistedState;
184
+ try {
185
+ existing = JSON.parse(readFileSync(this.#statePath, "utf8")) as PersistedState;
186
+ } catch {
187
+ existing = {
188
+ privateKeyB64: Buffer.from(this.#privateKey).toString("base64"),
189
+ counter: String(this.#counter),
190
+ };
191
+ }
192
+ existing.lastProofHashB64 = hashB64;
193
+ writeFileSync(this.#statePath, JSON.stringify(existing, null, 2), "utf8");
194
+ }
195
+
196
+ /**
197
+ * Read the last proof hash from persistent state (for chaining).
198
+ * Returns undefined if no proof has been committed yet.
199
+ */
200
+ getLastProofHash(): string | undefined {
201
+ if (this.#statePath === undefined) return undefined;
202
+ try {
203
+ const raw = JSON.parse(readFileSync(this.#statePath, "utf8")) as PersistedState;
204
+ return raw.lastProofHashB64;
205
+ } catch {
206
+ return undefined;
207
+ }
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Factory methods
212
+ // ---------------------------------------------------------------------------
213
+
214
+ /**
215
+ * Create a StubHost with ephemeral (in-memory) state.
216
+ * Counter resets to 0 on every process restart.
217
+ * Async because key derivation is async in @noble/ed25519.
218
+ */
219
+ static async create(opts: StubHostOptions = {}): Promise<StubHost> {
220
+ const privateKey = opts.privateKey ?? utils.randomPrivateKey();
221
+ if (privateKey.length !== 32) {
222
+ throw new RangeError("StubHost: privateKey must be 32 bytes");
223
+ }
224
+ const publicKey = await getPublicKeyAsync(privateKey);
225
+
226
+ const resolved: Required<StubHostOptions> = {
227
+ privateKey,
228
+ measurement: opts.measurement ?? "stub:measurement:not-a-real-tee",
229
+ initialCounter: opts.initialCounter ?? 0n,
230
+ enableTime: opts.enableTime ?? true,
231
+ enableCounter: opts.enableCounter ?? true,
232
+ };
233
+
234
+ return new StubHost(privateKey, publicKey, resolved);
235
+ }
236
+
237
+ /**
238
+ * Create a StubHost that persists identity and counter to a JSON file.
239
+ *
240
+ * On first call: generates a fresh keypair and writes it to `statePath`.
241
+ * On subsequent calls: loads the existing keypair and counter and resumes.
242
+ *
243
+ * This lets a local demo service maintain a stable identity and
244
+ * monotonically increasing counter across restarts.
245
+ *
246
+ * The state file is plaintext JSON — not for production use.
247
+ */
248
+ static async createPersistent(opts: PersistentStubHostOptions): Promise<StubHost> {
249
+ const { statePath, measurement, enableTime, enableCounter } = opts;
250
+
251
+ let privateKey: Uint8Array;
252
+ let initialCounter: bigint;
253
+
254
+ // Try to load existing state
255
+ let existingState: PersistedState | undefined;
256
+ try {
257
+ existingState = JSON.parse(readFileSync(statePath, "utf8")) as PersistedState;
258
+ } catch {
259
+ // File doesn't exist or is corrupt — start fresh
260
+ }
261
+
262
+ if (existingState !== undefined) {
263
+ privateKey = new Uint8Array(Buffer.from(existingState.privateKeyB64, "base64"));
264
+ initialCounter = BigInt(existingState.counter);
265
+ } else {
266
+ // First run — generate a fresh key
267
+ privateKey = utils.randomPrivateKey();
268
+ initialCounter = 0n;
269
+ // Ensure the directory exists
270
+ mkdirSync(dirname(statePath), { recursive: true });
271
+ const initialState: PersistedState = {
272
+ privateKeyB64: Buffer.from(privateKey).toString("base64"),
273
+ counter: String(initialCounter),
274
+ };
275
+ writeFileSync(statePath, JSON.stringify(initialState, null, 2), "utf8");
276
+ }
277
+
278
+ if (privateKey.length !== 32) {
279
+ throw new RangeError("StubHost.createPersistent: loaded key is not 32 bytes");
280
+ }
281
+
282
+ const publicKey = await getPublicKeyAsync(privateKey);
283
+
284
+ const resolved: Required<StubHostOptions> = {
285
+ privateKey,
286
+ measurement: measurement ?? "stub:measurement:not-a-real-tee",
287
+ initialCounter,
288
+ enableTime: enableTime ?? true,
289
+ enableCounter: enableCounter ?? true,
290
+ };
291
+
292
+ return new StubHost(privateKey, publicKey, resolved, statePath);
293
+ }
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // Accessors
297
+ // ---------------------------------------------------------------------------
298
+
299
+ /**
300
+ * Return the HostCapabilities object to pass to Constructor.initialize().
301
+ */
302
+ get host(): HostCapabilities {
303
+ return this.#host;
304
+ }
305
+
306
+ /** Return the raw private key bytes (test use only). */
307
+ get privateKeyBytes(): Uint8Array {
308
+ return this.#privateKey;
309
+ }
310
+
311
+ /** Return the raw public key bytes. */
312
+ get publicKeyBytes(): Uint8Array {
313
+ return this.#publicKey;
314
+ }
315
+
316
+ /** Return the current counter value without advancing it. */
317
+ get currentCounter(): bigint {
318
+ return this.#counter;
319
+ }
320
+ }