@qntx/wallet-worker 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -8
- package/dist/browser-dy5h3c5e.js +199 -0
- package/dist/browser.mjs +56 -27
- package/dist/browser.mjs.map +1 -1
- package/dist/kernel.manifest.json +91 -0
- package/dist/kernel.node.js +7117 -8537
- package/dist/kernel.worker.js +7024 -8311
- package/dist/node-gxb54jcq.js +23 -0
- package/dist/node-nhrchcf5.js +3979 -0
- package/dist/node.d.mts +3 -1
- package/dist/node.mjs +96 -24
- package/dist/node.mjs.map +1 -1
- package/dist/signer-yectsdty.js +10892 -0
- package/dist/spark.wasm +0 -0
- package/dist/worker-gxb54jcq.js +23 -0
- package/dist/worker-nhrchcf5.js +3979 -0
- package/package.json +18 -12
package/README.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Web and Node host for [`@qntx/wallet`](https://www.npmjs.com/package/@qntx/wallet): spawns the
|
|
4
4
|
wallet kernel in a Worker (`dist/kernel.worker.js` / `dist/kernel.node.js`, bundled), keeps the
|
|
5
|
-
encrypted vault in IndexedDB (Web) or a main-thread
|
|
5
|
+
encrypted vault in IndexedDB (Web) or a main-thread store server (Node), and runs the WebAuthn
|
|
6
6
|
PRF passkey ceremony on the main thread. Peer dependency: the exact `@qntx/wallet` release this
|
|
7
|
-
kernel was built against — the host/kernel protocol requires it.
|
|
7
|
+
kernel was built against — the host/kernel protocol requires it. `dist/kernel.manifest.json`
|
|
8
|
+
records the protocol version, vault format, every pinned kernel dependency's lockfile integrity
|
|
9
|
+
and the SHA-256 of each bundle.
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
npm install @qntx/wallet-worker @qntx/wallet
|
|
@@ -19,12 +21,12 @@ import { createWebWallet } from "@qntx/wallet-worker/browser";
|
|
|
19
21
|
|
|
20
22
|
const wallet = await createWebWallet({
|
|
21
23
|
passkey: {
|
|
22
|
-
rpId: "app.example.com",
|
|
24
|
+
rpId: "app.example.com", // permanent — see Security notes
|
|
23
25
|
rpName: "Example",
|
|
24
26
|
walletSalt: "example/wallet",
|
|
25
27
|
vaultSalt: "example/vault",
|
|
26
28
|
},
|
|
27
|
-
autoLock: { platform: "web", idleMinutes: 15 },
|
|
29
|
+
autoLock: { platform: "web", idleMinutes: 15 }, // 5 | 15 | 60; max session 12 h
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
const created = await wallet.wallets.createWithPasskey({ name: "Example Wallet" });
|
|
@@ -54,10 +56,34 @@ const wallet = await createNodeWallet({
|
|
|
54
56
|
|
|
55
57
|
## Security notes
|
|
56
58
|
|
|
57
|
-
- Secrets exist only inside the kernel Worker; lock
|
|
58
|
-
(ciphertext + public headers) persists in the store.
|
|
59
|
-
-
|
|
60
|
-
|
|
59
|
+
- Secrets exist only inside the kernel Worker; `vault.lock()` resolves immediately on the host
|
|
60
|
+
and destroys the worker — the vault image (ciphertext + public headers) persists in the store.
|
|
61
|
+
- Session limits belong to the worker, not the page: `idleMinutes` accepts 5, 15 or 60 and the
|
|
62
|
+
absolute session cap is a fixed 12 hours, measured on a monotonic clock. On the Web only user
|
|
63
|
+
presence (input events, a tab becoming visible) refreshes the idle timer — background request
|
|
64
|
+
traffic does not; on Node every successful request does.
|
|
65
|
+
- `WalletInfo.backup` is `"mnemonic"`, `"passkey"` (a synced passkey — prompt the user to export)
|
|
66
|
+
or `"none"` (a device-bound passkey — survives only on this device). `exportMnemonic` upgrades
|
|
67
|
+
the record to `"mnemonic"`.
|
|
68
|
+
- `createSoftwarePasskey(seed, { synced })` — `synced` defaults to `true` and lands in every
|
|
69
|
+
`PrfResult`; pass `{ synced: false }` to exercise the device-bound path.
|
|
70
|
+
- `passkey.rpId` and the two salts are permanent identifiers: changing them makes existing
|
|
71
|
+
passkey factors and passkey-derived wallets unrecoverable. Moving domains needs WebAuthn
|
|
72
|
+
related origins (`/.well-known/webauthn`); offer `exportMnemonic` for passkey-derived wallets.
|
|
73
|
+
- IndexedDB upgrades are never `blocked`: the worker closes its connection on `versionchange`,
|
|
74
|
+
and once the database is newer than this build's schema the vault reports `unsupported` —
|
|
75
|
+
update the app, do not reset.
|
|
76
|
+
- The Node store's RPC failures surface as `unavailable` and precondition failures as
|
|
77
|
+
`conflict` — neither hangs a kernel call.
|
|
78
|
+
- The Web host is a **hot wallet**. CSP needs `worker-src 'self'`. The host spawns the kernel
|
|
79
|
+
with `new Worker(new URL("kernel.worker.js", import.meta.url))`, which bundlers emit as a
|
|
80
|
+
worker entry with its chunks and `spark.wasm`. Consumer setup: Vite —
|
|
81
|
+
`worker: { format: "es" }` (required to keep the Spark backend lazy; the default `"iife"`
|
|
82
|
+
still runs but ships Spark inside the initial worker) and dev-server
|
|
83
|
+
`optimizeDeps.exclude: ["@qntx/wallet-worker"]`; webpack 5 — nothing.
|
|
84
|
+
- `dist/kernel.manifest.json` pins the artifacts the package was built from. Consumer bundlers
|
|
85
|
+
re-bundle the worker, so the shipped bytes differ from the manifest's; `spark.wasm` is still
|
|
86
|
+
verified by its integrity constant when the kernel loads it.
|
|
61
87
|
- `@qntx/wallet/host` is for host packages only — not application API.
|
|
62
88
|
|
|
63
89
|
## License
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SparkError,
|
|
3
|
+
SparkCryptoError
|
|
4
|
+
} from "./worker-gxb54jcq.js";
|
|
5
|
+
|
|
6
|
+
// ../../node_modules/.bun/@qntx+spark@0.5.0/node_modules/@qntx/spark/dist/crypto.mjs
|
|
7
|
+
var SPARK_ABI_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
// ../../node_modules/.bun/@qntx+spark-wasm@0.5.0+8f9f9cc94002ed5a/node_modules/@qntx/spark-wasm/dist/integrity-BP6ziDEM.mjs
|
|
10
|
+
var OUT_PARAMS_LEN = 8;
|
|
11
|
+
var INTERNAL_ERROR = 8;
|
|
12
|
+
function requireMemory(value) {
|
|
13
|
+
if (!(value instanceof WebAssembly.Memory))
|
|
14
|
+
throw new SparkError("spark.wasm missing memory export");
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function requireFn(value, name) {
|
|
18
|
+
if (typeof value !== "function")
|
|
19
|
+
throw new SparkError(`spark.wasm missing ${name} export`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function sparkWasmExports(raw) {
|
|
23
|
+
return {
|
|
24
|
+
memory: requireMemory(raw["memory"]),
|
|
25
|
+
qs_abi_version: requireFn(raw["qs_abi_version"], "qs_abi_version"),
|
|
26
|
+
qs_alloc: requireFn(raw["qs_alloc"], "qs_alloc"),
|
|
27
|
+
qs_free: requireFn(raw["qs_free"], "qs_free"),
|
|
28
|
+
qs_frost_commit: requireFn(raw["qs_frost_commit"], "qs_frost_commit"),
|
|
29
|
+
qs_frost_sign: requireFn(raw["qs_frost_sign"], "qs_frost_sign"),
|
|
30
|
+
qs_frost_aggregate: requireFn(raw["qs_frost_aggregate"], "qs_frost_aggregate"),
|
|
31
|
+
qs_ecies_encrypt: requireFn(raw["qs_ecies_encrypt"], "qs_ecies_encrypt"),
|
|
32
|
+
qs_ecies_decrypt: requireFn(raw["qs_ecies_decrypt"], "qs_ecies_decrypt"),
|
|
33
|
+
qs_split_secret_with_proofs: requireFn(raw["qs_split_secret_with_proofs"], "qs_split_secret_with_proofs")
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function copyIn(exports, bytes) {
|
|
37
|
+
const len = bytes.length;
|
|
38
|
+
if (len === 0)
|
|
39
|
+
return 0;
|
|
40
|
+
const ptr = Number(exports.qs_alloc(len)) >>> 0;
|
|
41
|
+
if (ptr === 0)
|
|
42
|
+
throw new SparkCryptoError(INTERNAL_ERROR);
|
|
43
|
+
new Uint8Array(exports.memory.buffer, ptr, len).set(bytes);
|
|
44
|
+
return ptr;
|
|
45
|
+
}
|
|
46
|
+
function freeIn(exports, ptr, len) {
|
|
47
|
+
if (ptr === 0 || len === 0)
|
|
48
|
+
return;
|
|
49
|
+
new Uint8Array(exports.memory.buffer, ptr, len).fill(0);
|
|
50
|
+
exports.qs_free(ptr, len);
|
|
51
|
+
}
|
|
52
|
+
function callOut(exports, invoke) {
|
|
53
|
+
const params = Number(exports.qs_alloc(OUT_PARAMS_LEN)) >>> 0;
|
|
54
|
+
if (params === 0)
|
|
55
|
+
throw new SparkCryptoError(INTERNAL_ERROR);
|
|
56
|
+
try {
|
|
57
|
+
const code = Number(invoke(params, params + 4));
|
|
58
|
+
if (code !== 0)
|
|
59
|
+
throw new SparkCryptoError(code);
|
|
60
|
+
const view = new DataView(exports.memory.buffer, params, OUT_PARAMS_LEN);
|
|
61
|
+
const outPtr = view.getUint32(0, true) >>> 0;
|
|
62
|
+
const outLen = view.getUint32(4, true) >>> 0;
|
|
63
|
+
if (outPtr === 0)
|
|
64
|
+
return /* @__PURE__ */ new Uint8Array(0);
|
|
65
|
+
const result = new Uint8Array(outLen);
|
|
66
|
+
result.set(new Uint8Array(exports.memory.buffer, outPtr, outLen));
|
|
67
|
+
exports.qs_free(outPtr, outLen);
|
|
68
|
+
return result;
|
|
69
|
+
} finally {
|
|
70
|
+
freeIn(exports, params, OUT_PARAMS_LEN);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function wasmFrostCommit(exports, hedge, random) {
|
|
74
|
+
const hedgePtr = copyIn(exports, hedge);
|
|
75
|
+
const randomPtr = copyIn(exports, random);
|
|
76
|
+
try {
|
|
77
|
+
return callOut(exports, (out, outLen) => exports.qs_frost_commit(hedgePtr, hedge.length, randomPtr, random.length, out, outLen));
|
|
78
|
+
} finally {
|
|
79
|
+
freeIn(exports, hedgePtr, hedge.length);
|
|
80
|
+
freeIn(exports, randomPtr, random.length);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function wasmFrostSign(exports, request) {
|
|
84
|
+
const requestPtr = copyIn(exports, request);
|
|
85
|
+
try {
|
|
86
|
+
return callOut(exports, (out, outLen) => exports.qs_frost_sign(requestPtr, request.length, out, outLen));
|
|
87
|
+
} finally {
|
|
88
|
+
freeIn(exports, requestPtr, request.length);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function wasmFrostAggregate(exports, request) {
|
|
92
|
+
const requestPtr = copyIn(exports, request);
|
|
93
|
+
try {
|
|
94
|
+
return callOut(exports, (out, outLen) => exports.qs_frost_aggregate(requestPtr, request.length, out, outLen));
|
|
95
|
+
} finally {
|
|
96
|
+
freeIn(exports, requestPtr, request.length);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function wasmEciesEncrypt(exports, publicKey, plaintext, random) {
|
|
100
|
+
const pkPtr = copyIn(exports, publicKey);
|
|
101
|
+
const msgPtr = copyIn(exports, plaintext);
|
|
102
|
+
const randomPtr = copyIn(exports, random);
|
|
103
|
+
try {
|
|
104
|
+
return callOut(exports, (out, outLen) => exports.qs_ecies_encrypt(pkPtr, publicKey.length, msgPtr, plaintext.length, randomPtr, random.length, out, outLen));
|
|
105
|
+
} finally {
|
|
106
|
+
freeIn(exports, pkPtr, publicKey.length);
|
|
107
|
+
freeIn(exports, msgPtr, plaintext.length);
|
|
108
|
+
freeIn(exports, randomPtr, random.length);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function wasmEciesDecrypt(exports, privateKey, ciphertext) {
|
|
112
|
+
const skPtr = copyIn(exports, privateKey);
|
|
113
|
+
const ctPtr = copyIn(exports, ciphertext);
|
|
114
|
+
try {
|
|
115
|
+
return callOut(exports, (out, outLen) => exports.qs_ecies_decrypt(skPtr, privateKey.length, ctPtr, ciphertext.length, out, outLen));
|
|
116
|
+
} finally {
|
|
117
|
+
freeIn(exports, skPtr, privateKey.length);
|
|
118
|
+
freeIn(exports, ctPtr, ciphertext.length);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function checkUint32(value, name) {
|
|
122
|
+
if (!Number.isInteger(value) || value < 0 || value > 4294967295)
|
|
123
|
+
throw new SparkError(`${name} must be an integer in [0, 2^32)`);
|
|
124
|
+
}
|
|
125
|
+
function wasmSplitSecretWithProofs(exports, secret, threshold, shares, random) {
|
|
126
|
+
checkUint32(threshold, "threshold");
|
|
127
|
+
checkUint32(shares, "shares");
|
|
128
|
+
const secretPtr = copyIn(exports, secret);
|
|
129
|
+
const randomPtr = copyIn(exports, random);
|
|
130
|
+
try {
|
|
131
|
+
return callOut(exports, (out, outLen) => exports.qs_split_secret_with_proofs(secretPtr, secret.length, threshold, shares, randomPtr, random.length, out, outLen));
|
|
132
|
+
} finally {
|
|
133
|
+
freeIn(exports, secretPtr, secret.length);
|
|
134
|
+
freeIn(exports, randomPtr, random.length);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function instantiateSparkWasm(bytes) {
|
|
138
|
+
const module = await WebAssembly.compile(bytes);
|
|
139
|
+
const imports = WebAssembly.Module.imports(module);
|
|
140
|
+
if (imports.length > 0)
|
|
141
|
+
throw new SparkError(`spark.wasm module must not declare imports (got ${imports.length})`);
|
|
142
|
+
const instance = await WebAssembly.instantiate(module, {});
|
|
143
|
+
const version = Number(requireFn(instance.exports["qs_abi_version"], "qs_abi_version")());
|
|
144
|
+
if (version !== SPARK_ABI_VERSION)
|
|
145
|
+
throw new SparkError(`spark.wasm ABI version ${version} does not match loader ABI version ${SPARK_ABI_VERSION}`);
|
|
146
|
+
return sparkWasmExports(instance.exports);
|
|
147
|
+
}
|
|
148
|
+
var WasmPoisonedError = class extends SparkCryptoError {
|
|
149
|
+
name = "WasmPoisonedError";
|
|
150
|
+
};
|
|
151
|
+
function runSparkPoisoned(poison, memory, fn) {
|
|
152
|
+
if (poison.error !== undefined)
|
|
153
|
+
throw poison.error;
|
|
154
|
+
try {
|
|
155
|
+
return fn();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof WebAssembly.RuntimeError) {
|
|
158
|
+
new Uint8Array(memory.buffer).fill(0);
|
|
159
|
+
poison.error = new WasmPoisonedError(8, { cause: error });
|
|
160
|
+
throw poison.error;
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function bindSparkExports(exports) {
|
|
166
|
+
const poison = {};
|
|
167
|
+
const run = (fn) => runSparkPoisoned(poison, exports.memory, fn);
|
|
168
|
+
return {
|
|
169
|
+
abiVersion: SPARK_ABI_VERSION,
|
|
170
|
+
frostCommit: (hedge, random) => run(() => wasmFrostCommit(exports, hedge, random)),
|
|
171
|
+
frostSign: (request) => run(() => wasmFrostSign(exports, request)),
|
|
172
|
+
frostAggregate: (request) => run(() => wasmFrostAggregate(exports, request)),
|
|
173
|
+
eciesEncrypt: (publicKey, plaintext, random) => run(() => wasmEciesEncrypt(exports, publicKey, plaintext, random)),
|
|
174
|
+
eciesDecrypt: (privateKey, ciphertext) => run(() => wasmEciesDecrypt(exports, privateKey, ciphertext)),
|
|
175
|
+
splitSecretWithProofs: (secret, threshold, shares, random) => run(() => wasmSplitSecretWithProofs(exports, secret, threshold, shares, random))
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
var SPARK_WASM_INTEGRITY = "sha256-PFqThpVpNZHbmIuR13MZqLAupIIhfVcd3nMtB9QMTTE=";
|
|
179
|
+
|
|
180
|
+
// ../../node_modules/.bun/@qntx+spark-wasm@0.5.0+8f9f9cc94002ed5a/node_modules/@qntx/spark-wasm/dist/browser.mjs
|
|
181
|
+
async function fetchWasmUrl(url) {
|
|
182
|
+
const res = await fetch(url);
|
|
183
|
+
if (!res.ok)
|
|
184
|
+
throw new SparkError(`spark.wasm fetch failed (HTTP ${res.status})`);
|
|
185
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
186
|
+
}
|
|
187
|
+
async function wasmBytes(opts) {
|
|
188
|
+
const source = opts?.module;
|
|
189
|
+
if (source !== undefined)
|
|
190
|
+
return source;
|
|
191
|
+
return fetchWasmUrl(new URL("spark.wasm", import.meta.url));
|
|
192
|
+
}
|
|
193
|
+
async function loadSparkWasm(opts) {
|
|
194
|
+
return bindSparkExports(await instantiateSparkWasm(await wasmBytes(opts)));
|
|
195
|
+
}
|
|
196
|
+
export {
|
|
197
|
+
SPARK_WASM_INTEGRITY,
|
|
198
|
+
loadSparkWasm
|
|
199
|
+
};
|
package/dist/browser.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WalletError } from "@qntx/wallet";
|
|
2
|
-
import { connectKernel } from "@qntx/wallet/host";
|
|
2
|
+
import { PASSKEY_PURPOSE_VAULT, PASSKEY_PURPOSE_WALLET, connectKernel } from "@qntx/wallet/host";
|
|
3
3
|
//#region src/browser/passkey.ts
|
|
4
4
|
/**
|
|
5
5
|
* The WebAuthn passkey ceremony (main thread only): `navigator.credentials` with the PRF extension.
|
|
@@ -8,8 +8,6 @@ import { connectKernel } from "@qntx/wallet/host";
|
|
|
8
8
|
* `prf.enabled`, PRF results and the user-verification flag — a credential that cannot prove UV
|
|
9
9
|
* never reaches the kernel.
|
|
10
10
|
*/
|
|
11
|
-
const PURPOSE_WALLET = 1;
|
|
12
|
-
const PURPOSE_VAULT = 2;
|
|
13
11
|
const UV_FLAG = 4;
|
|
14
12
|
function factorUnavailable(message) {
|
|
15
13
|
return new WalletError("factor-unavailable", message);
|
|
@@ -26,6 +24,13 @@ function mapCeremonyError(error) {
|
|
|
26
24
|
function userVerified(authenticatorData) {
|
|
27
25
|
return authenticatorData.byteLength > 32 && (new DataView(authenticatorData).getUint8(32) & UV_FLAG) !== 0;
|
|
28
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether the credential is backed up off-device (flags byte at offset 32): synced requires both BE
|
|
29
|
+
* (the credential is eligible for backup) and BS (the authenticator actually backs it up).
|
|
30
|
+
*/
|
|
31
|
+
function credentialSynced(authenticatorData) {
|
|
32
|
+
return authenticatorData.byteLength > 32 && (new DataView(authenticatorData).getUint8(32) & 24) === 24;
|
|
33
|
+
}
|
|
29
34
|
function prfEval(salts) {
|
|
30
35
|
return salts.second === void 0 ? { first: new Uint8Array(salts.first) } : {
|
|
31
36
|
first: new Uint8Array(salts.first),
|
|
@@ -37,14 +42,16 @@ function copyBufferSource(source) {
|
|
|
37
42
|
const view = source instanceof ArrayBuffer ? new Uint8Array(source) : new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
|
38
43
|
return new Uint8Array(view);
|
|
39
44
|
}
|
|
40
|
-
function readPrfResult(credential, results, wantSecond) {
|
|
45
|
+
function readPrfResult(credential, results, wantSecond, synced, userHandle) {
|
|
41
46
|
const first = results?.first;
|
|
42
47
|
const second = results?.second;
|
|
43
48
|
if (first === void 0 || wantSecond && second === void 0) throw factorUnavailable("the passkey produced no PRF output");
|
|
44
49
|
return {
|
|
45
50
|
credentialId: copyBufferSource(credential.rawId),
|
|
46
51
|
first: copyBufferSource(first),
|
|
47
|
-
...second === void 0 ? {} : { second: copyBufferSource(second) }
|
|
52
|
+
...second === void 0 ? {} : { second: copyBufferSource(second) },
|
|
53
|
+
...userHandle === void 0 ? {} : { userHandle: copyBufferSource(userHandle) },
|
|
54
|
+
synced
|
|
48
55
|
};
|
|
49
56
|
}
|
|
50
57
|
function checkCredential(credential) {
|
|
@@ -76,12 +83,12 @@ function createWebAuthnPasskey(rp) {
|
|
|
76
83
|
const { response } = credential;
|
|
77
84
|
if (!(response instanceof AuthenticatorAssertionResponse)) throw factorUnavailable("the authenticator returned no assertion response");
|
|
78
85
|
if (!userVerified(response.authenticatorData)) throw factorUnavailable("the passkey assertion was not user-verified");
|
|
79
|
-
return readPrfResult(credential, credential.getClientExtensionResults().prf?.results, input.salts.second !== void 0);
|
|
86
|
+
return readPrfResult(credential, credential.getClientExtensionResults().prf?.results, input.salts.second !== void 0, credentialSynced(response.authenticatorData), response.userHandle ?? void 0);
|
|
80
87
|
}
|
|
81
88
|
return {
|
|
82
89
|
create: async (input) => {
|
|
83
90
|
const userId = /* @__PURE__ */ new Uint8Array(16);
|
|
84
|
-
userId[0] = input.purpose === "wallet" ?
|
|
91
|
+
userId[0] = input.purpose === "wallet" ? PASSKEY_PURPOSE_WALLET : PASSKEY_PURPOSE_VAULT;
|
|
85
92
|
crypto.getRandomValues(userId.subarray(1));
|
|
86
93
|
let credential;
|
|
87
94
|
try {
|
|
@@ -120,8 +127,9 @@ function createWebAuthnPasskey(rp) {
|
|
|
120
127
|
credentialIds: [copyBufferSource(credential.rawId)],
|
|
121
128
|
salts: input.salts
|
|
122
129
|
});
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
const authenticatorData = response.getAuthenticatorData();
|
|
131
|
+
if (!userVerified(authenticatorData)) throw factorUnavailable("the passkey creation was not user-verified");
|
|
132
|
+
return readPrfResult(credential, extensions.prf.results, input.salts.second !== void 0, credentialSynced(authenticatorData));
|
|
125
133
|
},
|
|
126
134
|
assert
|
|
127
135
|
};
|
|
@@ -131,11 +139,37 @@ function createWebAuthnPasskey(rp) {
|
|
|
131
139
|
/**
|
|
132
140
|
* The Web host: `createWebWallet` spawns the bundled kernel worker (dist/kernel.worker.js) whose
|
|
133
141
|
* vault lives in the worker's own IndexedDB. Lock destroys the worker; the vault survives in
|
|
134
|
-
* IndexedDB, so the respawned worker reloads it.
|
|
135
|
-
*
|
|
142
|
+
* IndexedDB, so the respawned worker reloads it. `pagehide` locks immediately; the idle timer is
|
|
143
|
+
* refreshed by user-presence signals, not request traffic. The WebAuthn ceremony runs on the main
|
|
144
|
+
* thread.
|
|
145
|
+
*/
|
|
146
|
+
const LISTENER_OPTIONS = {
|
|
147
|
+
capture: true,
|
|
148
|
+
passive: true
|
|
149
|
+
};
|
|
150
|
+
const ACTIVITY_EVENTS = [
|
|
151
|
+
"pointerdown",
|
|
152
|
+
"pointermove",
|
|
153
|
+
"keydown",
|
|
154
|
+
"wheel",
|
|
155
|
+
"touchstart"
|
|
156
|
+
];
|
|
157
|
+
/**
|
|
158
|
+
* The browser's user-presence source: direct input on the page, or returning to a visible tab.
|
|
159
|
+
* Capture-phase passive listeners do a single boolean check per event — high-frequency signals
|
|
160
|
+
* (scroll, pointermove) cost nothing. Returns the unsubscribe the wallet owns.
|
|
136
161
|
*/
|
|
137
|
-
|
|
138
|
-
const
|
|
162
|
+
function subscribePresence(onActivity) {
|
|
163
|
+
for (const type of ACTIVITY_EVENTS) globalThis.addEventListener(type, onActivity, LISTENER_OPTIONS);
|
|
164
|
+
const onVisible = () => {
|
|
165
|
+
if (document.visibilityState === "visible") onActivity();
|
|
166
|
+
};
|
|
167
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
168
|
+
return () => {
|
|
169
|
+
for (const type of ACTIVITY_EVENTS) globalThis.removeEventListener(type, onActivity, LISTENER_OPTIONS);
|
|
170
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
139
173
|
async function bestEffort(task) {
|
|
140
174
|
try {
|
|
141
175
|
await task();
|
|
@@ -146,16 +180,14 @@ async function createWebWallet(options) {
|
|
|
146
180
|
if (autoLock.platform !== "web") throw new WalletError("invalid-input", "createWebWallet accepts only the web autoLock policy");
|
|
147
181
|
const init = {
|
|
148
182
|
t: "init",
|
|
149
|
-
|
|
150
|
-
maxSessionMs: MAX_SESSION_MS
|
|
183
|
+
idleMinutes: autoLock.idleMinutes
|
|
151
184
|
};
|
|
152
|
-
const kernelUrl = new URL("kernel.worker.js", import.meta.url);
|
|
153
185
|
const ceremony = createWebAuthnPasskey({
|
|
154
186
|
id: options.passkey.rpId,
|
|
155
187
|
name: options.passkey.rpName
|
|
156
188
|
});
|
|
157
189
|
const spawn = () => {
|
|
158
|
-
const worker = new Worker(
|
|
190
|
+
const worker = new Worker(new URL("kernel.worker.js", import.meta.url), {
|
|
159
191
|
type: "module",
|
|
160
192
|
name: "qntx-wallet"
|
|
161
193
|
});
|
|
@@ -193,23 +225,20 @@ async function createWebWallet(options) {
|
|
|
193
225
|
spawn,
|
|
194
226
|
passkey: options.passkey,
|
|
195
227
|
ceremony,
|
|
228
|
+
setTimer: (delayMs, fn) => {
|
|
229
|
+
const timer = globalThis.setTimeout(fn, delayMs);
|
|
230
|
+
return () => {
|
|
231
|
+
globalThis.clearTimeout(timer);
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
presence: subscribePresence,
|
|
196
235
|
onDestroy: () => {
|
|
197
|
-
document.removeEventListener("visibilitychange", onVisibility);
|
|
198
236
|
globalThis.removeEventListener("pagehide", onPageHide);
|
|
199
|
-
globalThis.clearTimeout(hiddenTimer);
|
|
200
237
|
}
|
|
201
238
|
});
|
|
202
|
-
let hiddenTimer = 0;
|
|
203
|
-
const onVisibility = () => {
|
|
204
|
-
if (document.visibilityState === "hidden") hiddenTimer = globalThis.setTimeout(() => {
|
|
205
|
-
bestEffort(async () => wallet.vault.lock());
|
|
206
|
-
}, HIDDEN_LOCK_MS);
|
|
207
|
-
else globalThis.clearTimeout(hiddenTimer);
|
|
208
|
-
};
|
|
209
239
|
const onPageHide = () => {
|
|
210
240
|
bestEffort(async () => wallet.vault.lock());
|
|
211
241
|
};
|
|
212
|
-
document.addEventListener("visibilitychange", onVisibility);
|
|
213
242
|
globalThis.addEventListener("pagehide", onPageHide);
|
|
214
243
|
let persisted = false;
|
|
215
244
|
const unsubscribe = wallet.subscribe(() => {
|
package/dist/browser.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.mjs","names":[],"sources":["../src/browser/passkey.ts","../src/browser/index.ts"],"sourcesContent":["/**\n * The WebAuthn passkey ceremony (main thread only): `navigator.credentials` with the PRF extension.\n * A wallet passkey derives the wallet seed from `PRF(walletSalt)`; the vault factor derives the\n * vault KEK from `PRF(vaultSalt)` on the same credential. Every ceremony output is checked for\n * `prf.enabled`, PRF results and the user-verification flag — a credential that cannot prove UV\n * never reaches the kernel.\n */\nimport { WalletError } from \"@qntx/wallet\";\nimport type { PasskeyCeremony, PrfResult, PrfSalts } from \"@qntx/wallet/host\";\n\nexport type WebAuthnRp = {\n /** RP id — the origin's effective domain, e.g. `location.hostname`. */\n readonly id: string;\n readonly name: string;\n};\n\nconst PURPOSE_WALLET = 0x01;\nconst PURPOSE_VAULT = 0x02;\nconst UV_FLAG = 0x04; // authenticator-data flags byte (offset 32), bit 2 = user verified\n\nfunction factorUnavailable(message: string): WalletError {\n return new WalletError(\"factor-unavailable\", message);\n}\n\nfunction cancelled(message: string): WalletError {\n return new WalletError(\"cancelled\", message);\n}\n\nfunction mapCeremonyError(error: unknown): never {\n if (\n error instanceof DOMException &&\n (error.name === \"NotAllowedError\" || error.name === \"AbortError\")\n ) {\n throw cancelled(\"the passkey ceremony was cancelled\");\n }\n if (error instanceof WalletError) {\n throw error;\n }\n throw factorUnavailable(\"the passkey ceremony failed\");\n}\n\n/** The UV bit of an authenticator-data blob (flags byte at offset 32, bit 0x04). */\nfunction userVerified(authenticatorData: ArrayBuffer): boolean {\n return (\n authenticatorData.byteLength > 32 &&\n // oxlint-disable-next-line eslint/no-bitwise -- flags bit test\n (new DataView(authenticatorData).getUint8(32) & UV_FLAG) !== 0\n );\n}\n\nfunction prfEval(salts: PrfSalts): AuthenticationExtensionsPRFValues {\n // The constructor copy narrows Uint8Array<ArrayBufferLike> to Uint8Array<ArrayBuffer> —\n // the DOM BufferSource does not admit shared-array-buffer views.\n return salts.second === undefined\n ? { first: new Uint8Array(salts.first) }\n : { first: new Uint8Array(salts.first), second: new Uint8Array(salts.second) };\n}\n\n/** A detached copy of a BufferSource — the returned Uint8Array owns its own fresh buffer. */\nfunction copyBufferSource(source: BufferSource): Uint8Array {\n const view =\n source instanceof ArrayBuffer\n ? new Uint8Array(source)\n : new Uint8Array(source.buffer, source.byteOffset, source.byteLength);\n return new Uint8Array(view);\n}\n\nfunction readPrfResult(\n credential: PublicKeyCredential,\n results: AuthenticationExtensionsPRFValues | undefined,\n wantSecond: boolean,\n): PrfResult {\n const first = results?.first;\n const second = results?.second;\n if (first === undefined || (wantSecond && second === undefined)) {\n throw factorUnavailable(\"the passkey produced no PRF output\");\n }\n // Copy into fresh Uint8Arrays and drop every reference to the extension's ArrayBuffers.\n return {\n credentialId: copyBufferSource(credential.rawId),\n first: copyBufferSource(first),\n ...(second === undefined ? {} : { second: copyBufferSource(second) }),\n };\n}\n\nfunction checkCredential(credential: unknown): PublicKeyCredential {\n if (!(credential instanceof PublicKeyCredential)) {\n throw factorUnavailable(\"the authenticator returned no public-key credential\");\n }\n return credential;\n}\n\n/**\n * The WebAuthn ceremony. `create` registers a resident credential with `prf.eval` over the salts;\n * when the extension returns no results a single assertion over the new credential produces them.\n * `assert` runs a discoverable (or allowCredentials-scoped) get with the same eval shape.\n */\nexport function createWebAuthnPasskey(rp: WebAuthnRp): PasskeyCeremony {\n async function assert(input: {\n readonly credentialIds?: ReadonlyArray<Uint8Array>;\n readonly salts: PrfSalts;\n }): Promise<PrfResult> {\n let credential: PublicKeyCredential;\n try {\n credential = checkCredential(\n await navigator.credentials.get({\n publicKey: {\n rpId: rp.id,\n challenge: crypto.getRandomValues(new Uint8Array(32)),\n userVerification: \"required\",\n ...(input.credentialIds === undefined\n ? {}\n : {\n allowCredentials: input.credentialIds.map((id) => ({\n type: \"public-key\" as const,\n id: new Uint8Array(id),\n })),\n }),\n extensions: { prf: { eval: prfEval(input.salts) } },\n },\n }),\n );\n } catch (error) {\n mapCeremonyError(error);\n }\n const { response } = credential;\n if (!(response instanceof AuthenticatorAssertionResponse)) {\n throw factorUnavailable(\"the authenticator returned no assertion response\");\n }\n if (!userVerified(response.authenticatorData)) {\n throw factorUnavailable(\"the passkey assertion was not user-verified\");\n }\n return readPrfResult(\n credential,\n credential.getClientExtensionResults().prf?.results,\n input.salts.second !== undefined,\n );\n }\n\n return {\n create: async (input) => {\n const userId = new Uint8Array(16);\n userId[0] = input.purpose === \"wallet\" ? PURPOSE_WALLET : PURPOSE_VAULT;\n crypto.getRandomValues(userId.subarray(1));\n let credential: PublicKeyCredential;\n try {\n credential = checkCredential(\n await navigator.credentials.create({\n publicKey: {\n rp: { id: rp.id, name: rp.name },\n user: {\n id: userId,\n name: input.name,\n displayName: input.name,\n },\n challenge: crypto.getRandomValues(new Uint8Array(32)),\n pubKeyCredParams: [\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n ],\n authenticatorSelection: {\n residentKey: \"required\",\n userVerification: \"required\",\n },\n extensions: { prf: { eval: prfEval(input.salts) } },\n },\n }),\n );\n } catch (error) {\n mapCeremonyError(error);\n }\n const { response } = credential;\n if (!(response instanceof AuthenticatorAttestationResponse)) {\n throw factorUnavailable(\"the authenticator returned no attestation response\");\n }\n const extensions = credential.getClientExtensionResults();\n if (extensions.prf?.enabled !== true) {\n throw factorUnavailable(\"the authenticator does not support the PRF extension\");\n }\n if (extensions.prf.results === undefined) {\n // Some authenticators omit creation-time PRF outputs: one assertion over the new\n // credential produces them (and it is that response whose UV flag must be checked).\n return assert({\n credentialIds: [copyBufferSource(credential.rawId)],\n salts: input.salts,\n });\n }\n if (!userVerified(response.getAuthenticatorData())) {\n throw factorUnavailable(\"the passkey creation was not user-verified\");\n }\n return readPrfResult(credential, extensions.prf.results, input.salts.second !== undefined);\n },\n assert,\n };\n}\n","/**\n * The Web host: `createWebWallet` spawns the bundled kernel worker (dist/kernel.worker.js) whose\n * vault lives in the worker's own IndexedDB. Lock destroys the worker; the vault survives in\n * IndexedDB, so the respawned worker reloads it. Page lifecycle locks: 5 minutes hidden or\n * `pagehide`. The WebAuthn ceremony runs on the main thread.\n */\nimport { WalletError } from \"@qntx/wallet\";\nimport type { Wallet, WalletOptions } from \"@qntx/wallet\";\nimport { connectKernel } from \"@qntx/wallet/host\";\nimport type { MainPort } from \"@qntx/wallet/host\";\nimport type { ProtocolValue } from \"@qntx/wallet/protocol\";\n\nimport type { BrowserWorkerInit } from \"../shared.ts\";\nimport { createWebAuthnPasskey } from \"./passkey.ts\";\n\nconst MAX_SESSION_MS = 12 * 3_600_000; // fixed: 12 h\nconst HIDDEN_LOCK_MS = 5 * 60_000; // lock after the page has been hidden for 5 minutes\n\nasync function bestEffort(task: () => Promise<unknown>): Promise<void> {\n try {\n await task();\n } catch {\n // Lifecycle hooks and persistence hints must never surface failures.\n }\n}\n\nexport async function createWebWallet(options: WalletOptions): Promise<Wallet> {\n const { autoLock } = options;\n if (autoLock.platform !== \"web\") {\n throw new WalletError(\"invalid-input\", \"createWebWallet accepts only the web autoLock policy\");\n }\n const init: BrowserWorkerInit = {\n t: \"init\",\n idleMs: autoLock.idleMinutes * 60_000,\n maxSessionMs: MAX_SESSION_MS,\n };\n const kernelUrl = new URL(\"kernel.worker.js\", import.meta.url);\n const ceremony = createWebAuthnPasskey({\n id: options.passkey.rpId,\n name: options.passkey.rpName,\n });\n\n const spawn = (): MainPort => {\n const worker = new Worker(kernelUrl, { type: \"module\", name: \"qntx-wallet\" });\n // oxlint-disable-next-line unicorn/require-post-message-target-origin -- Worker.postMessage takes a transfer list, not an origin\n worker.postMessage(init); // the kernel validates it strictly before startKernel\n return {\n post: (message: ProtocolValue, transfer: ArrayBuffer[]) => {\n worker.postMessage(message, transfer);\n },\n listen: (handler) => {\n const listener = (event: MessageEvent): void => {\n handler(event.data);\n };\n worker.addEventListener(\"message\", listener);\n return () => {\n worker.removeEventListener(\"message\", listener);\n };\n },\n onExit: (handler) => {\n // Web Workers have no exit event; error/messageerror are the only death signals.\n const listener = (): void => {\n handler();\n };\n worker.addEventListener(\"error\", listener);\n worker.addEventListener(\"messageerror\", listener);\n return () => {\n worker.removeEventListener(\"error\", listener);\n worker.removeEventListener(\"messageerror\", listener);\n };\n },\n terminate: () => {\n worker.terminate();\n },\n };\n };\n\n const wallet = await connectKernel({\n spawn,\n passkey: options.passkey,\n ceremony,\n onDestroy: () => {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n globalThis.removeEventListener(\"pagehide\", onPageHide);\n globalThis.clearTimeout(hiddenTimer);\n },\n });\n\n // Page lifecycle: lock when the page has been hidden for 5 minutes, and on pagehide. A\n // BFCache-frozen page still runs the queued locked-event handler on restore, so the wallet\n // respawns then — terminating the worker directly would leave the wallet pointing at a dead\n // port.\n let hiddenTimer = 0;\n const onVisibility = (): void => {\n if (document.visibilityState === \"hidden\") {\n hiddenTimer = globalThis.setTimeout(() => {\n void bestEffort(async () => wallet.vault.lock());\n }, HIDDEN_LOCK_MS);\n } else {\n globalThis.clearTimeout(hiddenTimer);\n }\n };\n const onPageHide = (): void => {\n void bestEffort(async () => wallet.vault.lock());\n };\n document.addEventListener(\"visibilitychange\", onVisibility);\n globalThis.addEventListener(\"pagehide\", onPageHide);\n\n // Ask the browser to persist the vault once it exists — a denied request is fine.\n let persisted = false;\n const unsubscribe = wallet.subscribe(() => {\n if (!persisted && wallet.getSnapshot().vault !== \"uninitialized\") {\n persisted = true;\n unsubscribe();\n void bestEffort(async () => navigator.storage.persist());\n }\n });\n\n return wallet;\n}\n\nexport { createWebAuthnPasskey } from \"./passkey.ts\";\nexport type { WebAuthnRp } from \"./passkey.ts\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"browser.mjs","names":[],"sources":["../src/browser/passkey.ts","../src/browser/index.ts"],"sourcesContent":["/**\n * The WebAuthn passkey ceremony (main thread only): `navigator.credentials` with the PRF extension.\n * A wallet passkey derives the wallet seed from `PRF(walletSalt)`; the vault factor derives the\n * vault KEK from `PRF(vaultSalt)` on the same credential. Every ceremony output is checked for\n * `prf.enabled`, PRF results and the user-verification flag — a credential that cannot prove UV\n * never reaches the kernel.\n */\nimport { WalletError } from \"@qntx/wallet\";\nimport { PASSKEY_PURPOSE_VAULT, PASSKEY_PURPOSE_WALLET } from \"@qntx/wallet/host\";\nimport type { PasskeyCeremony, PrfResult, PrfSalts } from \"@qntx/wallet/host\";\n\nexport type WebAuthnRp = {\n /** RP id — the origin's effective domain, e.g. `location.hostname`. */\n readonly id: string;\n readonly name: string;\n};\n\nconst UV_FLAG = 0x04; // authenticator-data flags byte (offset 32), bit 2 = user verified\nconst BE_FLAG = 0x08; // bit 3 = credential is backup-eligible (multi-device capable)\nconst BS_FLAG = 0x10; // bit 4 = credential is currently backed up (synced)\n\nfunction factorUnavailable(message: string): WalletError {\n return new WalletError(\"factor-unavailable\", message);\n}\n\nfunction cancelled(message: string): WalletError {\n return new WalletError(\"cancelled\", message);\n}\n\nfunction mapCeremonyError(error: unknown): never {\n if (\n error instanceof DOMException &&\n (error.name === \"NotAllowedError\" || error.name === \"AbortError\")\n ) {\n throw cancelled(\"the passkey ceremony was cancelled\");\n }\n if (error instanceof WalletError) {\n throw error;\n }\n throw factorUnavailable(\"the passkey ceremony failed\");\n}\n\n/** The UV bit of an authenticator-data blob (flags byte at offset 32, bit 0x04). */\nfunction userVerified(authenticatorData: ArrayBuffer): boolean {\n return (\n authenticatorData.byteLength > 32 &&\n // oxlint-disable-next-line eslint/no-bitwise -- flags bit test\n (new DataView(authenticatorData).getUint8(32) & UV_FLAG) !== 0\n );\n}\n\n/**\n * Whether the credential is backed up off-device (flags byte at offset 32): synced requires both BE\n * (the credential is eligible for backup) and BS (the authenticator actually backs it up).\n */\nfunction credentialSynced(authenticatorData: ArrayBuffer): boolean {\n return (\n authenticatorData.byteLength > 32 &&\n // oxlint-disable-next-line eslint/no-bitwise -- flags bit test\n (new DataView(authenticatorData).getUint8(32) & (BE_FLAG | BS_FLAG)) === (BE_FLAG | BS_FLAG)\n );\n}\n\nfunction prfEval(salts: PrfSalts): AuthenticationExtensionsPRFValues {\n // The constructor copy narrows Uint8Array<ArrayBufferLike> to Uint8Array<ArrayBuffer> —\n // the DOM BufferSource does not admit shared-array-buffer views.\n return salts.second === undefined\n ? { first: new Uint8Array(salts.first) }\n : { first: new Uint8Array(salts.first), second: new Uint8Array(salts.second) };\n}\n\n/** A detached copy of a BufferSource — the returned Uint8Array owns its own fresh buffer. */\nfunction copyBufferSource(source: BufferSource): Uint8Array {\n const view =\n source instanceof ArrayBuffer\n ? new Uint8Array(source)\n : new Uint8Array(source.buffer, source.byteOffset, source.byteLength);\n return new Uint8Array(view);\n}\n\nfunction readPrfResult(\n credential: PublicKeyCredential,\n results: AuthenticationExtensionsPRFValues | undefined,\n wantSecond: boolean,\n synced: boolean,\n userHandle?: ArrayBuffer,\n): PrfResult {\n const first = results?.first;\n const second = results?.second;\n if (first === undefined || (wantSecond && second === undefined)) {\n throw factorUnavailable(\"the passkey produced no PRF output\");\n }\n // Copy into fresh Uint8Arrays and drop every reference to the extension's ArrayBuffers.\n return {\n credentialId: copyBufferSource(credential.rawId),\n first: copyBufferSource(first),\n ...(second === undefined ? {} : { second: copyBufferSource(second) }),\n ...(userHandle === undefined ? {} : { userHandle: copyBufferSource(userHandle) }),\n synced,\n };\n}\n\nfunction checkCredential(credential: unknown): PublicKeyCredential {\n if (!(credential instanceof PublicKeyCredential)) {\n throw factorUnavailable(\"the authenticator returned no public-key credential\");\n }\n return credential;\n}\n\n/**\n * The WebAuthn ceremony. `create` registers a resident credential with `prf.eval` over the salts;\n * when the extension returns no results a single assertion over the new credential produces them.\n * `assert` runs a discoverable (or allowCredentials-scoped) get with the same eval shape.\n */\nexport function createWebAuthnPasskey(rp: WebAuthnRp): PasskeyCeremony {\n async function assert(input: {\n readonly credentialIds?: ReadonlyArray<Uint8Array>;\n readonly salts: PrfSalts;\n }): Promise<PrfResult> {\n let credential: PublicKeyCredential;\n try {\n credential = checkCredential(\n await navigator.credentials.get({\n publicKey: {\n rpId: rp.id,\n challenge: crypto.getRandomValues(new Uint8Array(32)),\n userVerification: \"required\",\n ...(input.credentialIds === undefined\n ? {}\n : {\n allowCredentials: input.credentialIds.map((id) => ({\n type: \"public-key\" as const,\n id: new Uint8Array(id),\n })),\n }),\n extensions: { prf: { eval: prfEval(input.salts) } },\n },\n }),\n );\n } catch (error) {\n mapCeremonyError(error);\n }\n const { response } = credential;\n if (!(response instanceof AuthenticatorAssertionResponse)) {\n throw factorUnavailable(\"the authenticator returned no assertion response\");\n }\n if (!userVerified(response.authenticatorData)) {\n throw factorUnavailable(\"the passkey assertion was not user-verified\");\n }\n return readPrfResult(\n credential,\n credential.getClientExtensionResults().prf?.results,\n input.salts.second !== undefined,\n credentialSynced(response.authenticatorData),\n response.userHandle ?? undefined,\n );\n }\n\n return {\n create: async (input) => {\n const userId = new Uint8Array(16);\n userId[0] = input.purpose === \"wallet\" ? PASSKEY_PURPOSE_WALLET : PASSKEY_PURPOSE_VAULT;\n crypto.getRandomValues(userId.subarray(1));\n let credential: PublicKeyCredential;\n try {\n credential = checkCredential(\n await navigator.credentials.create({\n publicKey: {\n rp: { id: rp.id, name: rp.name },\n user: {\n id: userId,\n name: input.name,\n displayName: input.name,\n },\n challenge: crypto.getRandomValues(new Uint8Array(32)),\n pubKeyCredParams: [\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n ],\n authenticatorSelection: {\n residentKey: \"required\",\n userVerification: \"required\",\n },\n extensions: { prf: { eval: prfEval(input.salts) } },\n },\n }),\n );\n } catch (error) {\n mapCeremonyError(error);\n }\n const { response } = credential;\n if (!(response instanceof AuthenticatorAttestationResponse)) {\n throw factorUnavailable(\"the authenticator returned no attestation response\");\n }\n const extensions = credential.getClientExtensionResults();\n if (extensions.prf?.enabled !== true) {\n throw factorUnavailable(\"the authenticator does not support the PRF extension\");\n }\n if (extensions.prf.results === undefined) {\n // Some authenticators omit creation-time PRF outputs: one assertion over the new\n // credential produces them (it is that response whose UV and BE/BS flags get read).\n return assert({\n credentialIds: [copyBufferSource(credential.rawId)],\n salts: input.salts,\n });\n }\n const authenticatorData = response.getAuthenticatorData();\n if (!userVerified(authenticatorData)) {\n throw factorUnavailable(\"the passkey creation was not user-verified\");\n }\n return readPrfResult(\n credential,\n extensions.prf.results,\n input.salts.second !== undefined,\n credentialSynced(authenticatorData),\n );\n },\n assert,\n };\n}\n","/**\n * The Web host: `createWebWallet` spawns the bundled kernel worker (dist/kernel.worker.js) whose\n * vault lives in the worker's own IndexedDB. Lock destroys the worker; the vault survives in\n * IndexedDB, so the respawned worker reloads it. `pagehide` locks immediately; the idle timer is\n * refreshed by user-presence signals, not request traffic. The WebAuthn ceremony runs on the main\n * thread.\n */\nimport { WalletError } from \"@qntx/wallet\";\nimport type { Wallet, WalletOptions } from \"@qntx/wallet\";\nimport { connectKernel } from \"@qntx/wallet/host\";\nimport type { MainPort } from \"@qntx/wallet/host\";\nimport type { ProtocolValue } from \"@qntx/wallet/protocol\";\n\nimport type { BrowserWorkerInit } from \"../shared.ts\";\nimport { createWebAuthnPasskey } from \"./passkey.ts\";\n\nconst LISTENER_OPTIONS = { capture: true, passive: true };\nconst ACTIVITY_EVENTS = [\"pointerdown\", \"pointermove\", \"keydown\", \"wheel\", \"touchstart\"];\n\n/**\n * The browser's user-presence source: direct input on the page, or returning to a visible tab.\n * Capture-phase passive listeners do a single boolean check per event — high-frequency signals\n * (scroll, pointermove) cost nothing. Returns the unsubscribe the wallet owns.\n */\nfunction subscribePresence(onActivity: () => void): () => void {\n for (const type of ACTIVITY_EVENTS) {\n globalThis.addEventListener(type, onActivity, LISTENER_OPTIONS);\n }\n const onVisible = (): void => {\n if (document.visibilityState === \"visible\") {\n onActivity();\n }\n };\n document.addEventListener(\"visibilitychange\", onVisible);\n return () => {\n for (const type of ACTIVITY_EVENTS) {\n globalThis.removeEventListener(type, onActivity, LISTENER_OPTIONS);\n }\n document.removeEventListener(\"visibilitychange\", onVisible);\n };\n}\n\nasync function bestEffort(task: () => Promise<unknown>): Promise<void> {\n try {\n await task();\n } catch {\n // Lifecycle hooks and persistence hints must never surface failures.\n }\n}\n\nexport async function createWebWallet(options: WalletOptions): Promise<Wallet> {\n const { autoLock } = options;\n if (autoLock.platform !== \"web\") {\n throw new WalletError(\"invalid-input\", \"createWebWallet accepts only the web autoLock policy\");\n }\n // The page sends only the idle choice; the worker owns the maximum session duration.\n const init: BrowserWorkerInit = { t: \"init\", idleMinutes: autoLock.idleMinutes };\n const ceremony = createWebAuthnPasskey({\n id: options.passkey.rpId,\n name: options.passkey.rpName,\n });\n\n const spawn = (): MainPort => {\n // Keep this exact `new Worker(new URL(\"…\", import.meta.url))` shape: bundlers (Vite,\n // webpack, Parcel, esbuild-based tools) recognize only this literal form as a worker entry and\n // bundle the kernel with its chunks and spark.wasm. A precomputed URL is copied as one opaque\n // asset, and the worker then dies loading its first chunk.\n const worker = new Worker(new URL(\"kernel.worker.js\", import.meta.url), {\n type: \"module\",\n name: \"qntx-wallet\",\n });\n // oxlint-disable-next-line unicorn/require-post-message-target-origin -- Worker.postMessage takes a transfer list, not an origin\n worker.postMessage(init); // the kernel validates it strictly before startKernel\n return {\n post: (message: ProtocolValue, transfer: ArrayBuffer[]) => {\n worker.postMessage(message, transfer);\n },\n listen: (handler) => {\n const listener = (event: MessageEvent): void => {\n handler(event.data);\n };\n worker.addEventListener(\"message\", listener);\n return () => {\n worker.removeEventListener(\"message\", listener);\n };\n },\n onExit: (handler) => {\n // Web Workers have no exit event; error/messageerror are the only death signals.\n const listener = (): void => {\n handler();\n };\n worker.addEventListener(\"error\", listener);\n worker.addEventListener(\"messageerror\", listener);\n return () => {\n worker.removeEventListener(\"error\", listener);\n worker.removeEventListener(\"messageerror\", listener);\n };\n },\n terminate: () => {\n worker.terminate();\n },\n };\n };\n\n const wallet = await connectKernel({\n spawn,\n passkey: options.passkey,\n ceremony,\n setTimer: (delayMs, fn) => {\n const timer = globalThis.setTimeout(fn, delayMs);\n return () => {\n globalThis.clearTimeout(timer);\n };\n },\n presence: subscribePresence,\n onDestroy: () => {\n globalThis.removeEventListener(\"pagehide\", onPageHide);\n },\n });\n\n // Page lifecycle: lock on pagehide. A BFCache-frozen page still runs the queued locked-event\n // handler on restore, so the wallet respawns then — terminating the worker directly would\n // leave the wallet pointing at a dead port.\n const onPageHide = (): void => {\n void bestEffort(async () => wallet.vault.lock());\n };\n globalThis.addEventListener(\"pagehide\", onPageHide);\n\n // Ask the browser to persist the vault once it exists — a denied request is fine.\n let persisted = false;\n const unsubscribe = wallet.subscribe(() => {\n if (!persisted && wallet.getSnapshot().vault !== \"uninitialized\") {\n persisted = true;\n unsubscribe();\n void bestEffort(async () => navigator.storage.persist());\n }\n });\n\n return wallet;\n}\n\nexport { createWebAuthnPasskey } from \"./passkey.ts\";\nexport type { WebAuthnRp } from \"./passkey.ts\";\n"],"mappings":";;;;;;;;;;AAiBA,MAAM,UAAU;AAIhB,SAAS,kBAAkB,SAA8B;CACvD,OAAO,IAAI,YAAY,sBAAsB,OAAO;AACtD;AAEA,SAAS,UAAU,SAA8B;CAC/C,OAAO,IAAI,YAAY,aAAa,OAAO;AAC7C;AAEA,SAAS,iBAAiB,OAAuB;CAC/C,IACE,iBAAiB,iBAChB,MAAM,SAAS,qBAAqB,MAAM,SAAS,eAEpD,MAAM,UAAU,oCAAoC;CAEtD,IAAI,iBAAiB,aACnB,MAAM;CAER,MAAM,kBAAkB,6BAA6B;AACvD;;AAGA,SAAS,aAAa,mBAAyC;CAC7D,OACE,kBAAkB,aAAa,OAE9B,IAAI,SAAS,iBAAiB,CAAC,CAAC,SAAS,EAAE,IAAI,aAAa;AAEjE;;;;;AAMA,SAAS,iBAAiB,mBAAyC;CACjE,OACE,kBAAkB,aAAa,OAE9B,IAAI,SAAS,iBAAiB,CAAC,CAAC,SAAS,EAAE,IAAK,QAAyB;AAE9E;AAEA,SAAS,QAAQ,OAAoD;CAGnE,OAAO,MAAM,WAAW,KAAA,IACpB,EAAE,OAAO,IAAI,WAAW,MAAM,KAAK,EAAE,IACrC;EAAE,OAAO,IAAI,WAAW,MAAM,KAAK;EAAG,QAAQ,IAAI,WAAW,MAAM,MAAM;CAAE;AACjF;;AAGA,SAAS,iBAAiB,QAAkC;CAC1D,MAAM,OACJ,kBAAkB,cACd,IAAI,WAAW,MAAM,IACrB,IAAI,WAAW,OAAO,QAAQ,OAAO,YAAY,OAAO,UAAU;CACxE,OAAO,IAAI,WAAW,IAAI;AAC5B;AAEA,SAAS,cACP,YACA,SACA,YACA,QACA,YACW;CACX,MAAM,QAAQ,SAAS;CACvB,MAAM,SAAS,SAAS;CACxB,IAAI,UAAU,KAAA,KAAc,cAAc,WAAW,KAAA,GACnD,MAAM,kBAAkB,oCAAoC;CAG9D,OAAO;EACL,cAAc,iBAAiB,WAAW,KAAK;EAC/C,OAAO,iBAAiB,KAAK;EAC7B,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,iBAAiB,MAAM,EAAE;EACnE,GAAI,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY,iBAAiB,UAAU,EAAE;EAC/E;CACF;AACF;AAEA,SAAS,gBAAgB,YAA0C;CACjE,IAAI,EAAE,sBAAsB,sBAC1B,MAAM,kBAAkB,qDAAqD;CAE/E,OAAO;AACT;;;;;;AAOA,SAAgB,sBAAsB,IAAiC;CACrE,eAAe,OAAO,OAGC;EACrB,IAAI;EACJ,IAAI;GACF,aAAa,gBACX,MAAM,UAAU,YAAY,IAAI,EAC9B,WAAW;IACT,MAAM,GAAG;IACT,WAAW,OAAO,gCAAgB,IAAI,WAAW,EAAE,CAAC;IACpD,kBAAkB;IAClB,GAAI,MAAM,kBAAkB,KAAA,IACxB,CAAC,IACD,EACE,kBAAkB,MAAM,cAAc,KAAK,QAAQ;KACjD,MAAM;KACN,IAAI,IAAI,WAAW,EAAE;IACvB,EAAE,EACJ;IACJ,YAAY,EAAE,KAAK,EAAE,MAAM,QAAQ,MAAM,KAAK,EAAE,EAAE;GACpD,EACF,CAAC,CACH;EACF,SAAS,OAAO;GACd,iBAAiB,KAAK;EACxB;EACA,MAAM,EAAE,aAAa;EACrB,IAAI,EAAE,oBAAoB,iCACxB,MAAM,kBAAkB,kDAAkD;EAE5E,IAAI,CAAC,aAAa,SAAS,iBAAiB,GAC1C,MAAM,kBAAkB,6CAA6C;EAEvE,OAAO,cACL,YACA,WAAW,0BAA0B,CAAC,CAAC,KAAK,SAC5C,MAAM,MAAM,WAAW,KAAA,GACvB,iBAAiB,SAAS,iBAAiB,GAC3C,SAAS,cAAc,KAAA,CACzB;CACF;CAEA,OAAO;EACL,QAAQ,OAAO,UAAU;GACvB,MAAM,yBAAS,IAAI,WAAW,EAAE;GAChC,OAAO,KAAK,MAAM,YAAY,WAAW,yBAAyB;GAClE,OAAO,gBAAgB,OAAO,SAAS,CAAC,CAAC;GACzC,IAAI;GACJ,IAAI;IACF,aAAa,gBACX,MAAM,UAAU,YAAY,OAAO,EACjC,WAAW;KACT,IAAI;MAAE,IAAI,GAAG;MAAI,MAAM,GAAG;KAAK;KAC/B,MAAM;MACJ,IAAI;MACJ,MAAM,MAAM;MACZ,aAAa,MAAM;KACrB;KACA,WAAW,OAAO,gCAAgB,IAAI,WAAW,EAAE,CAAC;KACpD,kBAAkB,CAChB;MAAE,MAAM;MAAc,KAAK;KAAG,GAC9B;MAAE,MAAM;MAAc,KAAK;KAAK,CAClC;KACA,wBAAwB;MACtB,aAAa;MACb,kBAAkB;KACpB;KACA,YAAY,EAAE,KAAK,EAAE,MAAM,QAAQ,MAAM,KAAK,EAAE,EAAE;IACpD,EACF,CAAC,CACH;GACF,SAAS,OAAO;IACd,iBAAiB,KAAK;GACxB;GACA,MAAM,EAAE,aAAa;GACrB,IAAI,EAAE,oBAAoB,mCACxB,MAAM,kBAAkB,oDAAoD;GAE9E,MAAM,aAAa,WAAW,0BAA0B;GACxD,IAAI,WAAW,KAAK,YAAY,MAC9B,MAAM,kBAAkB,sDAAsD;GAEhF,IAAI,WAAW,IAAI,YAAY,KAAA,GAG7B,OAAO,OAAO;IACZ,eAAe,CAAC,iBAAiB,WAAW,KAAK,CAAC;IAClD,OAAO,MAAM;GACf,CAAC;GAEH,MAAM,oBAAoB,SAAS,qBAAqB;GACxD,IAAI,CAAC,aAAa,iBAAiB,GACjC,MAAM,kBAAkB,4CAA4C;GAEtE,OAAO,cACL,YACA,WAAW,IAAI,SACf,MAAM,MAAM,WAAW,KAAA,GACvB,iBAAiB,iBAAiB,CACpC;EACF;EACA;CACF;AACF;;;;;;;;;;AC3MA,MAAM,mBAAmB;CAAE,SAAS;CAAM,SAAS;AAAK;AACxD,MAAM,kBAAkB;CAAC;CAAe;CAAe;CAAW;CAAS;AAAY;;;;;;AAOvF,SAAS,kBAAkB,YAAoC;CAC7D,KAAK,MAAM,QAAQ,iBACjB,WAAW,iBAAiB,MAAM,YAAY,gBAAgB;CAEhE,MAAM,kBAAwB;EAC5B,IAAI,SAAS,oBAAoB,WAC/B,WAAW;CAEf;CACA,SAAS,iBAAiB,oBAAoB,SAAS;CACvD,aAAa;EACX,KAAK,MAAM,QAAQ,iBACjB,WAAW,oBAAoB,MAAM,YAAY,gBAAgB;EAEnE,SAAS,oBAAoB,oBAAoB,SAAS;CAC5D;AACF;AAEA,eAAe,WAAW,MAA6C;CACrE,IAAI;EACF,MAAM,KAAK;CACb,QAAQ,CAER;AACF;AAEA,eAAsB,gBAAgB,SAAyC;CAC7E,MAAM,EAAE,aAAa;CACrB,IAAI,SAAS,aAAa,OACxB,MAAM,IAAI,YAAY,iBAAiB,sDAAsD;CAG/F,MAAM,OAA0B;EAAE,GAAG;EAAQ,aAAa,SAAS;CAAY;CAC/E,MAAM,WAAW,sBAAsB;EACrC,IAAI,QAAQ,QAAQ;EACpB,MAAM,QAAQ,QAAQ;CACxB,CAAC;CAED,MAAM,cAAwB;EAK5B,MAAM,SAAS,IAAI,OAAO,IAAI,IAAI,oBAAoB,YAAY,GAAG,GAAG;GACtE,MAAM;GACN,MAAM;EACR,CAAC;EAED,OAAO,YAAY,IAAI;EACvB,OAAO;GACL,OAAO,SAAwB,aAA4B;IACzD,OAAO,YAAY,SAAS,QAAQ;GACtC;GACA,SAAS,YAAY;IACnB,MAAM,YAAY,UAA8B;KAC9C,QAAQ,MAAM,IAAI;IACpB;IACA,OAAO,iBAAiB,WAAW,QAAQ;IAC3C,aAAa;KACX,OAAO,oBAAoB,WAAW,QAAQ;IAChD;GACF;GACA,SAAS,YAAY;IAEnB,MAAM,iBAAuB;KAC3B,QAAQ;IACV;IACA,OAAO,iBAAiB,SAAS,QAAQ;IACzC,OAAO,iBAAiB,gBAAgB,QAAQ;IAChD,aAAa;KACX,OAAO,oBAAoB,SAAS,QAAQ;KAC5C,OAAO,oBAAoB,gBAAgB,QAAQ;IACrD;GACF;GACA,iBAAiB;IACf,OAAO,UAAU;GACnB;EACF;CACF;CAEA,MAAM,SAAS,MAAM,cAAc;EACjC;EACA,SAAS,QAAQ;EACjB;EACA,WAAW,SAAS,OAAO;GACzB,MAAM,QAAQ,WAAW,WAAW,IAAI,OAAO;GAC/C,aAAa;IACX,WAAW,aAAa,KAAK;GAC/B;EACF;EACA,UAAU;EACV,iBAAiB;GACf,WAAW,oBAAoB,YAAY,UAAU;EACvD;CACF,CAAC;CAKD,MAAM,mBAAyB;EAC7B,WAAgB,YAAY,OAAO,MAAM,KAAK,CAAC;CACjD;CACA,WAAW,iBAAiB,YAAY,UAAU;CAGlD,IAAI,YAAY;CAChB,MAAM,cAAc,OAAO,gBAAgB;EACzC,IAAI,CAAC,aAAa,OAAO,YAAY,CAAC,CAAC,UAAU,iBAAiB;GAChE,YAAY;GACZ,YAAY;GACZ,WAAgB,YAAY,UAAU,QAAQ,QAAQ,CAAC;EACzD;CACF,CAAC;CAED,OAAO;AACT"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"protocol": 1,
|
|
3
|
+
"vaultFormat": 2,
|
|
4
|
+
"backends": {
|
|
5
|
+
"spark": {
|
|
6
|
+
"abiVersion": 1,
|
|
7
|
+
"wasmIntegrity": "sha256-PFqThpVpNZHbmIuR13MZqLAupIIhfVcd3nMtB9QMTTE="
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"libraries": [
|
|
11
|
+
{
|
|
12
|
+
"name": "@noble/ciphers",
|
|
13
|
+
"version": "2.4.0",
|
|
14
|
+
"integrity": "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw=="
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "@noble/curves",
|
|
18
|
+
"version": "2.4.0",
|
|
19
|
+
"integrity": "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew=="
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "@noble/hashes",
|
|
23
|
+
"version": "2.4.0",
|
|
24
|
+
"integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "@qntx/nostr",
|
|
28
|
+
"version": "0.15.0",
|
|
29
|
+
"integrity": "sha512-bXgvNSlj4p/uw996x8rlB9bI50wcY+uH9rq+2lJBn6hZAUetBrf6ujTrKRQCpzg5DS2MwBWl4kmV4aXLzPUFRQ=="
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "@qntx/spark",
|
|
33
|
+
"version": "0.5.0",
|
|
34
|
+
"integrity": "sha512-nRJzrACUqRkKPwZP676dtYHXLWDTb2AzhVKLzN8PLlrcF7IEDYjQvlAMV0USC22vcQh1EqtWPvLGG6X/dCQ4Pw=="
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "@qntx/spark-wasm",
|
|
38
|
+
"version": "0.5.0",
|
|
39
|
+
"integrity": "sha512-C+TQc3xr7TtIF/jX+gcgAKOgvxVqKR70fVZQHzSd6ulnjwXGqS/CmfjgH59kqEvTrsl6vyMu1r7p4cOweTTVWQ=="
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "@scure/base",
|
|
43
|
+
"version": "2.4.0",
|
|
44
|
+
"integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg=="
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "@scure/bip32",
|
|
48
|
+
"version": "2.4.0",
|
|
49
|
+
"integrity": "sha512-i3DS0CptAocyvqE4n3SUkpzeQK4vJMFwWLofTwRiiKo2aWojBOfyMCgfKw9HVpO6fSY5AK86sHS/Uzn8kK9Few=="
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "@scure/bip39",
|
|
53
|
+
"version": "2.4.0",
|
|
54
|
+
"integrity": "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA=="
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"bundles": [
|
|
58
|
+
{
|
|
59
|
+
"file": "kernel.worker.js",
|
|
60
|
+
"sha256": "f6bbcd9c410672061398fba3b1aa0b407667c2ab5828fe00a69ceba270113d1b"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"file": "node-gxb54jcq.js",
|
|
64
|
+
"sha256": "e58ca528749b9191f761652a3ad7dedc961e2ea2e7fc61987c9ed1afa588eb94"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"file": "worker-gxb54jcq.js",
|
|
68
|
+
"sha256": "e58ca528749b9191f761652a3ad7dedc961e2ea2e7fc61987c9ed1afa588eb94"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"file": "kernel.node.js",
|
|
72
|
+
"sha256": "ed259c5cc067602e8344a731403b5a05c37d46d234978a30f6b8a577936791c4"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"file": "browser-dy5h3c5e.js",
|
|
76
|
+
"sha256": "31c1cc26ff286d492668c5b4bf60833283cf8f17b252bdc234fbba94c67db155"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"file": "node-nhrchcf5.js",
|
|
80
|
+
"sha256": "55fdd5775b473d0f283502976f68a3413b8a5587ca6293b1021fe008fcbf8302"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"file": "signer-yectsdty.js",
|
|
84
|
+
"sha256": "6f26acebc7bb9b40b0074ef3efea88c1126d416b710e8e04e3deb297364aa82a"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"file": "worker-nhrchcf5.js",
|
|
88
|
+
"sha256": "55fdd5775b473d0f283502976f68a3413b8a5587ca6293b1021fe008fcbf8302"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|