]> git.codecow.com Git - libnemo.git/commitdiff
Implement notification to wallet of vault lock or unlock.
authorChris Duncan <chris@zoso.dev>
Tue, 9 Sep 2025 19:32:22 +0000 (12:32 -0700)
committerChris Duncan <chris@zoso.dev>
Tue, 9 Sep 2025 19:32:22 +0000 (12:32 -0700)
src/lib/vault/index.ts
src/lib/vault/vault-timer.ts
src/lib/vault/vault-worker.ts
src/lib/wallet/index.ts
src/lib/wallet/lock.ts
src/lib/wallet/unlock.ts
src/types.d.ts
test/test.lock-unlock.mjs

index 646fad6e75aa52c6dc9c37118d08328b0bf3d2a8..d7636e9bfc4eeb36d67ca75a93a63496a0599ca2 100644 (file)
@@ -21,6 +21,7 @@ export class Vault {
 
        #job?: Task
        #isIdle: boolean
+       #isLocked: boolean
        #isTerminated: boolean
        #queue: Task[] = []
        #url: string
@@ -28,6 +29,7 @@ export class Vault {
 
        constructor () {
                this.#isIdle = true
+               this.#isLocked = true
                this.#isTerminated = false
                this.#queue = []
                this.#url = URL.createObjectURL(new Blob([blob], { type: 'text/javascript' }))
@@ -46,6 +48,8 @@ export class Vault {
                Vault.#instances.push(this)
        }
 
+       get isLocked (): boolean { return this.#isLocked }
+
        request<T extends Data> (data: NamedData): Promise<NamedData<T>> {
                return new Promise((resolve, reject): void => {
                        if (this.#isTerminated) {
@@ -89,12 +93,16 @@ export class Vault {
        }
 
        #report (results: any): void {
+               if (results === 'locked' || results === 'unlocked') {
+                       this.#isLocked = results === 'locked'
+                       return
+               }
                if (this.#job == null) {
                        throw new Error('Worker returned results but had nowhere to report it.')
                }
                const { resolve, reject } = this.#job
                try {
-                       if (results.error != null) {
+                       if (results?.error != null) {
                                reject(results)
                        } else {
                                resolve(results)
index 96af34ad5152564730549dc0873224dd0f6f58b8..35efdd1d4f543ec917e05a4c655b183670a8dfad 100644 (file)
@@ -6,16 +6,19 @@ export class VaultTimer {
        #elapsed: number = 0
        #isPaused: boolean = false
        #start: number
+       #ticker: number | NodeJS.Timeout
        #timeout: number | NodeJS.Timeout
 
        constructor (f: () => any, t: number) {
+               this.#ticker = setInterval(() => { }, 1000)
                this.#f = f
                this.#start = performance.now()
-               this.#timeout = setTimeout(f, t)
+               this.#timeout = setTimeout(this.#f, t)
        }
 
        pause () {
                if (!this.#isPaused) {
+                       clearInterval(this.#ticker)
                        clearTimeout(this.#timeout)
                        this.#elapsed = performance.now() - this.#start
                        this.#isPaused = true
@@ -24,6 +27,7 @@ export class VaultTimer {
 
        resume () {
                if (this.#isPaused) {
+                       this.#ticker = setInterval(() => { }, 1000)
                        this.#start = performance.now()
                        this.#timeout = setTimeout(this.#f, this.#elapsed)
                        this.#isPaused = false
index 5cce6657bc02d7c4cceea1c68e3cd2d7b7d1fe25..9187a7765c297023c886e617e7b2167bd4925ebf 100644 (file)
@@ -32,7 +32,7 @@ export class VaultWorker {
                        const action = this.#parseAction(data)
                        const keySalt = this.#parseKeySalt(action, data)
                        Passkey.create(action, keySalt, data)
-                               .then((key: CryptoKey | undefined): Promise<NamedData> => {
+                               .then((key: CryptoKey | undefined): Promise<NamedData | void> => {
                                        const type = this.#parseType(action, data)
                                        const iv = this.#parseIv(action, data)
                                        const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message } = this.#extractData(action, data)
@@ -72,9 +72,11 @@ export class VaultWorker {
                                })
                                .then(result => {
                                        const transfer = []
-                                       for (const k of Object.keys(result)) {
-                                               if (result[k] instanceof ArrayBuffer || result[k] instanceof CryptoKey) {
-                                                       transfer.push(result[k])
+                                       if (result) {
+                                               for (const k of Object.keys(result)) {
+                                                       if (result[k] instanceof ArrayBuffer || result[k] instanceof CryptoKey) {
+                                                               transfer.push(result[k])
+                                                       }
                                                }
                                        }
                                        //@ts-expect-error
@@ -183,12 +185,13 @@ export class VaultWorker {
                        .finally(() => this.lock())
        }
 
-       lock (): NamedData<boolean> {
+       lock (): void {
                this.#mnemonic = undefined
                this.#seed = undefined
                this.#locked = true
                this.#timeout?.pause()
-               return { isLocked: this.#locked }
+               BROWSER: postMessage('locked')
+               NODE: this.#parentPort?.postMessage('locked')
        }
 
        /**
@@ -228,7 +231,7 @@ export class VaultWorker {
        /**
        * Decrypts the input and sets the seed and, if it is included, the mnemonic.
        */
-       unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<NamedData<boolean>> {
+       unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<void> {
                if (type == null) {
                        throw new TypeError('Wallet type is required')
                }
@@ -253,8 +256,9 @@ export class VaultWorker {
                                this.#seed = seed
                                this.#mnemonic = mnemonic
                                this.#locked = false
-                               this.#timeout = new VaultTimer(() => this.lock(), 120000)
-                               return { isUnlocked: !this.#locked }
+                               this.#timeout = new VaultTimer(this.lock.bind(this), 120000)
+                               BROWSER: postMessage('unlocked')
+                               NODE: this.#parentPort?.postMessage('unlocked')
                        })
                        .catch(err => {
                                console.error(err)
index 6f5ecfe971b8eb24fb2e7223e3f99db312363c51..751e47d52e85298ff03f9b25d494ac81633ffedd 100644 (file)
@@ -135,6 +135,13 @@ export class Wallet {
        */\r
        get id (): string { return this.#id }\r
 \r
+       /**\r
+       * @returns True if the wallet is locked, else false\r
+       */\r
+       get isLocked (): boolean {\r
+               return this.#vault.isLocked\r
+       }\r
+\r
        /**\r
        * Algorithm or device used to create wallet and derive accounts.\r
        */\r
index 3f3e3ca93d7c9f8a7342f25241b6ddc609e1090e..7d0922006b387f4f3e3e145642ae57bf70d3c1f9 100644 (file)
@@ -17,10 +17,10 @@ export async function _lock (wallet: Wallet, vault: Vault): Promise<void> {
                                }
                        })
                } else {
-                       const { isLocked } = await vault.request<boolean>({
+                       await vault.request({
                                action: 'lock'
                        })
-                       if (!isLocked) {
+                       if (!wallet.isLocked) {
                                throw new Error('Lock request to Vault failed')
                        }
                }
index f46e3fd769c9d1123514891f6eab2f86d8269e90..c53d5fd5100f7bbb0178f7b78bca6fedf56f58a2 100644 (file)
@@ -20,7 +20,7 @@ export async function _unlock (wallet: Wallet, vault: Vault, password: unknown):
                                throw new TypeError('Password must be a string')
                        }
                        const { iv, salt, encrypted } = await _get(wallet.id)
-                       const { isUnlocked } = await vault.request<boolean>({
+                       await vault.request({
                                action: 'unlock',
                                type: wallet.type,
                                password: utf8.toBuffer(password),
@@ -28,7 +28,7 @@ export async function _unlock (wallet: Wallet, vault: Vault, password: unknown):
                                keySalt: salt,
                                encrypted
                        })
-                       if (!isUnlocked) {
+                       if (wallet.isLocked) {
                                throw new Error('Unlock request to Vault failed')
                        }
                }
index d4f776d3d9139de60fba71e62d19094ebfcc9037..4d2d32103af9264a6e7d2ae24cfc08ff4896f4b5 100644 (file)
@@ -667,6 +667,10 @@ export declare class Wallet {
        */
        get id (): string
        /**
+       * @returns True if the wallet is locked, else false
+       */
+       get isLocked (): boolean
+       /**
        * Algorithm or device used to create wallet and derive accounts.
        */
        get type (): WalletType
index 14386df9e90191a33e0f4aaa986eefd3d44dfeed..1618ad9c8fa9c219608e646b5286133273bed80f 100644 (file)
@@ -138,9 +138,12 @@ await Promise.all([
                        await assert.resolves(wallet.destroy())\r
                })\r
 \r
-               await test('wallet automatic lock resets after user activity', { skip: true }, async () => {\r
+               await test('wallet automatic lock resets after user activity', { skip: false }, async () => {\r
+                       console.log('Starting autolock test...')\r
                        const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
+                       assert.equal(wallet.isLocked, true)\r
                        await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+                       assert.equal(wallet.isLocked, false)\r
 \r
                        assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC))\r
                        assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
@@ -149,6 +152,7 @@ await Promise.all([
                                console.log('Waiting 1 minute...')\r
                                setTimeout(async () => {\r
                                        // should still be unlocked\r
+                                       assert.equal(wallet.isLocked, false)\r
                                        const account = await wallet.account(0)\r
                                        assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0)\r
                                        resolve(null)\r
@@ -159,6 +163,7 @@ await Promise.all([
                                console.log('Timer should be reset, waiting 1 minute...')\r
                                setTimeout(async () => {\r
                                        // should still be unlocked from account() reset and not initial unlock\r
+                                       assert.equal(wallet.isLocked, false)\r
                                        assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC))\r
                                        assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
                                        resolve(null)\r
@@ -169,6 +174,7 @@ await Promise.all([
                                console.log('Timer should not be reset by verify, waiting 1 minute...')\r
                                setTimeout(async () => {\r
                                        // should be locked from account() reset and not reset by verify()\r
+                                       assert.equal(wallet.isLocked, true)\r
                                        await assert.rejects(wallet.verify(NANO_TEST_VECTORS.MNEMONIC))\r
                                        await assert.rejects(wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
                                        await assert.resolves(wallet.destroy())\r