@prosopo/account 2.8.103 → 2.8.104

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.
@@ -1,60 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import { web3Enable } from "@polkadot/extension-dapp";
16
- import type { InjectedExtension } from "@polkadot/extension-inject/types";
17
- import { cryptoWaitReady } from "@polkadot/util-crypto";
18
- import { ProsopoError } from "@prosopo/common";
19
- import type { Account, ProcaptchaClientConfigOutput } from "@prosopo/types";
20
- import { Extension } from "./Extension.js";
21
-
22
- /**
23
- * Class for interfacing with web3 accounts.
24
- */
25
- export class ExtensionWeb3 extends Extension {
26
- public async getAccount(
27
- config: ProcaptchaClientConfigOutput,
28
- ): Promise<Account> {
29
- const { dappName, userAccountAddress: address } = config;
30
-
31
- if (!address) {
32
- throw new ProsopoError("WIDGET.NO_ACCOUNTS_FOUND", {
33
- context: { error: "No account address provided" },
34
- });
35
- }
36
-
37
- await cryptoWaitReady();
38
-
39
- // enable access to all extensions
40
- const extensions: InjectedExtension[] = await web3Enable(dappName);
41
- if (extensions.length === 0) {
42
- throw new ProsopoError("WIDGET.NO_EXTENSION_FOUND");
43
- }
44
-
45
- // search through all extensions for the one that has the account
46
- for (const extension of extensions) {
47
- const accounts = await extension.accounts.get();
48
- const account = accounts.find((account) => account.address === address);
49
- if (account) {
50
- return { account, extension };
51
- }
52
- }
53
-
54
- throw new ProsopoError("WIDGET.ACCOUNT_NOT_FOUND", {
55
- context: { error: `No account found matching ${address}` },
56
- });
57
- }
58
- }
59
-
60
- export default ExtensionWeb3;
package/src/index.ts DELETED
@@ -1,16 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
- export * from "./extension/Extension.js";
15
- export * from "./extension/ExtensionWeb2.js";
16
- export * from "./extension/ExtensionWeb3.js";
@@ -1,172 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- // Tests for ExtensionWeb2.createAccount. The interesting behaviour to lock
16
- // down is the two-branch account-derivation path:
17
- //
18
- // 1. Worker branch (preferred): `workerManager.entropyToKeypair(...)` →
19
- // `keyring.addFromPair({publicKey, secretKey})` — the sr25519 scalar-
20
- // mul runs off the main thread, main thread just packages the bytes.
21
- // 2. Fallback branch: worker throws (CSP block, task timeout, etc.) →
22
- // dynamic-import `entropyToMnemonic` → `keyring.addFromMnemonic(...)`
23
- // on the main thread.
24
- //
25
- // Both branches must produce the same account address for a given fingerprint
26
- // or session identity breaks between the two paths — a repeat visitor whose
27
- // worker fails would look like a different user to the provider.
28
-
29
- import { stringToU8a } from "@polkadot/util";
30
- import { getFingerprint } from "@prosopo/fingerprint";
31
- import { Keyring } from "@prosopo/keyring";
32
- import { entropyToMnemonic, hexHash } from "@prosopo/util-crypto";
33
- import { beforeEach, describe, expect, it, vi } from "vitest";
34
-
35
- // vi.hoisted() makes these accessible from within vi.mock factories, which
36
- // run before the module's own imports at collection time.
37
- const mocks = vi.hoisted(() => {
38
- const entropyToKeypair = vi.fn();
39
- return {
40
- entropyToKeypair,
41
- getCryptoWorkerManager: vi.fn(() => ({
42
- prewarm: vi.fn(),
43
- entropyToKeypair,
44
- entropyToMnemonic: vi.fn(),
45
- })),
46
- };
47
- });
48
-
49
- vi.mock("@prosopo/fingerprint", async (importOriginal) => {
50
- const actual =
51
- (await importOriginal()) as typeof import("@prosopo/fingerprint");
52
- return {
53
- ...actual,
54
- getFingerprint: vi.fn(),
55
- prefetchFingerprint: vi.fn(),
56
- };
57
- });
58
-
59
- vi.mock("../workers/CryptoWorkerManager.js", () => ({
60
- getCryptoWorkerManager: mocks.getCryptoWorkerManager,
61
- // Types re-exported at runtime aren't needed for the tests but keep the
62
- // mock shape aligned with the module's actual named exports.
63
- CryptoWorkerManager: class {},
64
- }));
65
-
66
- // Imported after mocks so the module picks up the mocked getFingerprint and
67
- // worker manager during its top-level side effects (prefetchFingerprint,
68
- // getCryptoWorkerManager().prewarm()).
69
- const { default: ExtensionWeb2 } = await import(
70
- "../extension/ExtensionWeb2.js"
71
- );
72
-
73
- // Fixed browser-fingerprint hex the mocked getFingerprint always returns.
74
- // The real code hashes this to 128 bits, hex-encodes it, and passes the ASCII
75
- // hex bytes as entropy to the mnemonic derivation — so both branches under
76
- // test see the same input regardless of the underlying fingerprint.
77
- const FIXED_FINGERPRINT_HEX = "0x1234567890abcdef1234567890abcdef";
78
-
79
- // Config placeholder — ExtensionWeb2.createAccount ignores fields other than
80
- // what the fingerprint hash + keyring type need.
81
- const config = {
82
- account: { address: "" },
83
- web2: true,
84
- defaultEnvironment: "test",
85
- } as unknown as Parameters<InstanceType<typeof ExtensionWeb2>["getAccount"]>[0];
86
-
87
- describe("ExtensionWeb2.createAccount", () => {
88
- beforeEach(() => {
89
- vi.mocked(getFingerprint).mockReset();
90
- mocks.entropyToKeypair.mockReset();
91
- vi.mocked(getFingerprint).mockResolvedValue(FIXED_FINGERPRINT_HEX);
92
- });
93
-
94
- it("worker branch: derives account from worker-returned keypair bytes", async () => {
95
- // Compute the expected keypair the same way ExtensionWeb2 would — we
96
- // need to feed the mock the exact bytes the worker would produce for
97
- // this fingerprint, otherwise the reconstructed address won't match.
98
- const entropyHex = hexHash(FIXED_FINGERPRINT_HEX, 128).slice(2);
99
- const u8Entropy = stringToU8a(entropyHex);
100
- const mnemonic = entropyToMnemonic(u8Entropy);
101
- const keyring = new Keyring({ type: "sr25519" });
102
- const expectedPair = keyring.addFromMnemonic(mnemonic);
103
-
104
- mocks.entropyToKeypair.mockResolvedValue({
105
- publicKey: expectedPair.publicKey,
106
- secretKey:
107
- // Fish the secret out of the reference pair — this is what the
108
- // real CryptoWorker returns from sr25519FromSeed(seed).
109
- (expectedPair as unknown as { secretKey: Uint8Array }).secretKey ??
110
- new Uint8Array(64),
111
- mnemonic,
112
- });
113
-
114
- const ext = new ExtensionWeb2();
115
- const { account } = await ext.getAccount(config);
116
-
117
- expect(mocks.entropyToKeypair).toHaveBeenCalledTimes(1);
118
- // The worker got called with the entropy ExtensionWeb2 hashed from the
119
- // fingerprint — verifies the argument-shaping code isn't broken.
120
- const [entropyArg] = mocks.entropyToKeypair.mock.calls[0] ?? [];
121
- expect(entropyArg).toBeInstanceOf(Uint8Array);
122
- expect(entropyArg).toEqual(u8Entropy);
123
- // Account address matches the addFromMnemonic-derived one — proves the
124
- // addFromPair wrapping preserves account identity.
125
- expect(account.address).toBe(expectedPair.address);
126
- });
127
-
128
- it("fallback branch: derives account on main thread when worker throws", async () => {
129
- // Simulate a worker failure — CSP block, worker script parse failure,
130
- // task timeout — anything that surfaces as a rejection from
131
- // entropyToKeypair. Fallback path must reach the same account address.
132
- mocks.entropyToKeypair.mockRejectedValue(new Error("Worker unavailable"));
133
-
134
- const entropyHex = hexHash(FIXED_FINGERPRINT_HEX, 128).slice(2);
135
- const u8Entropy = stringToU8a(entropyHex);
136
- const mnemonic = entropyToMnemonic(u8Entropy);
137
- const keyring = new Keyring({ type: "sr25519" });
138
- const expectedPair = keyring.addFromMnemonic(mnemonic);
139
-
140
- const ext = new ExtensionWeb2();
141
- const { account } = await ext.getAccount(config);
142
-
143
- expect(mocks.entropyToKeypair).toHaveBeenCalledTimes(1);
144
- // Fallback still gives us a working keypair with the same address —
145
- // this is the invariant that lets us safely swap in the worker path.
146
- expect(account.address).toBe(expectedPair.address);
147
- });
148
-
149
- it("worker and fallback branches derive the same account address", async () => {
150
- // Direct A/B: one call goes through the worker mock, the next through
151
- // the fallback. Assert the addresses match.
152
- const entropyHex = hexHash(FIXED_FINGERPRINT_HEX, 128).slice(2);
153
- const u8Entropy = stringToU8a(entropyHex);
154
- const mnemonic = entropyToMnemonic(u8Entropy);
155
- const refKeyring = new Keyring({ type: "sr25519" });
156
- const refPair = refKeyring.addFromMnemonic(mnemonic);
157
-
158
- mocks.entropyToKeypair.mockResolvedValueOnce({
159
- publicKey: refPair.publicKey,
160
- secretKey:
161
- (refPair as unknown as { secretKey: Uint8Array }).secretKey ??
162
- new Uint8Array(64),
163
- mnemonic,
164
- });
165
- const workerAccount = await new ExtensionWeb2().getAccount(config);
166
-
167
- mocks.entropyToKeypair.mockRejectedValueOnce(new Error("worker down"));
168
- const fallbackAccount = await new ExtensionWeb2().getAccount(config);
169
-
170
- expect(workerAccount.account.address).toBe(fallbackAccount.account.address);
171
- });
172
- });
@@ -1,146 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- // Unit tests for the CryptoWorker task implementations. The worker itself
16
- // bootstraps under `?worker&inline` in the browser build and can't easily be
17
- // spun up in a Node vitest env, so these tests exercise the same primitives
18
- // the worker's message handler dispatches to (entropyToMnemonic,
19
- // mnemonicToMiniSecret, sr25519FromSeed) and prove they produce a keypair
20
- // equivalent to `keyring.addFromMnemonic` — the shipped-blob invariant that
21
- // `ExtensionWeb2.createAccount`'s worker path relies on.
22
-
23
- import { stringToU8a } from "@polkadot/util";
24
- import { Keyring } from "@prosopo/keyring";
25
- import {
26
- entropyToMnemonic,
27
- mnemonicToMiniSecret,
28
- sr25519FromSeed,
29
- } from "@prosopo/util-crypto";
30
- import { describe, expect, it } from "vitest";
31
-
32
- // Mirrors ExtensionWeb2.createAccount: browserEntropy → hexHash(., 128) →
33
- // slice(2) → 32 ASCII-hex characters → stringToU8a → 32 bytes. Fixed value
34
- // so tests are deterministic (real code path uses the browser fingerprint).
35
- const FIXED_ENTROPY_HEX = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6";
36
-
37
- const entropyBytes = (hex: string): Uint8Array => stringToU8a(hex);
38
-
39
- describe("CryptoWorker task primitives", () => {
40
- describe("entropyToKeypair pipeline", () => {
41
- it("derives an sr25519 keypair from entropy deterministically", () => {
42
- const entropy = entropyBytes(FIXED_ENTROPY_HEX);
43
-
44
- const mnemonic = entropyToMnemonic(entropy);
45
- const seed = mnemonicToMiniSecret(mnemonic);
46
- const { publicKey, secretKey } = sr25519FromSeed(seed);
47
-
48
- expect(publicKey).toBeInstanceOf(Uint8Array);
49
- expect(secretKey).toBeInstanceOf(Uint8Array);
50
- expect(publicKey.length).toBe(32);
51
- expect(secretKey.length).toBe(64);
52
-
53
- // Re-running with the same entropy must yield the same keys —
54
- // otherwise the CryptoWorker cache would be poisoned by a
55
- // non-deterministic derivation.
56
- const again = sr25519FromSeed(
57
- mnemonicToMiniSecret(entropyToMnemonic(entropy)),
58
- );
59
- expect(again.publicKey).toEqual(publicKey);
60
- expect(again.secretKey).toEqual(secretKey);
61
- });
62
-
63
- it("produces the same address as keyring.addFromMnemonic (worker/main-thread parity)", () => {
64
- const entropy = entropyBytes(FIXED_ENTROPY_HEX);
65
- const mnemonic = entropyToMnemonic(entropy);
66
-
67
- // Worker path: entropy → mnemonic → miniSecret → sr25519 → addFromPair
68
- const seed = mnemonicToMiniSecret(mnemonic);
69
- const { publicKey, secretKey } = sr25519FromSeed(seed);
70
- const workerKeyring = new Keyring({ type: "sr25519" });
71
- const workerPair = workerKeyring.addFromPair(
72
- { publicKey, secretKey },
73
- {},
74
- "sr25519",
75
- );
76
-
77
- // Legacy main-thread path: keyring.addFromMnemonic
78
- const mainKeyring = new Keyring({ type: "sr25519" });
79
- const mainPair = mainKeyring.addFromMnemonic(mnemonic);
80
-
81
- // Addresses must match exactly — this is the invariant that guarantees
82
- // swapping addFromMnemonic → addFromPair(entropyToKeypair(...)) is a
83
- // behavioural no-op for account identity.
84
- expect(workerPair.address).toBe(mainPair.address);
85
- expect(workerPair.publicKey).toEqual(mainPair.publicKey);
86
- expect(workerPair.type).toBe(mainPair.type);
87
- });
88
-
89
- it("signatures produced by addFromPair verify against the addFromMnemonic public key", () => {
90
- const entropy = entropyBytes(FIXED_ENTROPY_HEX);
91
- const mnemonic = entropyToMnemonic(entropy);
92
- const seed = mnemonicToMiniSecret(mnemonic);
93
- const { publicKey, secretKey } = sr25519FromSeed(seed);
94
-
95
- const workerKeyring = new Keyring({ type: "sr25519" });
96
- const workerPair = workerKeyring.addFromPair(
97
- { publicKey, secretKey },
98
- {},
99
- "sr25519",
100
- );
101
- const mainKeyring = new Keyring({ type: "sr25519" });
102
- const mainPair = mainKeyring.addFromMnemonic(mnemonic);
103
-
104
- const message = stringToU8a("prosopo-test-message");
105
- const workerSig = workerPair.sign(message);
106
- const mainSig = mainPair.sign(message);
107
-
108
- expect(workerSig.length).toBe(64);
109
- expect(mainSig.length).toBe(64);
110
- // sr25519 signatures are non-deterministic, so we can't byte-compare;
111
- // but each signature must verify against BOTH pairs' public keys.
112
- expect(mainPair.verify(message, workerSig, mainPair.publicKey)).toBe(
113
- true,
114
- );
115
- expect(workerPair.verify(message, mainSig, workerPair.publicKey)).toBe(
116
- true,
117
- );
118
- });
119
- });
120
-
121
- describe("entropyToMnemonic (existing worker task, unchanged)", () => {
122
- it("produces a 24-word BIP39 mnemonic from 32 bytes of entropy", () => {
123
- const entropy = entropyBytes(FIXED_ENTROPY_HEX);
124
- const mnemonic = entropyToMnemonic(entropy);
125
- const words = mnemonic.split(" ");
126
- // 32 bytes of entropy = 256 bits → 24 BIP39 words.
127
- expect(words.length).toBe(24);
128
- for (const w of words) {
129
- expect(w).toMatch(/^[a-z]+$/);
130
- }
131
- });
132
-
133
- it("is deterministic for a given entropy", () => {
134
- const entropy = entropyBytes(FIXED_ENTROPY_HEX);
135
- expect(entropyToMnemonic(entropy)).toBe(entropyToMnemonic(entropy));
136
- });
137
-
138
- it("rejects entropy that isn't 16/20/24/28/32 bytes", () => {
139
- // bip39 accepts only these sizes — CryptoWorker relies on the caller
140
- // passing valid entropy (already-hashed to 32 bytes in ExtensionWeb2).
141
- expect(() => entropyToMnemonic(new Uint8Array(15))).toThrow();
142
- expect(() => entropyToMnemonic(new Uint8Array(17))).toThrow();
143
- expect(() => entropyToMnemonic(new Uint8Array(33))).toThrow();
144
- });
145
- });
146
- });
@@ -1,192 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import CryptoWorkerConstructor from "./cryptoWorker.js?worker&inline";
16
-
17
- interface CryptoWorkerMessage {
18
- taskId: string;
19
- task: string;
20
- data: Record<string, string | Uint8Array>;
21
- }
22
-
23
- export interface KeypairBytes {
24
- publicKey: Uint8Array;
25
- secretKey: Uint8Array;
26
- }
27
-
28
- export interface EntropyToKeypairResult extends KeypairBytes {
29
- mnemonic: string;
30
- }
31
-
32
- type CryptoWorkerResult = string | KeypairBytes | EntropyToKeypairResult;
33
-
34
- interface CryptoWorkerResponse {
35
- taskId: string;
36
- result?: CryptoWorkerResult;
37
- error?: string;
38
- }
39
-
40
- /**
41
- * Manages Web Worker for cryptographic operations to prevent blocking the main thread
42
- */
43
- export class CryptoWorkerManager {
44
- private worker: Worker | null = null;
45
- private isInitializing = false;
46
-
47
- /**
48
- * Initialize the worker. No test round-trip — construction failure throws
49
- * synchronously (caught below); post-construction failures surface via
50
- * `worker.onerror` (cleans up so the next call retries); anything that
51
- * gets past both surfaces via `runTask`'s timeout + reject path, which
52
- * already falls back to main-thread crypto. The prior `testWorker()` ping
53
- * was a Blob-URL-era defensive check that no longer earns its ~30–80ms
54
- * critical-path cost under Vite's `?worker&inline` constructor.
55
- */
56
- private initWorker(): void {
57
- if (this.worker || this.isInitializing) {
58
- return;
59
- }
60
-
61
- this.isInitializing = true;
62
-
63
- try {
64
- this.worker = new CryptoWorkerConstructor({ type: "module" });
65
-
66
- this.worker.onerror = () => {
67
- // Terminate the failed worker; the next runTask will re-init.
68
- this.cleanup();
69
- };
70
- } catch {
71
- this.cleanup();
72
- } finally {
73
- this.isInitializing = false;
74
- }
75
- }
76
-
77
- /**
78
- * Eagerly spawn the worker so the first runTask() doesn't pay the
79
- * worker-spawn + module-parse cost. Synchronous now — construction is
80
- * fire-and-forget with cleanup via `worker.onerror`.
81
- */
82
- prewarm(): void {
83
- this.initWorker();
84
- }
85
-
86
- /**
87
- * Run a task in the Web Worker
88
- */
89
- async runTask<T>(
90
- task: string,
91
- data: Record<string, string | Uint8Array>,
92
- ): Promise<T> {
93
- this.initWorker();
94
-
95
- if (!this.worker) {
96
- throw new Error("Failed to initialize worker");
97
- }
98
-
99
- const worker = this.worker;
100
-
101
- return new Promise((resolve, reject) => {
102
- const taskId = Math.random().toString(36).substr(2, 9);
103
- const timeoutId = setTimeout(() => {
104
- // Clean up listeners on timeout
105
- worker.removeEventListener("message", handleMessage);
106
- worker.removeEventListener("error", handleError);
107
- reject(new Error(`Worker task ${task} timeout (Task ID: ${taskId})`));
108
- }, 10000);
109
-
110
- const handleMessage = (event: MessageEvent<CryptoWorkerResponse>) => {
111
- if (event.data.taskId === taskId) {
112
- clearTimeout(timeoutId);
113
- worker.removeEventListener("message", handleMessage);
114
- worker.removeEventListener("error", handleError);
115
-
116
- if (event.data.error) {
117
- // Reject with the error message sent back by the worker
118
- reject(new Error(`Worker task failed: ${event.data.error}`));
119
- } else {
120
- resolve(event.data.result as T); // Type assertion for clean resolution
121
- }
122
- }
123
- };
124
-
125
- const handleError = (error: ErrorEvent) => {
126
- clearTimeout(timeoutId);
127
- worker.removeEventListener("message", handleMessage);
128
- worker.removeEventListener("error", handleError);
129
- // Reject on global worker error while task is pending
130
- reject(new Error(`Worker error during task: ${error.message}`));
131
- };
132
-
133
- worker.addEventListener("message", handleMessage);
134
- worker.addEventListener("error", handleError);
135
-
136
- const message: CryptoWorkerMessage = { taskId, task, data };
137
- worker.postMessage(message);
138
- });
139
- }
140
-
141
- /**
142
- * Convert entropy to mnemonic using Web Worker
143
- */
144
- async entropyToMnemonic(entropy: Uint8Array): Promise<string> {
145
- return this.runTask<string>("entropyToMnemonic", { entropy });
146
- }
147
-
148
- /**
149
- * Fused: entropy → mnemonic → sr25519 keypair in a single worker
150
- * round-trip. Callers that don't need the mnemonic separately should
151
- * prefer this over two `entropyToMnemonic + keypairFromMnemonic` hops —
152
- * saves one postMessage transit (~30–80ms on constrained hardware / under
153
- * throttle) on the frictionless critical path. sr25519 derivation is a
154
- * ~500ms main-thread cost otherwise.
155
- */
156
- async entropyToKeypair(entropy: Uint8Array): Promise<EntropyToKeypairResult> {
157
- return this.runTask<EntropyToKeypairResult>("entropyToKeypair", {
158
- entropy,
159
- });
160
- }
161
-
162
- /**
163
- * Clean up worker resources
164
- */
165
- private cleanup(): void {
166
- if (this.worker) {
167
- this.worker.terminate();
168
- }
169
- this.worker = null;
170
- this.isInitializing = false;
171
- }
172
-
173
- /**
174
- * Public dispose method
175
- */
176
- public dispose(): void {
177
- this.cleanup();
178
- }
179
- }
180
-
181
- // Create a singleton instance for reuse
182
- let workerManagerInstance: CryptoWorkerManager | null = null;
183
-
184
- /**
185
- * Get the singleton CryptoWorkerManager instance
186
- */
187
- export function getCryptoWorkerManager(): CryptoWorkerManager {
188
- if (!workerManagerInstance) {
189
- workerManagerInstance = new CryptoWorkerManager();
190
- }
191
- return workerManagerInstance;
192
- }
@@ -1,98 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- // cryptoWorker.ts
16
-
17
- import {
18
- entropyToMnemonic,
19
- mnemonicToMiniSecret,
20
- sr25519FromSeed,
21
- } from "@prosopo/util-crypto";
22
-
23
- interface CryptoWorkerMessage {
24
- taskId: string;
25
- task: "entropyToMnemonic" | "entropyToKeypair" | "test";
26
- data: {
27
- entropy?: Uint8Array;
28
- };
29
- }
30
-
31
- interface EntropyToKeypairResult {
32
- mnemonic: string;
33
- publicKey: Uint8Array;
34
- secretKey: Uint8Array;
35
- }
36
-
37
- interface CryptoWorkerResponse {
38
- taskId: string;
39
- result?: string | EntropyToKeypairResult;
40
- error?: string;
41
- }
42
-
43
- // The main message listener runs in the worker's global scope
44
- self.addEventListener(
45
- "message",
46
- async (event: MessageEvent<CryptoWorkerMessage>) => {
47
- const { taskId, task, data } = event.data;
48
-
49
- try {
50
- let result: string | EntropyToKeypairResult;
51
-
52
- switch (task) {
53
- case "test":
54
- // Respond to test message to confirm worker is ready
55
- result = "ready";
56
- break;
57
- case "entropyToMnemonic":
58
- if (!data.entropy) {
59
- throw new Error("Entropy data is required");
60
- }
61
- result = entropyToMnemonic(data.entropy);
62
- break;
63
- case "entropyToKeypair":
64
- // Fused: entropy → mnemonic → sr25519 keypair in one worker
65
- // round-trip. Saves the postMessage transit on the frictionless
66
- // critical path (~30–80ms per hop on constrained hardware).
67
- // Called by ExtensionWeb2.createAccount when the caller doesn't
68
- // need the mnemonic string for any other purpose than deriving
69
- // the pair. sr25519 derivation is a scalar multiplication on
70
- // Ristretto25519 — ~500ms mid-tier, ~800ms mobile — and was the
71
- // single biggest main-thread cost pre-worker.
72
- if (!data.entropy) {
73
- throw new Error("Entropy data is required");
74
- }
75
- result = processEntropyToKeypair(data.entropy);
76
- break;
77
- default:
78
- throw new Error(`Unknown task: ${task}`);
79
- }
80
-
81
- const response: CryptoWorkerResponse = { taskId, result };
82
- self.postMessage(response);
83
- } catch (error) {
84
- const response: CryptoWorkerResponse = {
85
- taskId,
86
- error: error instanceof Error ? error.message : "Unknown error",
87
- };
88
- self.postMessage(response);
89
- }
90
- },
91
- );
92
-
93
- function processEntropyToKeypair(entropy: Uint8Array): EntropyToKeypairResult {
94
- const mnemonic = entropyToMnemonic(entropy);
95
- const seed = mnemonicToMiniSecret(mnemonic);
96
- const { publicKey, secretKey } = sr25519FromSeed(seed);
97
- return { mnemonic, publicKey, secretKey };
98
- }