@zeke-02/tinfoil 0.0.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.
Files changed (65) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +169 -0
  3. package/dist/__tests__/test-utils.d.ts +1 -0
  4. package/dist/__tests__/test-utils.js +44 -0
  5. package/dist/ai-sdk-provider.d.ts +7 -0
  6. package/dist/ai-sdk-provider.js +23 -0
  7. package/dist/config.d.ts +17 -0
  8. package/dist/config.js +20 -0
  9. package/dist/encrypted-body-fetch.d.ts +8 -0
  10. package/dist/encrypted-body-fetch.js +93 -0
  11. package/dist/env.d.ts +5 -0
  12. package/dist/env.js +20 -0
  13. package/dist/esm/__tests__/test-utils.d.ts +1 -0
  14. package/dist/esm/__tests__/test-utils.js +38 -0
  15. package/dist/esm/ai-sdk-provider.d.ts +7 -0
  16. package/dist/esm/ai-sdk-provider.js +20 -0
  17. package/dist/esm/config.d.ts +17 -0
  18. package/dist/esm/config.js +17 -0
  19. package/dist/esm/encrypted-body-fetch.d.ts +8 -0
  20. package/dist/esm/encrypted-body-fetch.js +86 -0
  21. package/dist/esm/env.d.ts +5 -0
  22. package/dist/esm/env.js +17 -0
  23. package/dist/esm/fetch-adapter.d.ts +21 -0
  24. package/dist/esm/fetch-adapter.js +23 -0
  25. package/dist/esm/index.browser.d.ts +7 -0
  26. package/dist/esm/index.browser.js +8 -0
  27. package/dist/esm/index.d.ts +8 -0
  28. package/dist/esm/index.js +12 -0
  29. package/dist/esm/pinned-tls-fetch.d.ts +1 -0
  30. package/dist/esm/pinned-tls-fetch.js +110 -0
  31. package/dist/esm/secure-client.d.ts +20 -0
  32. package/dist/esm/secure-client.js +123 -0
  33. package/dist/esm/secure-fetch.browser.d.ts +1 -0
  34. package/dist/esm/secure-fetch.browser.js +10 -0
  35. package/dist/esm/secure-fetch.d.ts +1 -0
  36. package/dist/esm/secure-fetch.js +22 -0
  37. package/dist/esm/tinfoilai.d.ts +54 -0
  38. package/dist/esm/tinfoilai.js +134 -0
  39. package/dist/esm/unverified-client.d.ts +18 -0
  40. package/dist/esm/unverified-client.js +33 -0
  41. package/dist/esm/verifier.d.ts +141 -0
  42. package/dist/esm/verifier.js +741 -0
  43. package/dist/esm/wasm-exec.js +668 -0
  44. package/dist/fetch-adapter.d.ts +21 -0
  45. package/dist/fetch-adapter.js +27 -0
  46. package/dist/index.browser.d.ts +7 -0
  47. package/dist/index.browser.js +29 -0
  48. package/dist/index.d.ts +8 -0
  49. package/dist/index.js +49 -0
  50. package/dist/pinned-tls-fetch.d.ts +1 -0
  51. package/dist/pinned-tls-fetch.js +116 -0
  52. package/dist/secure-client.d.ts +20 -0
  53. package/dist/secure-client.js +127 -0
  54. package/dist/secure-fetch.browser.d.ts +1 -0
  55. package/dist/secure-fetch.browser.js +13 -0
  56. package/dist/secure-fetch.d.ts +1 -0
  57. package/dist/secure-fetch.js +25 -0
  58. package/dist/tinfoilai.d.ts +54 -0
  59. package/dist/tinfoilai.js +141 -0
  60. package/dist/unverified-client.d.ts +18 -0
  61. package/dist/unverified-client.js +37 -0
  62. package/dist/verifier.d.ts +141 -0
  63. package/dist/verifier.js +781 -0
  64. package/dist/wasm-exec.js +668 -0
  65. package/package.json +97 -0
