]> git.codecow.com Git - nano25519.git/commitdiff
Split async host and worker into separate files and export worker as string with...
authorChris Duncan <chris@codecow.com>
Thu, 20 Aug 2026 00:04:42 +0000 (17:04 -0700)
committerChris Duncan <chris@codecow.com>
Thu, 20 Aug 2026 00:04:42 +0000 (17:04 -0700)
esbuild/config.mjs
esbuild/inject/worker.mjs [new file with mode: 0644]
src/async.ts
src/lib/host.ts [new file with mode: 0644]
src/lib/worker.ts

index e552886b7dbd6ef82e54bd226079bf3d41dd2f97..bbd1dc9905a68e721ab09948d4a48b6aabd73ae8 100644 (file)
@@ -1,6 +1,7 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
+import { browserWorker, nodeWorker } from './inject/worker.mjs'
 /**
  * @type import("esbuild").BuildOptions
  */
@@ -27,6 +28,9 @@ export const browserOptions = {
                { in: 'src/index.ts', out: 'browser' }
        ],
        dropLabels: ['NODE'],
+       define: {
+               'NANO25519_WORKER': browserWorker
+       },
 }
 
 /**
@@ -43,4 +47,7 @@ export const nodeOptions = {
        entryNames: '[name]',
        external: ['node:worker_threads'],
        dropLabels: ['BROWSER'],
+       define: {
+               'NANO25519_WORKER': nodeWorker
+       },
 }
diff --git a/esbuild/inject/worker.mjs b/esbuild/inject/worker.mjs
new file mode 100644 (file)
index 0000000..5d5602a
--- /dev/null
@@ -0,0 +1,34 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { build } from 'esbuild'
+
+/**
+ * @type {import('esbuild').BuildOptions}
+ */
+const workerOptions = {
+       bundle: true,
+       loader: {
+               '.wasm': 'binary',
+       },
+       entryPoints: ['./src/lib/worker.ts'],
+       format: 'esm',
+       legalComments: 'inline',
+       write: false,
+       drop: ['console', 'debugger'],
+       minify: true,
+}
+
+export const browserWorker = JSON.stringify((await build({
+       ...workerOptions,
+       platform: 'browser',
+       target: 'es2022',
+       dropLabels: ['NODE'],
+})).outputFiles?.[0].text)
+
+export const nodeWorker = JSON.stringify((await build({
+       ...workerOptions,
+       platform: 'node',
+       target: 'node22',
+       dropLabels: ['BROWSER'],
+})).outputFiles?.[0].text)
index d7eb04436f0330cc8a5f983d0b90a7551c330c63..fe8bdac03fdf79688d9b27d8be3c6cac52ed10b9 100644 (file)
@@ -1,7 +1,7 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-import { run } from './lib/worker'
+import { run } from './lib/host'
 
 /**
  * Asynchronous Nano public key derivation using WebAssembly.
diff --git a/src/lib/host.ts b/src/lib/host.ts
new file mode 100644 (file)
index 0000000..3548262
--- /dev/null
@@ -0,0 +1,168 @@
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { UUID } from 'node:crypto'
+import { Worker as NodeWorker } from 'node:worker_threads'
+
+type Action = 'derive' | 'sign' | 'start' | 'verify'
+
+//@ts-expect-error
+const nano25519_worker = NANO25519_WORKER
+
+/**
+ * Host code for asynchronous Web Worker
+ */
+let isWorkerReady: boolean = false
+let starting: Promise<unknown> | undefined
+let tasks: Map<string, Parameters<ConstructorParameters<PromiseConstructor>[0]>> = new Map()
+let worker: Worker | NodeWorker
+let url: string
+
+function isBytes (a: unknown): a is Uint8Array<ArrayBuffer> {
+       return a instanceof Uint8Array && a.buffer instanceof ArrayBuffer
+}
+
+// Create worker module
+function init (): void {
+       try {
+               BROWSER: {
+                       if (url) URL.revokeObjectURL(url)
+                       url = URL.createObjectURL(new Blob([nano25519_worker], { type: 'text/javascript' }))
+                       worker = new Worker(url, { type: 'module' })
+                       worker.onmessage = report
+                       worker.onerror = reset
+               }
+               NODE: {
+                       worker = new NodeWorker(nano25519_worker, {
+                               eval: true,
+                               stderr: false,
+                               stdout: false
+                       })
+                       worker.on('message', report)
+                       worker.on('error', reset)
+               }
+               console.log(`nano25519/async initialized`)
+               isWorkerReady = true
+       } catch (e: any) {
+               isWorkerReady = false
+               throw new Nano25519WorkerError(e)
+       }
+}
+
+// Helper for environment-specific messaging to worker
+function post (id: UUID, data: Record<string, any> & Record<'action', Action>, transfer?: ArrayBuffer[]): void {
+       data.id = id
+       BROWSER: worker.postMessage(data, transfer)
+       NODE: worker.postMessage({ data }, transfer)
+}
+
+// Parse and validate worker message
+function report (msg: { data: Record<string, unknown> }): void {
+       const { data } = msg
+       if (!('id' in data) || typeof data.id !== 'string') return
+
+       const executor = tasks.get(data.id)
+       if (executor == null) return
+       const [ok, err] = executor
+       tasks.delete(data.id)
+
+       const { result } = data
+       console.log('received result from worker')
+       if (typeof result !== 'boolean' && typeof result !== 'string' && !isBytes(result)) {
+               return err(`expected boolean, string, or bytes; received ${result?.constructor?.name ?? typeof result} '${result}`)
+       }
+       return ok(result)
+}
+
+// Reconstruct worker when errors occur
+function reset (): void {
+       console.warn(`nano25519 encountered an error. Reinitializing...`)
+       starting = undefined
+       for (const [_, err] of tasks.values()) {
+               err(new Nano25519WorkerError('worker reset, try again'))
+       }
+       tasks.clear()
+       worker.terminate()
+       init()
+}
+
+// Check that the worker is running and listening before sending messages
+async function start (): Promise<unknown> {
+       return starting ??= new Promise((resolve, reject): void => {
+               console.log('starting worker')
+               if (!isWorkerReady) init()
+               const id = crypto.randomUUID()
+               tasks.set(id, [resolve, reject])
+               post(id, { action: 'start' })
+       })
+}
+
+// Send command and relevant data to nano25519 worker
+async function dispatch (data: Record<'action', Action> & Record<string, string | ArrayBuffer | Uint8Array<ArrayBuffer>>): Promise<unknown> {
+       const id = crypto.randomUUID()
+       const transfer: ArrayBuffer[] = []
+       for (let k of Object.keys(data)) {
+               if (isBytes(data[k])) {
+                       data[k] = data[k].slice().buffer
+                       transfer.push(data[k])
+               }
+       }
+       console.log('sending data to worker')
+       return new Promise((resolve, reject) => {
+               tasks.set(id, [resolve, reject])
+               post(id, data, transfer)
+       })
+}
+
+export async function run (data: Record<'action', 'derive'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
+export async function run (data: Record<'action', 'sign'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
+export async function run (data: Record<'action', 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean>
+export async function run (data: Record<'action', 'derive' | 'sign' | 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean | string | Uint8Array<ArrayBuffer>> {
+       try {
+               const result = await start()
+               if (result === 'listening') {
+                       console.log('worker listening')
+               } else if (typeof result === 'string') {
+                       throw new Error(result)
+               } else {
+                       throw new Error('unknown error')
+               }
+       } catch (e: any) {
+               starting = undefined
+               throw new Nano25519WorkerError(e)
+       }
+
+       const result = await dispatch(data)
+       switch (data.action) {
+               case 'derive': {
+                       if ((isBytes(result) && result.byteLength === 32) || (typeof result === 'string' && /^[0-9a-f]{64}$/i.test(result))) {
+                               return result
+                       }
+                       break
+               }
+               case 'sign': {
+                       if ((isBytes(result) && result.byteLength === 64) || (typeof result === 'string' && /^[0-9a-f]{128}$/i.test(result))) {
+                               return result
+                       }
+                       break
+               }
+               case 'verify': {
+                       if (typeof result === 'boolean') {
+                               return result
+                       }
+                       break
+               }
+       }
+       throw new Nano25519ResultError(data.action, result)
+}
+
+class Nano25519WorkerError extends Error {
+       constructor (cause?: unknown) {
+               super(`async process error`, { cause })
+       }
+}
+class Nano25519ResultError extends Error {
+       constructor (action: string, cause?: unknown) {
+               super(`${action} result invalid`, { cause })
+       }
+}
index ecb732f3b6638749bdf945c24f6e34b73e9cdacf..01a2f6d78f1a184b346f5232f67135448c20da0e 100644 (file)
@@ -2,10 +2,10 @@
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
 import { UUID } from 'node:crypto'
-import { MessagePort as NodeMessagePort, Worker as NodeWorker } from 'node:worker_threads'
+import { MessagePort as NodeMessagePort } from 'node:worker_threads'
+import { nano25519_init } from './nano25519'
 //@ts-expect-error
 import nano25519_wasm from '../../build/nano25519.wasm'
-import { nano25519_init } from './nano25519'
 
 type Action = 'derive' | 'sign' | 'start' | 'verify'
 
@@ -19,244 +19,85 @@ type Data = {
        signature?: string
 }
 
-const nano25519_worker_init = ({ derive, sign, verify }: ReturnType<typeof nano25519_init>) => {
-       let host: NodeMessagePort | null = null
+const { derive, sign, verify } = nano25519_init(nano25519_wasm)
 
-       /**
-        * Parses inbound data when nano25519 is started as a Web Worker. Only called
-        * by functions in `async` module.
-        * @param {object} message.data - Worker commands and related data
-        */
-       function listener (message: unknown): void {
-               NODE: if (host == null) return queueMicrotask(() => listener(message))
-               if (message == null
-                       || typeof message !== 'object'
-                       || !('data' in message)
-                       || message.data == null
-                       || typeof message.data !== 'object'
-                       || !('id' in message.data)
-                       || typeof message.data.id !== 'string'
-                       || !('action' in message.data)
-                       || typeof message.data.action !== 'string'
-               ) return
-               let result: undefined | boolean | string | Uint8Array<ArrayBuffer>
-               let id: undefined | UUID
-               try {
-                       const data: Data = message.data as object & { id: UUID, action: Action }
-                       id = data.id
-
-                       switch (data.action) {
-                               case 'start': {
-                                       result = 'listening'
-                                       break
-                               }
-                               case 'derive': {
-                                       const { privateKey } = data
-                                       const publicKey = derive(privateKey)
-                                       if (publicKey == null) {
-                                               throw new TypeError('Invalid public key from WASM derive()')
-                                       }
-                                       result = publicKey
-                                       break
-                               }
-                               case 'sign': {
-                                       const { message, secretKey } = data
-                                       const signature = sign(message, secretKey)
-                                       if (signature == null) {
-                                               throw new TypeError('Invalid signature from WASM sign()')
-                                       }
-                                       result = signature
-                                       break
-                               }
-                               case 'verify': {
-                                       const { message, publicKey, signature } = data
-                                       const verification = verify(signature, message, publicKey)
-                                       if (verification == null) {
-                                               throw new TypeError('Invalid verification from WASM verify()')
-                                       }
-                                       result = verification
-                                       break
-                               }
-                               default: {
-                                       throw new TypeError(`Invalid action '${data.action}'`)
-                               }
-                       }
-               } catch (err: any) {
-                       result = JSON.stringify(err?.message ?? err ?? 'unknown error in nano25519 worker listener')
-               } finally {
-                       const data = { id, result }
-                       BROWSER: postMessage(data)
-                       NODE: host?.postMessage({ data })
-               }
-       }
-       BROWSER: addEventListener('message', listener)
-       NODE: {
-               if (host == null) {
-                       import('node:worker_threads')
-                               .then(({ parentPort }): void => {
-                                       host = parentPort
-                                       host?.on('message', listener)
-                               })
-               }
-       }
-}
-
-const nano25519_worker = `;(${nano25519_worker_init})((${nano25519_init})([${nano25519_wasm}]));`
+let host: NodeMessagePort | null = null
 
 /**
- * Host code for asynchronous Web Worker
+ * Parses inbound data when nano25519 is started as a Web Worker. Only called
+ * by functions in `async` module.
+ * @param {object} message.data - Worker commands and related data
  */
-let isWorkerReady: boolean = false
-let starting: Promise<unknown> | undefined
-let tasks: Map<string, Parameters<ConstructorParameters<PromiseConstructor>[0]>> = new Map()
-let worker: Worker | NodeWorker
-let url: string
-
-function isBytes (a: unknown): a is Uint8Array<ArrayBuffer> {
-       return a instanceof Uint8Array && a.buffer instanceof ArrayBuffer
-}
-
-// Create worker module
-function init (): void {
+function listener (message: unknown): void {
+       NODE: if (host == null) return queueMicrotask(() => listener(message))
+       if (message == null
+               || typeof message !== 'object'
+               || !('data' in message)
+               || message.data == null
+               || typeof message.data !== 'object'
+               || !('id' in message.data)
+               || typeof message.data.id !== 'string'
+               || !('action' in message.data)
+               || typeof message.data.action !== 'string'
+       ) return
+       let result: undefined | boolean | string | Uint8Array<ArrayBuffer>
+       let id: undefined | UUID
        try {
-               BROWSER: {
-                       if (url) URL.revokeObjectURL(url)
-                       url = URL.createObjectURL(new Blob([nano25519_worker], { type: 'text/javascript' }))
-                       worker = new Worker(url, { type: 'module' })
-                       worker.onmessage = report
-                       worker.onerror = reset
-               }
-               NODE: {
-                       worker = new NodeWorker(nano25519_worker, {
-                               eval: true,
-                               stderr: false,
-                               stdout: false
-                       })
-                       worker.on('message', report)
-                       worker.on('error', reset)
-               }
-               console.log(`nano25519/async initialized`)
-               isWorkerReady = true
-       } catch (e: any) {
-               isWorkerReady = false
-               throw new Nano25519WorkerError(e)
-       }
-}
-
-// Helper for environment-specific messaging to worker
-function post (id: UUID, data: Record<string, any> & Record<'action', Action>, transfer?: ArrayBuffer[]): void {
-       data.id = id
-       BROWSER: worker.postMessage(data, transfer)
-       NODE: worker.postMessage({ data }, transfer)
-}
-
-// Parse and validate worker message
-function report (msg: { data: Record<string, unknown> }): void {
-       const { data } = msg
-       if (!('id' in data) || typeof data.id !== 'string') return
-
-       const executor = tasks.get(data.id)
-       if (executor == null) return
-       const [ok, err] = executor
-       tasks.delete(data.id)
+               const data: Data = message.data as object & { id: UUID, action: Action }
+               id = data.id
 
-       const { result } = data
-       console.log('received result from worker')
-       if (typeof result !== 'boolean' && typeof result !== 'string' && !isBytes(result)) {
-               return err(`expected boolean, string, or bytes; received ${result?.constructor?.name ?? typeof result} '${result}`)
-       }
-       return ok(result)
-}
-
-// Reconstruct worker when errors occur
-function reset (): void {
-       console.warn(`nano25519 encountered an error. Reinitializing...`)
-       starting = undefined
-       for (const [_, err] of tasks.values()) {
-               err(new Nano25519WorkerError('worker reset, try again'))
-       }
-       tasks.clear()
-       worker.terminate()
-       init()
-}
-
-// Check that the worker is running and listening before sending messages
-async function start (): Promise<unknown> {
-       return starting ??= new Promise((resolve, reject): void => {
-               console.log('starting worker')
-               if (!isWorkerReady) init()
-               const id = crypto.randomUUID()
-               tasks.set(id, [resolve, reject])
-               post(id, { action: 'start' })
-       })
-}
-
-// Send command and relevant data to nano25519 worker
-async function dispatch (data: Record<'action', Action> & Record<string, string | ArrayBuffer | Uint8Array<ArrayBuffer>>): Promise<unknown> {
-       const id = crypto.randomUUID()
-       const transfer: ArrayBuffer[] = []
-       for (let k of Object.keys(data)) {
-               if (isBytes(data[k])) {
-                       data[k] = data[k].slice().buffer
-                       transfer.push(data[k])
-               }
-       }
-       console.log('sending data to worker')
-       return new Promise((resolve, reject) => {
-               tasks.set(id, [resolve, reject])
-               post(id, data, transfer)
-       })
-}
-
-export async function run (data: Record<'action', 'derive'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
-export async function run (data: Record<'action', 'sign'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
-export async function run (data: Record<'action', 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean>
-export async function run (data: Record<'action', 'derive' | 'sign' | 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean | string | Uint8Array<ArrayBuffer>> {
-       try {
-               const result = await start()
-               if (result === 'listening') {
-                       console.log('worker listening')
-               } else if (typeof result === 'string') {
-                       throw new Error(result)
-               } else {
-                       throw new Error('unknown error')
-               }
-       } catch (e: any) {
-               starting = undefined
-               throw new Nano25519WorkerError(e)
-       }
-
-       const result = await dispatch(data)
-       switch (data.action) {
-               case 'derive': {
-                       if ((isBytes(result) && result.byteLength === 32) || (typeof result === 'string' && /^[0-9a-f]{64}$/i.test(result))) {
-                               return result
+               switch (data.action) {
+                       case 'start': {
+                               result = 'listening'
+                               break
                        }
-                       break
-               }
-               case 'sign': {
-                       if ((isBytes(result) && result.byteLength === 64) || (typeof result === 'string' && /^[0-9a-f]{128}$/i.test(result))) {
-                               return result
+                       case 'derive': {
+                               const { privateKey } = data
+                               const publicKey = derive(privateKey)
+                               if (publicKey == null) {
+                                       throw new TypeError('Invalid public key from WASM derive()')
+                               }
+                               result = publicKey
+                               break
                        }
-                       break
-               }
-               case 'verify': {
-                       if (typeof result === 'boolean') {
-                               return result
+                       case 'sign': {
+                               const { message, secretKey } = data
+                               const signature = sign(message, secretKey)
+                               if (signature == null) {
+                                       throw new TypeError('Invalid signature from WASM sign()')
+                               }
+                               result = signature
+                               break
+                       }
+                       case 'verify': {
+                               const { message, publicKey, signature } = data
+                               const verification = verify(signature, message, publicKey)
+                               if (verification == null) {
+                                       throw new TypeError('Invalid verification from WASM verify()')
+                               }
+                               result = verification
+                               break
+                       }
+                       default: {
+                               throw new TypeError(`Invalid action '${data.action}'`)
                        }
-                       break
                }
+       } catch (err: any) {
+               result = JSON.stringify(err?.message ?? err ?? 'unknown error in nano25519 worker listener')
+       } finally {
+               const data = { id, result }
+               BROWSER: postMessage(data)
+               NODE: host?.postMessage({ data })
        }
-       throw new Nano25519ResultError(data.action, result)
 }
 
-class Nano25519WorkerError extends Error {
-       constructor (cause?: unknown) {
-               super(`async process error`, { cause })
-       }
-}
-class Nano25519ResultError extends Error {
-       constructor (action: string, cause?: unknown) {
-               super(`${action} result invalid`, { cause })
+BROWSER: addEventListener('message', listener)
+NODE: {
+       if (host == null) {
+               import('node:worker_threads')
+                       .then(({ parentPort }): void => {
+                               host = parentPort
+                               host?.on('message', listener)
+                       })
        }
 }