@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.
- package/LICENSE +661 -0
- package/README.md +169 -0
- package/dist/__tests__/test-utils.d.ts +1 -0
- package/dist/__tests__/test-utils.js +44 -0
- package/dist/ai-sdk-provider.d.ts +7 -0
- package/dist/ai-sdk-provider.js +23 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +20 -0
- package/dist/encrypted-body-fetch.d.ts +8 -0
- package/dist/encrypted-body-fetch.js +93 -0
- package/dist/env.d.ts +5 -0
- package/dist/env.js +20 -0
- package/dist/esm/__tests__/test-utils.d.ts +1 -0
- package/dist/esm/__tests__/test-utils.js +38 -0
- package/dist/esm/ai-sdk-provider.d.ts +7 -0
- package/dist/esm/ai-sdk-provider.js +20 -0
- package/dist/esm/config.d.ts +17 -0
- package/dist/esm/config.js +17 -0
- package/dist/esm/encrypted-body-fetch.d.ts +8 -0
- package/dist/esm/encrypted-body-fetch.js +86 -0
- package/dist/esm/env.d.ts +5 -0
- package/dist/esm/env.js +17 -0
- package/dist/esm/fetch-adapter.d.ts +21 -0
- package/dist/esm/fetch-adapter.js +23 -0
- package/dist/esm/index.browser.d.ts +7 -0
- package/dist/esm/index.browser.js +8 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/pinned-tls-fetch.d.ts +1 -0
- package/dist/esm/pinned-tls-fetch.js +110 -0
- package/dist/esm/secure-client.d.ts +20 -0
- package/dist/esm/secure-client.js +123 -0
- package/dist/esm/secure-fetch.browser.d.ts +1 -0
- package/dist/esm/secure-fetch.browser.js +10 -0
- package/dist/esm/secure-fetch.d.ts +1 -0
- package/dist/esm/secure-fetch.js +22 -0
- package/dist/esm/tinfoilai.d.ts +54 -0
- package/dist/esm/tinfoilai.js +134 -0
- package/dist/esm/unverified-client.d.ts +18 -0
- package/dist/esm/unverified-client.js +33 -0
- package/dist/esm/verifier.d.ts +141 -0
- package/dist/esm/verifier.js +741 -0
- package/dist/esm/wasm-exec.js +668 -0
- package/dist/fetch-adapter.d.ts +21 -0
- package/dist/fetch-adapter.js +27 -0
- package/dist/index.browser.d.ts +7 -0
- package/dist/index.browser.js +29 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +49 -0
- package/dist/pinned-tls-fetch.d.ts +1 -0
- package/dist/pinned-tls-fetch.js +116 -0
- package/dist/secure-client.d.ts +20 -0
- package/dist/secure-client.js +127 -0
- package/dist/secure-fetch.browser.d.ts +1 -0
- package/dist/secure-fetch.browser.js +13 -0
- package/dist/secure-fetch.d.ts +1 -0
- package/dist/secure-fetch.js +25 -0
- package/dist/tinfoilai.d.ts +54 -0
- package/dist/tinfoilai.js +141 -0
- package/dist/unverified-client.d.ts +18 -0
- package/dist/unverified-client.js +37 -0
- package/dist/verifier.d.ts +141 -0
- package/dist/verifier.js +781 -0
- package/dist/wasm-exec.js +668 -0
- package/package.json +97 -0
package/dist/verifier.js
ADDED
|
@@ -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
|
+
}
|