--- /dev/null
+//! 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 })
+ }
+}
//! 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'
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)
+ })
}
}