@@ -0,0 +1,781 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Verifier = void 0;
37
+ exports.compareMeasurementsDetailed = compareMeasurementsDetailed;
38
+ exports.compareMeasurements = compareMeasurements;
39
+ exports.suppressWasmLogs = suppressWasmLogs;
40
+ /**
41
+ * VERIFIER COMPONENT OVERVIEW
42
+ * ==========================
43
+ *
44
+ * This implementation performs three security checks entirely on the client using
45
+ * a Go WebAssembly module, and exposes a small TypeScript API around it.
46
+ *
47
+ * 1) REMOTE ATTESTATION (Enclave Verification)
48
+ * - Invokes Go WASM `verifyEnclave(host)` against the target enclave hostname
49
+ * - Verifies vendor certificate chains inside WASM (AMD SEV-SNP / Intel TDX)
50
+ * - Returns the enclave's runtime measurement and at least one public key (TLS fingerprint and/or HPKE)
51
+ * - Falls back to TLS-only verification if only TLS key is available (Node.js only)
52
+ *
53
+ * 2) CODE INTEGRITY (Release Verification)
54
+ * - Fetches the latest release notes via the Tinfoil GitHub proxy and extracts a digest
55
+ * (endpoint: https://api-github-proxy.tinfoil.sh)
56
+ * - Invokes Go WASM `verifyCode(configRepo, digest)` to obtain the expected code measurement
57
+ * - The Go implementation verifies provenance using Sigstore/Rekor for GitHub Actions builds
58
+ *
59
+ * 3) CODE CONSISTENCY (Measurement Comparison)
60
+ * - Compares the runtime measurement with the expected code measurement using
61
+ * platform-aware rules implemented in `compareMeasurements()`
62
+ *
63
+ * RUNTIME AND DELIVERY
64
+ * - All verification executes locally via WebAssembly (Go → WASM)
65
+ * - WASM loader: `wasm-exec.js`
66
+ * - WASM module URL: https://tinfoilsh.github.io/verifier-js/tinfoil-verifier.wasm
67
+ * - Works in Node 20+ and modern browsers with lightweight polyfills for
68
+ * `performance`, `TextEncoder`/`TextDecoder`, and `crypto.getRandomValues`
69
+ * - Go stdout/stderr is suppressed by default; toggle via `suppressWasmLogs()`
70
+ * - Module auto-initializes the WASM runtime on import
71
+ *
72
+ * PROXIES AND TRUST
73
+ * - GitHub proxy is used only to avoid rate limits; the WASM logic independently
74
+ * validates release provenance via Sigstore transparency logs
75
+ * - AMD KDS access may be proxied within the WASM for availability; AMD roots are
76
+ * embedded and the full chain is verified in Go to prevent forgery
77
+ *
78
+ * SUPPORTED PLATFORMS AND PREDICATES
79
+ * - Predicate types supported by this client: SNP/TDX multi-platform v1,
80
+ * TDX guest v1, SEV-SNP guest v1
81
+ * - See `compareMeasurements()` for exact register mapping rules
82
+ *
83
+ * PUBLIC API (this module)
84
+ * - `new Verifier({ serverURL?, configRepo? })`
85
+ * - `verify()` → full end-to-end verification and attestation response
86
+ * - `verifyEnclave(host?)` → runtime attestation only
87
+ * - `verifyCode(configRepo, digest)` → expected measurement for a specific release
88
+ * - `compareMeasurements(code, runtime)` → predicate-based comparison
89
+ * - `fetchLatestDigest(configRepo?)` → release digest via proxy
90
+ * - `suppressWasmLogs(suppress?)` → control WASM log output
91
+ */
92
+ const config_1 = require("./config");
93
+ const fetch_adapter_1 = require("./fetch-adapter");
94
+ let cachedTextEncoder = null;
95
+ function getTextEncoder() {
96
+ if (!cachedTextEncoder) {
97
+ if (typeof globalThis.TextEncoder !== "function") {
98
+ throw new Error("TextEncoder is not available in this environment");
99
+ }
100
+ cachedTextEncoder = globalThis.TextEncoder;
101
+ }
102
+ return cachedTextEncoder;
103
+ }
104
+ let cachedTextDecoder = null;
105
+ function getTextDecoder() {
106
+ if (!cachedTextDecoder) {
107
+ if (typeof globalThis.TextDecoder !== "function") {
108
+ throw new Error("TextDecoder is not available in this environment");
109
+ }
110
+ cachedTextDecoder = globalThis.TextDecoder;
111
+ }
112
+ return cachedTextDecoder;
113
+ }
114
+ const nodeRequire = createNodeRequire();
115
+ let wasmExecLoader = null;
116
+ // Platform type constants
117
+ // See https://github.com/tinfoilsh/verifier/
118
+ const PLATFORM_TYPES = {
119
+ SNP_TDX_MULTI_PLATFORM_V1: "https://tinfoil.sh/predicate/snp-tdx-multiplatform/v1",
120
+ TDX_GUEST_V1: "https://tinfoil.sh/predicate/tdx-guest/v1",
121
+ TDX_GUEST_V2: "https://tinfoil.sh/predicate/tdx-guest/v2",
122
+ SEV_GUEST_V1: "https://tinfoil.sh/predicate/sev-snp-guest/v1",
123
+ SEV_GUEST_V2: "https://tinfoil.sh/predicate/sev-snp-guest/v2",
124
+ HARDWARE_MEASUREMENTS_V1: "https://tinfoil.sh/predicate/hardware-measurements/v1",
125
+ };
126
+ const MEASUREMENT_ERROR_MESSAGES = {
127
+ FORMAT_MISMATCH: "attestation format mismatch",
128
+ MEASUREMENT_MISMATCH: "measurement mismatch",
129
+ RTMR1_MISMATCH: "RTMR1 mismatch",
130
+ RTMR2_MISMATCH: "RTMR2 mismatch",
131
+ FEW_REGISTERS: "fewer registers than expected",
132
+ MULTI_PLATFORM_MISMATCH: "multi-platform measurement mismatch",
133
+ MULTI_PLATFORM_SEV_SNP_MISMATCH: "multi-platform SEV-SNP measurement mismatch",
134
+ };
135
+ function registersEqual(a, b) {
136
+ if (a.length !== b.length) {
137
+ return false;
138
+ }
139
+ return a.every((value, index) => value === b[index]);
140
+ }
141
+ function compareMeasurementsError(codeMeasurement, runtimeMeasurement) {
142
+ if (codeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1 &&
143
+ runtimeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1) {
144
+ if (!registersEqual(codeMeasurement.registers, runtimeMeasurement.registers)) {
145
+ return new Error(MEASUREMENT_ERROR_MESSAGES.MULTI_PLATFORM_MISMATCH);
146
+ }
147
+ return null;
148
+ }
149
+ if (runtimeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1) {
150
+ return compareMeasurementsError(runtimeMeasurement, codeMeasurement);
151
+ }
152
+ if (codeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1) {
153
+ switch (runtimeMeasurement.type) {
154
+ case PLATFORM_TYPES.TDX_GUEST_V1: {
155
+ if (codeMeasurement.registers.length < 3 ||
156
+ runtimeMeasurement.registers.length < 4) {
157
+ return new Error(MEASUREMENT_ERROR_MESSAGES.FEW_REGISTERS);
158
+ }
159
+ const expectedRtmr1 = codeMeasurement.registers[1];
160
+ const expectedRtmr2 = codeMeasurement.registers[2];
161
+ const actualRtmr1 = runtimeMeasurement.registers[2];
162
+ const actualRtmr2 = runtimeMeasurement.registers[3];
163
+ if (expectedRtmr1 !== actualRtmr1) {
164
+ return new Error(MEASUREMENT_ERROR_MESSAGES.RTMR1_MISMATCH);
165
+ }
166
+ if (expectedRtmr2 !== actualRtmr2) {
167
+ return new Error(MEASUREMENT_ERROR_MESSAGES.RTMR2_MISMATCH);
168
+ }
169
+ return null;
170
+ }
171
+ case PLATFORM_TYPES.SEV_GUEST_V1:
172
+ case PLATFORM_TYPES.SEV_GUEST_V2: {
173
+ if (codeMeasurement.registers.length < 1 ||
174
+ runtimeMeasurement.registers.length < 1) {
175
+ return new Error(MEASUREMENT_ERROR_MESSAGES.FEW_REGISTERS);
176
+ }
177
+ const expectedSevSnp = codeMeasurement.registers[0];
178
+ const actualSevSnp = runtimeMeasurement.registers[0];
179
+ if (expectedSevSnp !== actualSevSnp) {
180
+ return new Error(MEASUREMENT_ERROR_MESSAGES.MULTI_PLATFORM_SEV_SNP_MISMATCH);
181
+ }
182
+ return null;
183
+ }
184
+ default:
185
+ return new Error(`unsupported enclave platform for multi-platform code measurements: ${runtimeMeasurement.type}`);
186
+ }
187
+ }
188
+ if (codeMeasurement.type !== runtimeMeasurement.type) {
189
+ return new Error(MEASUREMENT_ERROR_MESSAGES.FORMAT_MISMATCH);
190
+ }
191
+ if (!registersEqual(codeMeasurement.registers, runtimeMeasurement.registers)) {
192
+ return new Error(MEASUREMENT_ERROR_MESSAGES.MEASUREMENT_MISMATCH);
193
+ }
194
+ return null;
195
+ }
196
+ function compareMeasurementsDetailed(codeMeasurement, runtimeMeasurement) {
197
+ const error = compareMeasurementsError(codeMeasurement, runtimeMeasurement);
198
+ if (error) {
199
+ return { match: false, error };
200
+ }
201
+ return { match: true };
202
+ }
203
+ /**
204
+ * Compare two measurements according to platform-specific rules
205
+ * This is predicate function for comparing attestation measurements
206
+ * taken from https://github.com/tinfoilsh/verifier/blob/main/attestation/attestation.go
207
+ *
208
+ * @param codeMeasurement - Expected measurement from code attestation
209
+ * @param runtimeMeasurement - Actual measurement from runtime attestation
210
+ * @returns true if measurements match according to platform rules
211
+ */
212
+ function compareMeasurements(codeMeasurement, runtimeMeasurement) {
213
+ return compareMeasurementsError(codeMeasurement, runtimeMeasurement) === null;
214
+ }
215
+ /**
216
+ * Verifier performs attestation verification for Tinfoil enclaves
217
+ *
218
+ * The verifier loads a WebAssembly module that:
219
+ * 1. Fetches the latest code release digest from GitHub
220
+ * 2. Performs runtime attestation against the enclave
221
+ * 3. Performs code attestation using the digest
222
+ * 4. Compares measurements using platform-specific logic
223
+ */
224
+ class Verifier {
225
+ constructor(options) {
226
+ const serverURL = options?.serverURL ?? config_1.TINFOIL_CONFIG.INFERENCE_BASE_URL;
227
+ this.serverURL = new URL(serverURL).hostname;
228
+ this.configRepo =
229
+ options?.configRepo ?? config_1.TINFOIL_CONFIG.INFERENCE_PROXY_REPO;
230
+ }
231
+ /**
232
+ * Execute a function with a fresh WASM instance that auto-cleans up
233
+ * This ensures Go runtime doesn't keep the process alive
234
+ */
235
+ static async executeWithWasm(fn) {
236
+ await initializeWasmGlobals();
237
+ const goInstance = new globalThis.Go();
238
+ // Load WASM module
239
+ const fetchFn = (0, fetch_adapter_1.getFetch)();
240
+ const wasmResponse = await fetchFn(Verifier.defaultWasmUrl);
241
+ if (!wasmResponse.ok) {
242
+ throw new Error(`Failed to fetch WASM: ${wasmResponse.status} ${wasmResponse.statusText}`);
243
+ }
244
+ const wasmBuffer = await wasmResponse.arrayBuffer();
245
+ const result = await WebAssembly.instantiate(wasmBuffer, goInstance.importObject);
246
+ // Start the Go instance in the background
247
+ // We don't await this - it runs continuously
248
+ goInstance.run(result.instance);
249
+ // Wait for WASM functions to be available
250
+ await new Promise((resolve) => setTimeout(resolve, 100));
251
+ if (typeof globalThis.verifyCode === "undefined" ||
252
+ typeof globalThis.verifyEnclave === "undefined") {
253
+ await new Promise((resolve) => setTimeout(resolve, 500));
254
+ }
255
+ // Apply log suppression if requested
256
+ if (Verifier.wasmLogsSuppressed && globalThis.fs?.writeSync) {
257
+ const fsObj = globalThis.fs;
258
+ if (!Verifier.originalFsWriteSync) {
259
+ Verifier.originalFsWriteSync = fsObj.writeSync.bind(fsObj);
260
+ }
261
+ fsObj.writeSync = function (_fd, buf) {
262
+ return buf.length;
263
+ };
264
+ }
265
+ try {
266
+ // Execute the user's function
267
+ const result = await fn();
268
+ return result;
269
+ }
270
+ finally {
271
+ // Clean up the Go instance
272
+ if (goInstance._scheduledTimeouts instanceof Map) {
273
+ for (const timeoutId of goInstance._scheduledTimeouts.values()) {
274
+ clearTimeout(timeoutId);
275
+ }
276
+ goInstance._scheduledTimeouts.clear();
277
+ }
278
+ if (typeof goInstance.exit === "function") {
279
+ try {
280
+ goInstance.exit(0);
281
+ }
282
+ catch (e) {
283
+ // Ignore exit errors
284
+ }
285
+ }
286
+ }
287
+ }
288
+ /**
289
+ * Fetch the latest release digest from GitHub
290
+ * @param configRepo - Repository name (e.g., "tinfoilsh/confidential-inference-proxy")
291
+ * @returns The digest hash
292
+ */
293
+ async fetchLatestDigest(configRepo) {
294
+ // GitHub Proxy Note:
295
+ // We use api-github-proxy.tinfoil.sh instead of the direct GitHub API to avoid
296
+ // rate limiting that could degrade UX. The proxy caches responses while the
297
+ // integrity of the data is independently verified in `verifyCode` via
298
+ // Sigstore transparency logs (Rekor). Using the proxy therefore does not
299
+ // weaken security.
300
+ const targetRepo = configRepo || this.configRepo;
301
+ const fetchFn = (0, fetch_adapter_1.getFetch)();
302
+ const releaseResponse = await fetchFn(`https://api-github-proxy.tinfoil.sh/repos/${targetRepo}/releases/latest`, {
303
+ headers: {
304
+ Accept: "application/vnd.github.v3+json",
305
+ "User-Agent": "tinfoil-node-client",
306
+ },
307
+ });
308
+ if (!releaseResponse.ok) {
309
+ throw new Error(`GitHub API request failed: ${releaseResponse.status} ${releaseResponse.statusText}`);
310
+ }
311
+ const releaseData = (await releaseResponse.json());
312
+ // Extract digest from release notes
313
+ const digestRegex = /Digest: `([a-f0-9]{64})`/;
314
+ const digestMatch = releaseData.body?.match(digestRegex);
315
+ if (!digestMatch) {
316
+ throw new Error("Could not find digest in release notes");
317
+ }
318
+ const digest = digestMatch[1];
319
+ return digest;
320
+ }
321
+ /**
322
+ * Perform runtime attestation on the enclave
323
+ * @param enclaveHost - The enclave hostname
324
+ * @returns Attestation response with measurement and keys
325
+ */
326
+ async verifyEnclave(enclaveHost) {
327
+ // Expose errors via explicit Promise rejection and add a timeout
328
+ return new Promise(async (resolve, reject) => {
329
+ try {
330
+ const targetHost = enclaveHost || this.serverURL;
331
+ if (typeof globalThis.verifyEnclave !== "function") {
332
+ reject(new Error("WASM verifyEnclave function not available"));
333
+ return;
334
+ }
335
+ let attestationResponse;
336
+ let timeoutHandle;
337
+ try {
338
+ const timeoutPromise = new Promise((_, timeoutReject) => {
339
+ timeoutHandle = setTimeout(() => timeoutReject(new Error("WASM verifyEnclave timed out after 10 seconds")), 10000);
340
+ });
341
+ attestationResponse = await Promise.race([
342
+ globalThis.verifyEnclave(targetHost),
343
+ timeoutPromise,
344
+ ]);
345
+ // Clear timeout on success
346
+ if (timeoutHandle !== undefined) {
347
+ clearTimeout(timeoutHandle);
348
+ }
349
+ }
350
+ catch (error) {
351
+ // Clear timeout on error
352
+ if (timeoutHandle !== undefined) {
353
+ clearTimeout(timeoutHandle);
354
+ }
355
+ reject(new Error(`WASM verifyEnclave failed: ${error}`));
356
+ return;
357
+ }
358
+ // Validate required fields - fail fast with explicit rejection
359
+ // At least one key must be present (TLS or HPKE)
360
+ if (!attestationResponse?.tls_public_key &&
361
+ !attestationResponse?.hpke_public_key) {
362
+ reject(new Error("Missing both tls_public_key and hpke_public_key in attestation response"));
363
+ return;
364
+ }
365
+ // Parse runtime measurement
366
+ let parsedRuntimeMeasurement;
367
+ try {
368
+ if (attestationResponse.measurement &&
369
+ typeof attestationResponse.measurement === "string") {
370
+ parsedRuntimeMeasurement = JSON.parse(attestationResponse.measurement);
371
+ }
372
+ else if (attestationResponse.measurement &&
373
+ typeof attestationResponse.measurement === "object") {
374
+ parsedRuntimeMeasurement = attestationResponse.measurement;
375
+ }
376
+ else {
377
+ reject(new Error("Invalid runtime measurement format"));
378
+ return;
379
+ }
380
+ }
381
+ catch (parseError) {
382
+ reject(new Error(`Failed to parse runtime measurement: ${parseError}`));
383
+ return;
384
+ }
385
+ const result = {
386
+ measurement: parsedRuntimeMeasurement,
387
+ };
388
+ // Include keys if available
389
+ if (attestationResponse.tls_public_key) {
390
+ result.tlsPublicKeyFingerprint = attestationResponse.tls_public_key;
391
+ }
392
+ if (attestationResponse.hpke_public_key) {
393
+ result.hpkePublicKey = attestationResponse.hpke_public_key;
394
+ }
395
+ resolve(result);
396
+ }
397
+ catch (outerError) {
398
+ reject(outerError);
399
+ }
400
+ });
401
+ }
402
+ /**
403
+ * Perform code attestation
404
+ * @param configRepo - Repository name
405
+ * @param digest - Code digest hash
406
+ * @returns Code measurement
407
+ */
408
+ async verifyCode(configRepo, digest) {
409
+ if (typeof globalThis.verifyCode !== "function") {
410
+ throw new Error("WASM verifyCode function not available");
411
+ }
412
+ const rawMeasurement = await globalThis.verifyCode(configRepo, digest);
413
+ const normalizedMeasurement = typeof rawMeasurement === "string"
414
+ ? (() => {
415
+ try {
416
+ return JSON.parse(rawMeasurement);
417
+ }
418
+ catch (error) {
419
+ throw new Error(`Invalid code measurement format: ${error.message}`);
420
+ }
421
+ })()
422
+ : rawMeasurement;
423
+ if (!normalizedMeasurement || typeof normalizedMeasurement !== "object") {
424
+ throw new Error("Invalid code measurement format");
425
+ }
426
+ const measurementObject = normalizedMeasurement;
427
+ if (typeof measurementObject.type !== "string" ||
428
+ !Array.isArray(measurementObject.registers)) {
429
+ throw new Error("Invalid code measurement format");
430
+ }
431
+ const parsedCodeMeasurement = {
432
+ type: measurementObject.type,
433
+ registers: measurementObject.registers.map((value) => String(value)),
434
+ };
435
+ return { measurement: parsedCodeMeasurement };
436
+ }
437
+ /**
438
+ * Perform attestation verification
439
+ *
440
+ * This method:
441
+ * 1. Fetches the latest code digest from GitHub releases
442
+ * 2. Calls verifyCode to get the expected measurement for the code
443
+ * 3. Calls verifyEnclave to get the actual runtime measurement
444
+ * 4. Compares measurements using platform-specific logic (see `compareMeasurements()`)
445
+ * 5. Returns the attestation response if verification succeeds
446
+ *
447
+ * The WASM runtime is automatically initialized and cleaned up within this method.
448
+ *
449
+ * @throws Error if measurements don't match or verification fails
450
+ */
451
+ async verify() {
452
+ return Verifier.executeWithWasm(async () => {
453
+ return this.verifyInternal();
454
+ });
455
+ }
456
+ /**
457
+ * Internal verification logic that runs within WASM context
458
+ */
459
+ async verifyInternal() {
460
+ const steps = {
461
+ fetchDigest: { status: "pending" },
462
+ verifyCode: { status: "pending" },
463
+ verifyEnclave: { status: "pending" },
464
+ compareMeasurements: { status: "pending" },
465
+ };
466
+ let releaseDigest;
467
+ let codeMeasurement;
468
+ let attestation;
469
+ try {
470
+ releaseDigest = await this.fetchLatestDigest(this.configRepo);
471
+ steps.fetchDigest = { status: "success" };
472
+ }
473
+ catch (error) {
474
+ steps.fetchDigest = { status: "failed", error: error.message };
475
+ this.lastVerificationDocument = {
476
+ configRepo: this.configRepo,
477
+ enclaveHost: this.serverURL,
478
+ releaseDigest: "",
479
+ codeMeasurement: { type: "", registers: [] },
480
+ enclaveMeasurement: { measurement: { type: "", registers: [] } },
481
+ securityVerified: false,
482
+ steps,
483
+ };
484
+ throw error;
485
+ }
486
+ try {
487
+ const results = await Promise.all([
488
+ this.verifyCode(this.configRepo, releaseDigest).then((result) => {
489
+ steps.verifyCode = { status: "success" };
490
+ return result;
491
+ }, (error) => {
492
+ steps.verifyCode = {
493
+ status: "failed",
494
+ error: error.message,
495
+ };
496
+ throw error;
497
+ }),
498
+ this.verifyEnclave(this.serverURL).then((result) => {
499
+ steps.verifyEnclave = { status: "success" };
500
+ return result;
501
+ }, (error) => {
502
+ steps.verifyEnclave = {
503
+ status: "failed",
504
+ error: error.message,
505
+ };
506
+ throw error;
507
+ }),
508
+ ]);
509
+ codeMeasurement = results[0].measurement;
510
+ attestation = results[1];
511
+ }
512
+ catch (error) {
513
+ this.lastVerificationDocument = {
514
+ configRepo: this.configRepo,
515
+ enclaveHost: this.serverURL,
516
+ releaseDigest: releaseDigest,
517
+ codeMeasurement: codeMeasurement,
518
+ enclaveMeasurement: attestation,
519
+ securityVerified: false,
520
+ steps,
521
+ };
522
+ throw error;
523
+ }
524
+ const measurementsMatchError = compareMeasurementsError(codeMeasurement, attestation.measurement);
525
+ if (measurementsMatchError) {
526
+ steps.compareMeasurements = {
527
+ status: "failed",
528
+ error: measurementsMatchError.message,
529
+ };
530
+ this.lastVerificationDocument = {
531
+ configRepo: this.configRepo,
532
+ enclaveHost: this.serverURL,
533
+ releaseDigest: releaseDigest,
534
+ codeMeasurement,
535
+ enclaveMeasurement: attestation,
536
+ securityVerified: false,
537
+ steps,
538
+ };
539
+ throw new Error(`Verification failed: measurements did not match.\nCode measurement (${codeMeasurement.type}: ${codeMeasurement.registers})\n` +
540
+ `Runtime measurement (${attestation.measurement.type}: ${attestation.measurement.registers}:)\n ${measurementsMatchError.message}`);
541
+ }
542
+ steps.compareMeasurements = { status: "success" };
543
+ this.lastVerificationDocument = {
544
+ configRepo: this.configRepo,
545
+ enclaveHost: this.serverURL,
546
+ releaseDigest: releaseDigest,
547
+ codeMeasurement,
548
+ enclaveMeasurement: attestation,
549
+ securityVerified: true,
550
+ steps,
551
+ };
552
+ return attestation;
553
+ }
554
+ /**
555
+ * Returns the full verification document from the last successful verify() call
556
+ */
557
+ getVerificationDocument() {
558
+ return this.lastVerificationDocument;
559
+ }
560
+ }
561
+ exports.Verifier = Verifier;
562
+ Verifier.goInstance = null;
563
+ Verifier.initializationPromise = null;
564
+ Verifier.defaultWasmUrl = "https://tinfoilsh.github.io/verifier-js/tinfoil-verifier.wasm";
565
+ Verifier.originalFsWriteSync = null;
566
+ Verifier.wasmLogsSuppressed = true;
567
+ Verifier.globalsInitialized = false;
568
+ // Start initialization as soon as the module loads
569
+ function shouldAutoInitializeWasm() {
570
+ const globalAny = globalThis;
571
+ const env = globalAny.process?.env;
572
+ const autoInitFlag = env?.TINFOIL_SKIP_WASM_AUTO_INIT ?? env?.TINFOIL_DISABLE_WASM_AUTO_INIT;
573
+ if (typeof autoInitFlag === "string") {
574
+ const normalized = autoInitFlag.toLowerCase();
575
+ if (normalized === "1" || normalized === "true") {
576
+ return false;
577
+ }
578
+ }
579
+ if (env?.NODE_ENV === "test") {
580
+ return false;
581
+ }
582
+ const isNode = typeof globalAny.process?.versions?.node === "string";
583
+ if (isNode) {
584
+ return false;
585
+ }
586
+ const hasBrowserGlobals = typeof globalAny.window === "object" &&
587
+ typeof globalAny.document === "object";
588
+ return hasBrowserGlobals;
589
+ }
590
+ /**
591
+ * Control WASM log output
592
+ *
593
+ * The Go WASM runtime outputs logs through a polyfilled fs.writeSync.
594
+ * This function allows suppressing those logs without affecting other console output.
595
+ *
596
+ * @param suppress - Whether to suppress WASM logs (default: true)
597
+ */
598
+ function suppressWasmLogs(suppress = true) {
599
+ globalThis.__tinfoilSuppressWasmLogs = suppress;
600
+ Verifier.wasmLogsSuppressed = suppress;
601
+ const fsObj = globalThis.fs;
602
+ if (!fsObj || typeof fsObj.writeSync !== "function")
603
+ return;
604
+ if (suppress) {
605
+ if (!Verifier.originalFsWriteSync) {
606
+ Verifier.originalFsWriteSync = fsObj.writeSync.bind(fsObj);
607
+ }
608
+ fsObj.writeSync = function (_fd, buf) {
609
+ return buf.length;
610
+ };
611
+ }
612
+ else if (Verifier.originalFsWriteSync) {
613
+ fsObj.writeSync = Verifier.originalFsWriteSync;
614
+ }
615
+ }
616
+ /**
617
+ * Initialize globals needed for Go WASM runtime
618
+ * This function sets up browser-like globals that the Go WASM runtime expects
619
+ */
620
+ async function initializeWasmGlobals() {
621
+ // Only initialize once
622
+ if (Verifier.globalsInitialized) {
623
+ return;
624
+ }
625
+ const root = globalThis;
626
+ // Performance API (Go runtime expects a few methods to exist)
627
+ if (!root.performance) {
628
+ root.performance = {
629
+ now: () => Date.now(),
630
+ markResourceTiming: () => { },
631
+ mark: () => { },
632
+ measure: () => { },
633
+ clearMarks: () => { },
634
+ clearMeasures: () => { },
635
+ getEntriesByName: () => [],
636
+ getEntriesByType: () => [],
637
+ getEntries: () => [],
638
+ };
639
+ }
640
+ else {
641
+ root.performance.now = root.performance.now ?? (() => Date.now());
642
+ root.performance.markResourceTiming =
643
+ root.performance.markResourceTiming ?? (() => { });
644
+ root.performance.mark = root.performance.mark ?? (() => { });
645
+ root.performance.measure = root.performance.measure ?? (() => { });
646
+ root.performance.clearMarks = root.performance.clearMarks ?? (() => { });
647
+ root.performance.clearMeasures =
648
+ root.performance.clearMeasures ?? (() => { });
649
+ root.performance.getEntriesByName =
650
+ root.performance.getEntriesByName ?? (() => []);
651
+ root.performance.getEntriesByType =
652
+ root.performance.getEntriesByType ?? (() => []);
653
+ root.performance.getEntries = root.performance.getEntries ?? (() => []);
654
+ }
655
+ // Text encoding
656
+ if (!root.TextEncoder) {
657
+ root.TextEncoder = getTextEncoder();
658
+ }
659
+ if (!root.TextDecoder) {
660
+ root.TextDecoder = getTextDecoder();
661
+ }
662
+ // Crypto API (needed by Go WASM)
663
+ ensureCrypto(root);
664
+ // Default: suppress WASM (Go) stdout/stderr logs unless explicitly enabled by caller
665
+ if (typeof root.__tinfoilSuppressWasmLogs === "undefined") {
666
+ root.__tinfoilSuppressWasmLogs = true;
667
+ }
668
+ // Force process to stay running (prevent Go from exiting Node process)
669
+ // This is a common issue with Go WASM in Node - it calls process.exit()
670
+ if (root.process &&
671
+ typeof root.process.exit === "function" &&
672
+ !root.__tinfoilProcessExitPatched) {
673
+ root.__tinfoilProcessExitPatched = true;
674
+ const originalExit = root.process.exit.bind(root.process);
675
+ root.__tinfoilOriginalProcessExit = originalExit;
676
+ // Replace process.exit to prevent the Go WASM runtime from terminating the Node.js process.
677
+ // When wasm log suppression is enabled, suppress the informational log about the ignored exit
678
+ // so callers can silence only the WASM-related noise while keeping application logs intact.
679
+ root.process.exit = ((code) => {
680
+ if (!root.__tinfoilSuppressWasmLogs) {
681
+ console.log(`Process exit called with code ${code} - ignoring to keep runtime alive`);
682
+ }
683
+ return undefined;
684
+ });
685
+ }
686
+ await loadWasmExec();
687
+ Verifier.globalsInitialized = true;
688
+ }
689
+ function ensureCrypto(root) {
690
+ const hasWorkingGetRandomValues = root.crypto && typeof root.crypto.getRandomValues === "function"
691
+ ? root.crypto
692
+ : resolveWindowCrypto(root);
693
+ if (hasWorkingGetRandomValues) {
694
+ if (!root.crypto) {
695
+ root.crypto = hasWorkingGetRandomValues;
696
+ }
697
+ return;
698
+ }
699
+ const nodeRandomBytes = resolveNodeRandomBytes();
700
+ if (!nodeRandomBytes) {
701
+ throw new Error("crypto.getRandomValues is not available in this environment");
702
+ }
703
+ const fallbackCrypto = {
704
+ getRandomValues: (buffer) => {
705
+ const bytes = nodeRandomBytes(buffer.length);
706
+ buffer.set(bytes);
707
+ return buffer;
708
+ },
709
+ };
710
+ try {
711
+ root.crypto = fallbackCrypto;
712
+ }
713
+ catch {
714
+ Object.defineProperty(root, "crypto", {
715
+ configurable: true,
716
+ enumerable: false,
717
+ value: fallbackCrypto,
718
+ writable: false,
719
+ });
720
+ }
721
+ }
722
+ function resolveWindowCrypto(root) {
723
+ const maybeWindow = root.window ?? (typeof window !== "undefined" ? window : undefined);
724
+ if (maybeWindow?.crypto &&
725
+ typeof maybeWindow.crypto.getRandomValues === "function") {
726
+ return maybeWindow.crypto;
727
+ }
728
+ return undefined;
729
+ }
730
+ function resolveNodeRandomBytes() {
731
+ if (!nodeRequire) {
732
+ return undefined;
733
+ }
734
+ try {
735
+ const cryptoModule = nodeRequire("crypto");
736
+ const randomBytes = typeof cryptoModule?.randomBytes === "function"
737
+ ? cryptoModule.randomBytes
738
+ : undefined;
739
+ if (randomBytes) {
740
+ return (size) => {
741
+ const result = randomBytes(size);
742
+ return result instanceof Uint8Array ? result : new Uint8Array(result);
743
+ };
744
+ }
745
+ }
746
+ catch {
747
+ return undefined;
748
+ }
749
+ return undefined;
750
+ }
751
+ function loadWasmExec() {
752
+ if (!wasmExecLoader) {
753
+ wasmExecLoader = (async () => {
754
+ // Prefer a dynamic import so bundlers (Next/Webpack/Vite) include the file.
755
+ // If that fails (e.g., pure Node without bundler), fall back to require.
756
+ try {
757
+ // @ts-expect-error: Local JS helper has no TS types; ambient module declared in wasm-exec.d.ts
758
+ await Promise.resolve().then(() => __importStar(require("./wasm-exec.js")));
759
+ }
760
+ catch {
761
+ if (nodeRequire) {
762
+ nodeRequire("./wasm-exec.js");
763
+ return;
764
+ }
765
+ throw new Error("Failed to load wasm-exec.js via dynamic import, and require() is unavailable");
766
+ }
767
+ })();
768
+ wasmExecLoader.catch(() => {
769
+ wasmExecLoader = null;
770
+ });
771
+ }
772
+ return wasmExecLoader;
773
+ }
774
+ function createNodeRequire() {
775
+ try {
776
+ return typeof require === "function" ? require : undefined;
777
+ }
778
+ catch {
779
+ return undefined;
780
+ }
781
+ }