@qntx/wallet-worker 0.5.2 → 0.7.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/dist/node.d.mts CHANGED
@@ -1,12 +1,9 @@
1
1
  import { AutoLockPolicy, PasskeyConfig, Wallet } from "@qntx/wallet";
2
2
  import { PasskeyCeremony } from "@qntx/wallet/host";
3
3
  import { Worker } from "node:worker_threads";
4
- //#region src/node/software-passkey.d.ts
5
4
  export declare function createSoftwarePasskey(seed: Uint8Array, options?: {
6
5
  readonly synced?: boolean;
7
6
  }): PasskeyCeremony;
8
- //#endregion
9
- //#region src/node/index.d.ts
10
7
  export type NodeWalletOptions = {
11
8
  readonly passkey: PasskeyConfig;
12
9
  readonly autoLock: AutoLockPolicy;
@@ -19,5 +16,4 @@ export type NodeWalletOptions = {
19
16
  readonly onSpawn?: (worker: Worker) => void;
20
17
  };
21
18
  export declare function createNodeWallet(options: NodeWalletOptions): Promise<Wallet>;
22
- //#endregion
23
19
  //# sourceMappingURL=node.d.mts.map
package/dist/node.mjs CHANGED
@@ -4,7 +4,7 @@ import { MessageChannel, Worker } from "node:worker_threads";
4
4
  import { sha256 } from "@noble/hashes/sha2.js";
5
5
  import { bytesToHex, concatBytes, utf8ToBytes } from "@noble/hashes/utils.js";
6
6
  import { hmac } from "@noble/hashes/hmac.js";
7
- //#region ../wallet-kernel/src/errors.ts
7
+ // ../wallet-kernel/src/errors.ts
8
8
  /**
9
9
  * The only error type kernel code throws. Its code is the public `WalletErrorCode`: the kernel
10
10
  * returns it verbatim in the error payload. Messages are static strings: they must never echo user
@@ -26,8 +26,7 @@ var KernelError = class extends Error {
26
26
  this.corrupted = corrupted;
27
27
  }
28
28
  };
29
- //#endregion
30
- //#region ../wallet-kernel/src/vault/memory.ts
29
+ // ../wallet-kernel/src/vault/memory.ts
31
30
  function cloneRecord(record) {
32
31
  return {
33
32
  ...record,
@@ -162,8 +161,7 @@ function createMemoryStore() {
162
161
  }
163
162
  };
164
163
  }
165
- //#endregion
166
- //#region src/shared.ts
164
+ // src/shared.ts
167
165
  function encodeImage(image) {
168
166
  return {
169
167
  meta: image.meta,
@@ -171,8 +169,7 @@ function encodeImage(image) {
171
169
  records: image.records
172
170
  };
173
171
  }
174
- //#endregion
175
- //#region src/node/store-server.ts
172
+ // src/node/store-server.ts
176
173
  function isStoreRequest(raw) {
177
174
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return false;
178
175
  const request = raw;
@@ -241,8 +238,7 @@ function serveStore(store, port) {
241
238
  port.close();
242
239
  };
243
240
  }
244
- //#endregion
245
- //#region src/node/software-passkey.ts
241
+ // src/node/software-passkey.ts
246
242
  /**
247
243
  * CreateSoftwarePasskey — a TEST-ONLY PasskeyCeremony. There is no authenticator, no user presence
248
244
  * check, no credential store: the "passkey" is a deterministic function of `seed`, so anyone
@@ -295,8 +291,7 @@ function createSoftwarePasskey(seed, options) {
295
291
  }
296
292
  };
297
293
  }
298
- //#endregion
299
- //#region src/node/index.ts
294
+ // src/node/index.ts
300
295
  /**
301
296
  * The Node host: `createNodeWallet` spawns the bundled kernel worker (dist/kernel.node.js) and
302
297
  * keeps the vault image — ciphertext + public headers, never secrets — in the main thread's
@@ -361,7 +356,6 @@ async function createNodeWallet(options) {
361
356
  }
362
357
  });
363
358
  }
364
- //#endregion
365
359
  export { createNodeWallet, createSoftwarePasskey };
366
360
 
367
361
  //# sourceMappingURL=node.mjs.map
package/dist/node.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"node.mjs","names":[],"sources":["../../wallet-kernel/src/errors.ts","../../wallet-kernel/src/vault/memory.ts","../src/shared.ts","../src/node/store-server.ts","../src/node/software-passkey.ts","../src/node/index.ts"],"sourcesContent":["import type { WalletErrorCode } from \"@qntx/wallet/types\";\n\nexport type KernelErrorCode = WalletErrorCode;\n\n/**\n * The only error type kernel code throws. Its code is the public `WalletErrorCode`: the kernel\n * returns it verbatim in the error payload. Messages are static strings: they must never echo user\n * input or secret contents.\n */\nexport class KernelError extends Error {\n readonly code: KernelErrorCode;\n readonly retryAt: number | undefined;\n /**\n * True when the error proves the stored vault itself is corrupt — the kernel latches the\n * corrupted state on it. Ordinary failures (a wrong factor, a bad param) leave it false.\n */\n readonly corrupted: boolean;\n\n constructor(code: KernelErrorCode, message: string, retryAt?: number, corrupted = false) {\n super(message);\n this.name = \"KernelError\";\n this.code = code;\n this.retryAt = retryAt;\n this.corrupted = corrupted;\n }\n}\n","import { KernelError } from \"../errors.ts\";\nimport type {\n RecordStore,\n StoredFactor,\n StoredRecord,\n StoreOp,\n VaultImage,\n VaultMeta,\n} from \"./store.ts\";\n\nfunction cloneRecord(record: StoredRecord): StoredRecord {\n return { ...record, sealed: Uint8Array.from(record.sealed) };\n}\n\nfunction cloneFactor(factor: StoredFactor): StoredFactor {\n return { ...factor, sealed: Uint8Array.from(factor.sealed) };\n}\n\nfunction cloneMeta(meta: VaultMeta): VaultMeta {\n return { ...meta };\n}\n\nexport function cloneImage(image: VaultImage): VaultImage {\n return {\n meta: image.meta === undefined ? undefined : cloneMeta(image.meta),\n records: image.records.map(cloneRecord),\n factors: image.factors.map(cloneFactor),\n };\n}\n\nfunction conflict(message: string): KernelError {\n return new KernelError(\"conflict\", `vault store: ${message}`);\n}\n\nfunction requireMeta(meta: VaultMeta | undefined): VaultMeta {\n if (meta === undefined) {\n throw conflict(\"vault is not initialized\");\n }\n return meta;\n}\n\n/**\n * In-memory RecordStore for the Node host and kernel tests. Loads and commits deep-copy every\n * Uint8Array, so callers can never mutate stored state. `commit` applies ops to a clone and swaps\n * atomically: a failing precondition (`init`, `expect-revision`, a counter op on an empty store)\n * leaves the store untouched. Content ops bump `meta.revision` once per commit; an `init` commit\n * writes its meta verbatim (revision stays as the initializer set it).\n */\n/**\n * Applies a commit's ops to a vault image and returns the next image — the single implementation of\n * record/factor op semantics shared by the memory store and the file-backed store in\n *\n * @qntx/wallet-native.\n */\nexport function applyStoreOps(image: VaultImage, ops: ReadonlyArray<StoreOp>): VaultImage {\n let { meta } = image;\n let { records } = image;\n let { factors } = image;\n let bump = false;\n let initialized = false;\n for (const op of ops) {\n switch (op.op) {\n case \"init\":\n if (meta !== undefined || records.length > 0 || factors.length > 0) {\n throw conflict(\"vault already initialized\");\n }\n meta = cloneMeta(op.meta);\n initialized = true;\n break;\n case \"expect-revision\":\n if (meta === undefined || meta.revision !== op.revision) {\n throw conflict(\"vault changed underneath\");\n }\n break;\n case \"set-key-id\":\n meta = { ...requireMeta(meta), keyId: op.keyId };\n bump = true;\n break;\n case \"record-failure\": {\n const current = requireMeta(meta);\n meta = {\n ...current,\n failedAttempts: current.failedAttempts + 1,\n lastFailureAt: op.at,\n };\n break;\n }\n case \"reset-failures\":\n meta = { ...requireMeta(meta), failedAttempts: 0, lastFailureAt: 0 };\n break;\n case \"put-record\":\n records = [\n ...records.filter(\n (record) => !(record.kind === op.record.kind && record.id === op.record.id),\n ),\n cloneRecord(op.record),\n ];\n bump = true;\n break;\n case \"delete-record\":\n records = records.filter((record) => !(record.kind === op.kind && record.id === op.id));\n bump = true;\n break;\n case \"put-factor\":\n factors = [\n ...factors.filter((factor) => factor.id !== op.factor.id),\n cloneFactor(op.factor),\n ];\n bump = true;\n break;\n case \"delete-factor\":\n factors = factors.filter((factor) => factor.id !== op.id);\n bump = true;\n break;\n case \"clear\":\n meta = undefined;\n records = [];\n factors = [];\n break;\n }\n }\n if (bump && !initialized && meta !== undefined) {\n meta = { ...meta, revision: meta.revision + 1 };\n }\n return { meta, records, factors };\n}\n\nexport function createMemoryStore(): RecordStore {\n let image: VaultImage = { meta: undefined, records: [], factors: [] };\n const listeners = new Set<() => void>();\n\n return {\n async load() {\n return cloneImage(image);\n },\n async commit(ops) {\n const next = applyStoreOps(image, ops);\n image = next;\n for (const listener of listeners) {\n listener();\n }\n },\n watch(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n","/**\n * The store RPC shared by the worker (`src/worker/store-client.ts`) and the main side\n * (`src/node/store-server.ts`). The vault image travels as plain structured-clone data — it is\n * ciphertext plus public headers only, never decrypted secrets. This file stays platform-neutral:\n * the port shape is injected on both sides.\n */\nimport type { KernelErrorCode, StoreOp, VaultImage } from \"@qntx/wallet-kernel\";\n\nexport type StorePort = {\n readonly post: (message: unknown) => void;\n readonly listen: (handler: (message: unknown) => void) => () => void;\n};\n\nexport type StoreRequest =\n | { readonly t: \"load\"; readonly id: number }\n | { readonly t: \"commit\"; readonly id: number; readonly ops: ReadonlyArray<StoreOp> };\n\nexport type StoreReply =\n | { readonly t: \"loaded\"; readonly id: number; readonly image: WireImage }\n | { readonly t: \"committed\"; readonly id: number }\n | { readonly t: \"failed\"; readonly id: number; readonly code: KernelErrorCode }\n | { readonly t: \"changed\" };\n\n/** Structured clone preserves `undefined` properties, so `meta` travels as-is. */\ntype WireImage = VaultImage;\n\nexport function encodeImage(image: VaultImage): WireImage {\n return { meta: image.meta, factors: image.factors, records: image.records };\n}\n\nexport { assertVaultImage } from \"@qntx/wallet-kernel\";\n\nexport type IdleMinutes = 5 | 15 | 60;\n\nexport function isIdleMinutes(value: unknown): value is IdleMinutes {\n return value === 5 || value === 15 || value === 60;\n}\n\nexport function idleMsOf(minutes: IdleMinutes): number {\n return minutes * 60_000;\n}\n\n/** The fixed maximum session duration owned by the worker entries (never a host input). */\nexport const MAX_SESSION_MS: number = 12 * 3_600_000;\n\n/**\n * The first message the Web host posts to `kernel.worker.js`: the session policy. The browser\n * worker's vault lives in IndexedDB inside the worker itself, so unlike Node there is no store port\n * — policy is all the kernel needs before `startKernel` runs.\n */\nexport type BrowserWorkerInit = {\n readonly t: \"init\";\n readonly idleMinutes: IdleMinutes;\n};\n\nfunction plainInitObject(value: unknown): Record<string, unknown> | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return undefined;\n }\n if (Object.getPrototypeOf(value) !== Object.prototype) {\n return undefined;\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed to a plain object above\n return value as Record<string, unknown>;\n}\n\n/** Strict init-message validation; anything else must not start the kernel. */\nexport function isBrowserWorkerInit(value: unknown): value is BrowserWorkerInit {\n const init = plainInitObject(value);\n return (\n init !== undefined &&\n Object.keys(init).sort().join(\",\") === \"idleMinutes,t\" &&\n init[\"t\"] === \"init\" &&\n isIdleMinutes(init[\"idleMinutes\"])\n );\n}\n\n/**\n * Strict `workerData` validation for the Node kernel worker: `{ idleMinutes, storePort }` and\n * nothing else. `storePort` is checked structurally (the real type lives behind the worker_threads\n * boundary); a wrong shape must not start the kernel.\n */\nexport function isNodeWorkerData(\n value: unknown,\n): value is { readonly idleMinutes: IdleMinutes; readonly storePort: unknown } {\n const data = plainInitObject(value);\n return (\n data !== undefined &&\n Object.keys(data).sort().join(\",\") === \"idleMinutes,storePort\" &&\n isIdleMinutes(data[\"idleMinutes\"]) &&\n typeof data[\"storePort\"] === \"object\" &&\n data[\"storePort\"] !== null\n );\n}\n","/**\n * Main-side half of the store RPC: serves the shared in-memory RecordStore to the worker and pushes\n * \"changed\" notices for commits that did NOT come through this port (the kernel's own commits must\n * not echo back as external changes).\n */\nimport type { MessagePort } from \"node:worker_threads\";\n\nimport { KernelError } from \"@qntx/wallet-kernel\";\nimport type { KernelErrorCode, RecordStore } from \"@qntx/wallet-kernel\";\n\nimport type { StoreReply, StoreRequest } from \"../shared.ts\";\nimport { encodeImage } from \"../shared.ts\";\n\n// Internal wire between this package's two halves — malformed payloads get dropped.\nfunction isStoreRequest(raw: unknown): raw is StoreRequest {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n return false;\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed to a plain object above\n const request = raw as Record<string, unknown>;\n if (typeof request[\"id\"] !== \"number\") {\n return false;\n }\n return request[\"t\"] === \"load\" || (request[\"t\"] === \"commit\" && Array.isArray(request[\"ops\"]));\n}\n\nexport function serveStore(store: RecordStore, port: MessagePort): () => void {\n let ownCommits = 0;\n\n const post = (reply: StoreReply): void => {\n port.postMessage(reply);\n };\n\n // A failed load/commit replies \"failed\" with the KernelError code (or \"unavailable\" for a\n // non-kernel failure) — a request that hangs would deadlock the kernel's sequential queue.\n const failed = (id: number, error: unknown): void => {\n const code: KernelErrorCode = error instanceof KernelError ? error.code : \"unavailable\";\n post({ t: \"failed\", id, code });\n };\n\n const answer = async (request: StoreRequest): Promise<void> => {\n if (request.t === \"load\") {\n try {\n post({ t: \"loaded\", id: request.id, image: encodeImage(await store.load()) });\n } catch (error) {\n failed(request.id, error);\n }\n return;\n }\n ownCommits++;\n try {\n await store.commit(request.ops);\n } catch (error) {\n ownCommits--; // a failed commit fires no watch — do not eat the next external notice\n failed(request.id, error);\n return;\n }\n post({ t: \"committed\", id: request.id });\n };\n\n const onMessage = (raw: unknown): void => {\n if (!isStoreRequest(raw)) {\n return;\n }\n void (async (): Promise<void> => {\n try {\n await answer(raw);\n } catch {\n // A dead worker cannot be answered; the exit path handles it.\n }\n })();\n };\n\n const unwatch = store.watch(() => {\n if (ownCommits > 0) {\n ownCommits--;\n return;\n }\n post({ t: \"changed\" });\n });\n\n port.on(\"message\", onMessage);\n port.start();\n\n return () => {\n unwatch();\n port.off(\"message\", onMessage);\n port.close();\n };\n}\n","/**\n * CreateSoftwarePasskey — a TEST-ONLY PasskeyCeremony. There is no authenticator, no user presence\n * check, no credential store: the \"passkey\" is a deterministic function of `seed`, so anyone\n * holding the seed owns every wallet it ever protects. Use it in tests and demos only.\n *\n * CredentialId = SHA-256(seed ‖ \"credential\" ‖ u32be(n)) (n increments per create) PRF(salt) =\n * HMAC-SHA256(HMAC-SHA256(seed, credentialId), SHA-256(UTF-8(\"WebAuthn PRF\") ‖ 0x00 ‖ salt))\n * UserHandle = purpose byte ‖ SHA-256(seed ‖ \"user-handle\" ‖ credentialId)[0:15] — the same\n * 16-byte, purpose-prefixed shape the browser create path puts in `user.id`.\n */\nimport { hmac } from \"@noble/hashes/hmac.js\";\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { bytesToHex, concatBytes, utf8ToBytes } from \"@noble/hashes/utils.js\";\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 function createSoftwarePasskey(\n seed: Uint8Array,\n options?: { readonly synced?: boolean },\n): PasskeyCeremony {\n const synced = options?.synced ?? true; // software credentials pretend to be synced by default\n let counter = 0;\n let lastCreated: Uint8Array | undefined;\n const known = new Map<string, { credentialId: Uint8Array; userHandle: Uint8Array }>();\n\n const prf = (credentialId: Uint8Array, salt: Uint8Array): Uint8Array => {\n const key = hmac(sha256, seed, credentialId);\n const message = sha256(concatBytes(utf8ToBytes(\"WebAuthn PRF\"), new Uint8Array([0]), salt));\n return hmac(sha256, key, message);\n };\n\n const result = (\n credentialId: Uint8Array,\n salts: PrfSalts,\n userHandle?: Uint8Array,\n ): PrfResult => ({\n credentialId: new Uint8Array(credentialId),\n first: prf(credentialId, salts.first),\n ...(salts.second === undefined ? {} : { second: prf(credentialId, salts.second) }),\n ...(userHandle === undefined ? {} : { userHandle: new Uint8Array(userHandle) }),\n synced,\n });\n\n return {\n // oxlint-disable-next-line typescript/require-await -- the ceremony contract is async; this test double computes synchronously\n create: async ({ purpose, salts }) => {\n const counterBytes = new Uint8Array(4);\n new DataView(counterBytes.buffer).setUint32(0, counter);\n counter++;\n const credentialId = sha256(concatBytes(seed, utf8ToBytes(\"credential\"), counterBytes));\n const purposeByte = purpose === \"wallet\" ? PASSKEY_PURPOSE_WALLET : PASSKEY_PURPOSE_VAULT;\n const userHandle = new Uint8Array(16);\n userHandle[0] = purposeByte;\n userHandle.set(\n sha256(concatBytes(seed, utf8ToBytes(\"user-handle\"), credentialId)).subarray(0, 15),\n 1,\n );\n known.set(bytesToHex(credentialId), { credentialId, userHandle });\n lastCreated = credentialId;\n return result(credentialId, salts);\n },\n // oxlint-disable-next-line typescript/require-await -- the ceremony contract is async; this test double computes synchronously\n assert: async ({ credentialIds, salts }) => {\n const credentialId =\n credentialIds === undefined\n ? lastCreated\n : credentialIds.find((id) => known.has(bytesToHex(id)));\n if (credentialId === undefined) {\n throw new WalletError(\n \"factor-unavailable\",\n credentialIds === undefined\n ? \"no software credential exists yet\"\n : \"unknown software credential\",\n );\n }\n const entry = known.get(bytesToHex(credentialId));\n return result(credentialId, salts, entry?.userHandle);\n },\n };\n}\n","/**\n * The Node host: `createNodeWallet` spawns the bundled kernel worker (dist/kernel.node.js) and\n * keeps the vault image — ciphertext + public headers, never secrets — in the main thread's\n * in-memory store, served to each worker over a MessageChannel. That is what lets a lock destroy\n * the whole worker without losing the vault.\n */\nimport { MessageChannel, Worker } from \"node:worker_threads\";\n\nimport { WalletError } from \"@qntx/wallet\";\nimport type { AutoLockPolicy, PasskeyConfig, Wallet } from \"@qntx/wallet\";\nimport { createMemoryStore } from \"@qntx/wallet-kernel\";\nimport { connectKernel } from \"@qntx/wallet/host\";\nimport type { MainPort, PasskeyCeremony } from \"@qntx/wallet/host\";\nimport type { ProtocolValue } from \"@qntx/wallet/protocol\";\n\nimport type { NodeWorkerData } from \"../worker/node.ts\";\nimport { serveStore } from \"./store-server.ts\";\n\nexport { createSoftwarePasskey } from \"./software-passkey.ts\";\n\nexport type NodeWalletOptions = {\n readonly passkey: PasskeyConfig;\n readonly autoLock: AutoLockPolicy;\n readonly authenticator?: PasskeyCeremony;\n /**\n * Test/debug hook: the worker file location and a spawn observer (count spawns, watch 'exit').\n * Defaults to the bundled dist/kernel.node.js next to this module.\n */\n readonly kernelUrl?: string | URL;\n readonly onSpawn?: (worker: Worker) => void;\n};\n\nexport async function createNodeWallet(options: NodeWalletOptions): Promise<Wallet> {\n const { autoLock } = options;\n if (autoLock.platform !== \"web\") {\n throw new WalletError(\"invalid-input\", \"createNodeWallet accepts only the web autoLock policy\");\n }\n // The host sends only the idle choice; the worker owns the maximum session duration.\n const workerData = { idleMinutes: autoLock.idleMinutes };\n const store = createMemoryStore(); // ciphertext + public headers only — safe in the main thread\n const kernelUrl = options.kernelUrl ?? new URL(\"kernel.node.js\", import.meta.url);\n\n const spawn = (): MainPort => {\n const channel = new MessageChannel();\n const stopStore = serveStore(store, channel.port1);\n const worker = new Worker(kernelUrl, {\n workerData: { ...workerData, storePort: channel.port2 } satisfies NodeWorkerData,\n transferList: [channel.port2],\n });\n options.onSpawn?.(worker);\n return {\n post: (message: ProtocolValue, transfer: ArrayBuffer[]) => {\n worker.postMessage(message, transfer);\n },\n listen: (handler) => {\n const listener = (message: unknown) => {\n handler(message);\n };\n worker.on(\"message\", listener);\n return () => {\n worker.off(\"message\", listener);\n };\n },\n onExit: (handler) => {\n const listener = () => {\n handler(\"crashed\");\n };\n worker.once(\"exit\", listener);\n return () => {\n worker.off(\"exit\", listener);\n };\n },\n terminate: () => {\n stopStore();\n void worker.terminate();\n },\n };\n };\n\n return connectKernel({\n factors: \"software\",\n spawn,\n passkey: options.passkey,\n ceremony: options.authenticator,\n setTimer: (delayMs, fn) => {\n const timer = setTimeout(fn, delayMs);\n return () => {\n clearTimeout(timer);\n };\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"node.mjs","names":[],"sources":["../../wallet-kernel/src/errors.ts","../../wallet-kernel/src/vault/memory.ts","../src/shared.ts","../src/node/store-server.ts","../src/node/software-passkey.ts","../src/node/index.ts"],"sourcesContent":["import type { WalletErrorCode } from \"@qntx/wallet/types\";\n\nexport type KernelErrorCode = WalletErrorCode;\n\n/**\n * The only error type kernel code throws. Its code is the public `WalletErrorCode`: the kernel\n * returns it verbatim in the error payload. Messages are static strings: they must never echo user\n * input or secret contents.\n */\nexport class KernelError extends Error {\n readonly code: KernelErrorCode;\n readonly retryAt: number | undefined;\n /**\n * True when the error proves the stored vault itself is corrupt — the kernel latches the\n * corrupted state on it. Ordinary failures (a wrong factor, a bad param) leave it false.\n */\n readonly corrupted: boolean;\n\n constructor(code: KernelErrorCode, message: string, retryAt?: number, corrupted = false) {\n super(message);\n this.name = \"KernelError\";\n this.code = code;\n this.retryAt = retryAt;\n this.corrupted = corrupted;\n }\n}\n","import { KernelError } from \"../errors.ts\";\nimport type {\n RecordStore,\n StoredFactor,\n StoredRecord,\n StoreOp,\n VaultImage,\n VaultMeta,\n} from \"./store.ts\";\n\nfunction cloneRecord(record: StoredRecord): StoredRecord {\n return { ...record, sealed: Uint8Array.from(record.sealed) };\n}\n\nfunction cloneFactor(factor: StoredFactor): StoredFactor {\n return { ...factor, sealed: Uint8Array.from(factor.sealed) };\n}\n\nfunction cloneMeta(meta: VaultMeta): VaultMeta {\n return { ...meta };\n}\n\nexport function cloneImage(image: VaultImage): VaultImage {\n return {\n meta: image.meta === undefined ? undefined : cloneMeta(image.meta),\n records: image.records.map(cloneRecord),\n factors: image.factors.map(cloneFactor),\n };\n}\n\nfunction conflict(message: string): KernelError {\n return new KernelError(\"conflict\", `vault store: ${message}`);\n}\n\nfunction requireMeta(meta: VaultMeta | undefined): VaultMeta {\n if (meta === undefined) {\n throw conflict(\"vault is not initialized\");\n }\n return meta;\n}\n\n/**\n * In-memory RecordStore for the Node host and kernel tests. Loads and commits deep-copy every\n * Uint8Array, so callers can never mutate stored state. `commit` applies ops to a clone and swaps\n * atomically: a failing precondition (`init`, `expect-revision`, a counter op on an empty store)\n * leaves the store untouched. Content ops bump `meta.revision` once per commit; an `init` commit\n * writes its meta verbatim (revision stays as the initializer set it).\n */\n/**\n * Applies a commit's ops to a vault image and returns the next image — the single implementation of\n * record/factor op semantics shared by the memory store and the file-backed store in\n *\n * @qntx/wallet-native.\n */\nexport function applyStoreOps(image: VaultImage, ops: ReadonlyArray<StoreOp>): VaultImage {\n let { meta } = image;\n let { records } = image;\n let { factors } = image;\n let bump = false;\n let initialized = false;\n for (const op of ops) {\n switch (op.op) {\n case \"init\":\n if (meta !== undefined || records.length > 0 || factors.length > 0) {\n throw conflict(\"vault already initialized\");\n }\n meta = cloneMeta(op.meta);\n initialized = true;\n break;\n case \"expect-revision\":\n if (meta === undefined || meta.revision !== op.revision) {\n throw conflict(\"vault changed underneath\");\n }\n break;\n case \"set-key-id\":\n meta = { ...requireMeta(meta), keyId: op.keyId };\n bump = true;\n break;\n case \"record-failure\": {\n const current = requireMeta(meta);\n meta = {\n ...current,\n failedAttempts: current.failedAttempts + 1,\n lastFailureAt: op.at,\n };\n break;\n }\n case \"reset-failures\":\n meta = { ...requireMeta(meta), failedAttempts: 0, lastFailureAt: 0 };\n break;\n case \"put-record\":\n records = [\n ...records.filter(\n (record) => !(record.kind === op.record.kind && record.id === op.record.id),\n ),\n cloneRecord(op.record),\n ];\n bump = true;\n break;\n case \"delete-record\":\n records = records.filter((record) => !(record.kind === op.kind && record.id === op.id));\n bump = true;\n break;\n case \"put-factor\":\n factors = [\n ...factors.filter((factor) => factor.id !== op.factor.id),\n cloneFactor(op.factor),\n ];\n bump = true;\n break;\n case \"delete-factor\":\n factors = factors.filter((factor) => factor.id !== op.id);\n bump = true;\n break;\n case \"clear\":\n meta = undefined;\n records = [];\n factors = [];\n break;\n }\n }\n if (bump && !initialized && meta !== undefined) {\n meta = { ...meta, revision: meta.revision + 1 };\n }\n return { meta, records, factors };\n}\n\nexport function createMemoryStore(): RecordStore {\n let image: VaultImage = { meta: undefined, records: [], factors: [] };\n const listeners = new Set<() => void>();\n\n return {\n async load() {\n return cloneImage(image);\n },\n async commit(ops) {\n const next = applyStoreOps(image, ops);\n image = next;\n for (const listener of listeners) {\n listener();\n }\n },\n watch(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n","/**\n * The store RPC shared by the worker (`src/worker/store-client.ts`) and the main side\n * (`src/node/store-server.ts`). The vault image travels as plain structured-clone data — it is\n * ciphertext plus public headers only, never decrypted secrets. This file stays platform-neutral:\n * the port shape is injected on both sides.\n */\nimport type { KernelErrorCode, StoreOp, VaultImage } from \"@qntx/wallet-kernel\";\n\nexport type StorePort = {\n readonly post: (message: unknown) => void;\n readonly listen: (handler: (message: unknown) => void) => () => void;\n};\n\nexport type StoreRequest =\n | { readonly t: \"load\"; readonly id: number }\n | { readonly t: \"commit\"; readonly id: number; readonly ops: ReadonlyArray<StoreOp> };\n\nexport type StoreReply =\n | { readonly t: \"loaded\"; readonly id: number; readonly image: WireImage }\n | { readonly t: \"committed\"; readonly id: number }\n | { readonly t: \"failed\"; readonly id: number; readonly code: KernelErrorCode }\n | { readonly t: \"changed\" };\n\n/** Structured clone preserves `undefined` properties, so `meta` travels as-is. */\ntype WireImage = VaultImage;\n\nexport function encodeImage(image: VaultImage): WireImage {\n return { meta: image.meta, factors: image.factors, records: image.records };\n}\n\nexport { assertVaultImage } from \"@qntx/wallet-kernel\";\n\nexport type IdleMinutes = 5 | 15 | 60;\n\nexport function isIdleMinutes(value: unknown): value is IdleMinutes {\n return value === 5 || value === 15 || value === 60;\n}\n\nexport function idleMsOf(minutes: IdleMinutes): number {\n return minutes * 60_000;\n}\n\n/** The fixed maximum session duration owned by the worker entries (never a host input). */\nexport const MAX_SESSION_MS: number = 12 * 3_600_000;\n\n/**\n * The first message the Web host posts to `kernel.worker.js`: the session policy. The browser\n * worker's vault lives in IndexedDB inside the worker itself, so unlike Node there is no store port\n * — policy is all the kernel needs before `startKernel` runs.\n */\nexport type BrowserWorkerInit = {\n readonly t: \"init\";\n readonly idleMinutes: IdleMinutes;\n};\n\nfunction plainInitObject(value: unknown): Record<string, unknown> | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return undefined;\n }\n if (Object.getPrototypeOf(value) !== Object.prototype) {\n return undefined;\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed to a plain object above\n return value as Record<string, unknown>;\n}\n\n/** Strict init-message validation; anything else must not start the kernel. */\nexport function isBrowserWorkerInit(value: unknown): value is BrowserWorkerInit {\n const init = plainInitObject(value);\n return (\n init !== undefined &&\n Object.keys(init).sort().join(\",\") === \"idleMinutes,t\" &&\n init[\"t\"] === \"init\" &&\n isIdleMinutes(init[\"idleMinutes\"])\n );\n}\n\n/**\n * Strict `workerData` validation for the Node kernel worker: `{ idleMinutes, storePort }` and\n * nothing else. `storePort` is checked structurally (the real type lives behind the worker_threads\n * boundary); a wrong shape must not start the kernel.\n */\nexport function isNodeWorkerData(\n value: unknown,\n): value is { readonly idleMinutes: IdleMinutes; readonly storePort: unknown } {\n const data = plainInitObject(value);\n return (\n data !== undefined &&\n Object.keys(data).sort().join(\",\") === \"idleMinutes,storePort\" &&\n isIdleMinutes(data[\"idleMinutes\"]) &&\n typeof data[\"storePort\"] === \"object\" &&\n data[\"storePort\"] !== null\n );\n}\n","/**\n * Main-side half of the store RPC: serves the shared in-memory RecordStore to the worker and pushes\n * \"changed\" notices for commits that did NOT come through this port (the kernel's own commits must\n * not echo back as external changes).\n */\nimport type { MessagePort } from \"node:worker_threads\";\n\nimport { KernelError } from \"@qntx/wallet-kernel\";\nimport type { KernelErrorCode, RecordStore } from \"@qntx/wallet-kernel\";\n\nimport type { StoreReply, StoreRequest } from \"../shared.ts\";\nimport { encodeImage } from \"../shared.ts\";\n\n// Internal wire between this package's two halves — malformed payloads get dropped.\nfunction isStoreRequest(raw: unknown): raw is StoreRequest {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n return false;\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed to a plain object above\n const request = raw as Record<string, unknown>;\n if (typeof request[\"id\"] !== \"number\") {\n return false;\n }\n return request[\"t\"] === \"load\" || (request[\"t\"] === \"commit\" && Array.isArray(request[\"ops\"]));\n}\n\nexport function serveStore(store: RecordStore, port: MessagePort): () => void {\n let ownCommits = 0;\n\n const post = (reply: StoreReply): void => {\n port.postMessage(reply);\n };\n\n // A failed load/commit replies \"failed\" with the KernelError code (or \"unavailable\" for a\n // non-kernel failure) — a request that hangs would deadlock the kernel's sequential queue.\n const failed = (id: number, error: unknown): void => {\n const code: KernelErrorCode = error instanceof KernelError ? error.code : \"unavailable\";\n post({ t: \"failed\", id, code });\n };\n\n const answer = async (request: StoreRequest): Promise<void> => {\n if (request.t === \"load\") {\n try {\n post({ t: \"loaded\", id: request.id, image: encodeImage(await store.load()) });\n } catch (error) {\n failed(request.id, error);\n }\n return;\n }\n ownCommits++;\n try {\n await store.commit(request.ops);\n } catch (error) {\n ownCommits--; // a failed commit fires no watch — do not eat the next external notice\n failed(request.id, error);\n return;\n }\n post({ t: \"committed\", id: request.id });\n };\n\n const onMessage = (raw: unknown): void => {\n if (!isStoreRequest(raw)) {\n return;\n }\n void (async (): Promise<void> => {\n try {\n await answer(raw);\n } catch {\n // A dead worker cannot be answered; the exit path handles it.\n }\n })();\n };\n\n const unwatch = store.watch(() => {\n if (ownCommits > 0) {\n ownCommits--;\n return;\n }\n post({ t: \"changed\" });\n });\n\n port.on(\"message\", onMessage);\n port.start();\n\n return () => {\n unwatch();\n port.off(\"message\", onMessage);\n port.close();\n };\n}\n","/**\n * CreateSoftwarePasskey — a TEST-ONLY PasskeyCeremony. There is no authenticator, no user presence\n * check, no credential store: the \"passkey\" is a deterministic function of `seed`, so anyone\n * holding the seed owns every wallet it ever protects. Use it in tests and demos only.\n *\n * CredentialId = SHA-256(seed ‖ \"credential\" ‖ u32be(n)) (n increments per create) PRF(salt) =\n * HMAC-SHA256(HMAC-SHA256(seed, credentialId), SHA-256(UTF-8(\"WebAuthn PRF\") ‖ 0x00 ‖ salt))\n * UserHandle = purpose byte ‖ SHA-256(seed ‖ \"user-handle\" ‖ credentialId)[0:15] — the same\n * 16-byte, purpose-prefixed shape the browser create path puts in `user.id`.\n */\nimport { hmac } from \"@noble/hashes/hmac.js\";\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { bytesToHex, concatBytes, utf8ToBytes } from \"@noble/hashes/utils.js\";\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 function createSoftwarePasskey(\n seed: Uint8Array,\n options?: { readonly synced?: boolean },\n): PasskeyCeremony {\n const synced = options?.synced ?? true; // software credentials pretend to be synced by default\n let counter = 0;\n let lastCreated: Uint8Array | undefined;\n const known = new Map<string, { credentialId: Uint8Array; userHandle: Uint8Array }>();\n\n const prf = (credentialId: Uint8Array, salt: Uint8Array): Uint8Array => {\n const key = hmac(sha256, seed, credentialId);\n const message = sha256(concatBytes(utf8ToBytes(\"WebAuthn PRF\"), new Uint8Array([0]), salt));\n return hmac(sha256, key, message);\n };\n\n const result = (\n credentialId: Uint8Array,\n salts: PrfSalts,\n userHandle?: Uint8Array,\n ): PrfResult => ({\n credentialId: new Uint8Array(credentialId),\n first: prf(credentialId, salts.first),\n ...(salts.second === undefined ? {} : { second: prf(credentialId, salts.second) }),\n ...(userHandle === undefined ? {} : { userHandle: new Uint8Array(userHandle) }),\n synced,\n });\n\n return {\n // oxlint-disable-next-line typescript/require-await -- the ceremony contract is async; this test double computes synchronously\n create: async ({ purpose, salts }) => {\n const counterBytes = new Uint8Array(4);\n new DataView(counterBytes.buffer).setUint32(0, counter);\n counter++;\n const credentialId = sha256(concatBytes(seed, utf8ToBytes(\"credential\"), counterBytes));\n const purposeByte = purpose === \"wallet\" ? PASSKEY_PURPOSE_WALLET : PASSKEY_PURPOSE_VAULT;\n const userHandle = new Uint8Array(16);\n userHandle[0] = purposeByte;\n userHandle.set(\n sha256(concatBytes(seed, utf8ToBytes(\"user-handle\"), credentialId)).subarray(0, 15),\n 1,\n );\n known.set(bytesToHex(credentialId), { credentialId, userHandle });\n lastCreated = credentialId;\n return result(credentialId, salts);\n },\n // oxlint-disable-next-line typescript/require-await -- the ceremony contract is async; this test double computes synchronously\n assert: async ({ credentialIds, salts }) => {\n const credentialId =\n credentialIds === undefined\n ? lastCreated\n : credentialIds.find((id) => known.has(bytesToHex(id)));\n if (credentialId === undefined) {\n throw new WalletError(\n \"factor-unavailable\",\n credentialIds === undefined\n ? \"no software credential exists yet\"\n : \"unknown software credential\",\n );\n }\n const entry = known.get(bytesToHex(credentialId));\n return result(credentialId, salts, entry?.userHandle);\n },\n };\n}\n","/**\n * The Node host: `createNodeWallet` spawns the bundled kernel worker (dist/kernel.node.js) and\n * keeps the vault image — ciphertext + public headers, never secrets — in the main thread's\n * in-memory store, served to each worker over a MessageChannel. That is what lets a lock destroy\n * the whole worker without losing the vault.\n */\nimport { MessageChannel, Worker } from \"node:worker_threads\";\n\nimport { WalletError } from \"@qntx/wallet\";\nimport type { AutoLockPolicy, PasskeyConfig, Wallet } from \"@qntx/wallet\";\nimport { createMemoryStore } from \"@qntx/wallet-kernel\";\nimport { connectKernel } from \"@qntx/wallet/host\";\nimport type { MainPort, PasskeyCeremony } from \"@qntx/wallet/host\";\nimport type { ProtocolValue } from \"@qntx/wallet/protocol\";\n\nimport type { NodeWorkerData } from \"../worker/node.ts\";\nimport { serveStore } from \"./store-server.ts\";\n\nexport { createSoftwarePasskey } from \"./software-passkey.ts\";\n\nexport type NodeWalletOptions = {\n readonly passkey: PasskeyConfig;\n readonly autoLock: AutoLockPolicy;\n readonly authenticator?: PasskeyCeremony;\n /**\n * Test/debug hook: the worker file location and a spawn observer (count spawns, watch 'exit').\n * Defaults to the bundled dist/kernel.node.js next to this module.\n */\n readonly kernelUrl?: string | URL;\n readonly onSpawn?: (worker: Worker) => void;\n};\n\nexport async function createNodeWallet(options: NodeWalletOptions): Promise<Wallet> {\n const { autoLock } = options;\n if (autoLock.platform !== \"web\") {\n throw new WalletError(\"invalid-input\", \"createNodeWallet accepts only the web autoLock policy\");\n }\n // The host sends only the idle choice; the worker owns the maximum session duration.\n const workerData = { idleMinutes: autoLock.idleMinutes };\n const store = createMemoryStore(); // ciphertext + public headers only — safe in the main thread\n const kernelUrl = options.kernelUrl ?? new URL(\"kernel.node.js\", import.meta.url);\n\n const spawn = (): MainPort => {\n const channel = new MessageChannel();\n const stopStore = serveStore(store, channel.port1);\n const worker = new Worker(kernelUrl, {\n workerData: { ...workerData, storePort: channel.port2 } satisfies NodeWorkerData,\n transferList: [channel.port2],\n });\n options.onSpawn?.(worker);\n return {\n post: (message: ProtocolValue, transfer: ArrayBuffer[]) => {\n worker.postMessage(message, transfer);\n },\n listen: (handler) => {\n const listener = (message: unknown) => {\n handler(message);\n };\n worker.on(\"message\", listener);\n return () => {\n worker.off(\"message\", listener);\n };\n },\n onExit: (handler) => {\n const listener = () => {\n handler(\"crashed\");\n };\n worker.once(\"exit\", listener);\n return () => {\n worker.off(\"exit\", listener);\n };\n },\n terminate: () => {\n stopStore();\n void worker.terminate();\n },\n };\n };\n\n return connectKernel({\n factors: \"software\",\n spawn,\n passkey: options.passkey,\n ceremony: options.authenticator,\n setTimer: (delayMs, fn) => {\n const timer = setTimeout(fn, delayMs);\n return () => {\n clearTimeout(timer);\n };\n },\n });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qntx/wallet-worker",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "description": "Web Worker and worker_threads hosts for @qntx/wallet",
5
5
  "keywords": [
6
6
  "nostr",
@@ -62,22 +62,22 @@
62
62
  "devDependencies": {
63
63
  "@arethetypeswrong/cli": "0.18.5",
64
64
  "@noble/hashes": "2.4.0",
65
- "@playwright/test": "1.63.0",
65
+ "@playwright/test": "1.64.1",
66
66
  "@qntx/nostr": "0.15.3",
67
67
  "@qntx/spark": "0.5.2",
68
68
  "@qntx/spark-wasm": "0.5.2",
69
69
  "@qntx/tsconfig": "1.0.1",
70
- "@qntx/wallet": "0.5.2",
71
- "@qntx/wallet-kernel": "0.5.2",
70
+ "@qntx/wallet": "0.7.0",
71
+ "@qntx/wallet-kernel": "0.7.0",
72
72
  "@scure/base": "2.4.0",
73
73
  "@scure/btc-signer": "2.4.1",
74
74
  "@types/node": "26.6.5",
75
75
  "publint": "0.3.25",
76
76
  "typescript": "7.0.2",
77
- "vite-plus": "1.3.0"
77
+ "vite-plus": "1.4.0"
78
78
  },
79
79
  "peerDependencies": {
80
- "@qntx/wallet": "0.5.2"
80
+ "@qntx/wallet": "0.7.0"
81
81
  },
82
82
  "devEngines": {
83
83
  "packageManager": {