//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { MessagePort as NodeMessagePort, Worker as NodeWorker } from 'node:worker_threads'
//@ts-expect-error
import nano25519_wasm from '../../build/nano25519.wasm'
-type Data = {
- url: string
- id: string
- action: string
- message?: string | ArrayBuffer
- privateKey?: string
- publicKey?: string
- secretKey?: string
- signature?: string
-}
type Exports = {
exports: {
derive: () => void,
}
}
-const nano25519_init = (bytes: number[]): { derive: typeof derive, sign: typeof sign, verify: typeof verify } => {
+export const nano25519_init = (bytes: number[]): { derive: typeof derive, sign: typeof sign, verify: typeof verify } => {
const wasm: Uint8Array<ArrayBuffer> = Uint8Array.from(bytes)
const module = new WebAssembly.Module(wasm)
const { exports } = new WebAssembly.Instance(module, {
}
export const nano25519 = nano25519_init(nano25519_wasm)
-
-const nano25519_worker_init = ({ derive, sign, verify }: typeof nano25519) => {
- let isListening = false
- let host: NodeMessagePort | null = null
- let client: string | undefined = globalThis.location?.href
-
- /**
- * 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 handleMessage (message: unknown): void {
- NODE: if (host == null) return queueMicrotask(() => handleMessage(message))
- if (message == null
- || typeof message !== 'object'
- || !('data' in message)
- || message.data == null
- || typeof message.data !== 'object'
- || !('url' in message.data)
- || typeof message.data.url !== 'string'
- || !('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 url: undefined | string
- let id: undefined | string
- try {
- const data: Data = message.data as object & { url: string, id: string, action: string }
- { ({ url, id } = data) }
- if (url !== client) return
-
- if (data.action === 'start') {
- isListening = true
- result = 'started'
- } else if (data.action === 'stop') {
- isListening = false
- result = 'stopped'
- } else if (isListening) {
- const { action } = data
- if (action === 'derive') {
- const { privateKey } = data
- const publicKey = derive(privateKey)
- if (publicKey == null) {
- throw new TypeError('Invalid public key from WASM derive()')
- }
- result = publicKey
- } else if (action === 'sign') {
- const { message, secretKey } = data
- const signature = sign(message, secretKey)
- if (signature == null) {
- throw new TypeError('Invalid signature from WASM sign()')
- }
- result = signature
- } else if (action === '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
- }
- }
- } catch (err: unknown) {
- if (typeof err === 'object' && err != null) {
- const { message } = err as { [k: string]: unknown }
- if (typeof message === 'string' && message !== 'divide by zero') {
- result = message
- }
- } else {
- result = JSON.stringify(err)
- }
- } finally {
- BROWSER: postMessage({ url, id, result })
- NODE: host?.postMessage({ data: { url, id, result } })
- }
- }
- BROWSER: addEventListener('message', handleMessage)
- NODE: {
- if (host == null) {
- import('node:worker_threads')
- .then(({ parentPort, threadId }): void => {
- host = parentPort
- client = threadId.toString()
- host?.on('message', handleMessage)
- })
- }
- }
-}
-
-const nano25519_worker = `;(${nano25519_worker_init})((${nano25519_init})([${nano25519_wasm}]));`
-
-/**
- * Host code for asynchronous Web Worker
- */
-let isWorkerReady: boolean = false
-let isWorkerListening: boolean = false
-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: {
- url = URL.createObjectURL(new Blob([nano25519_worker], { type: 'text/javascript' }))
- worker = new Worker(url, { type: 'module' })
- }
- NODE: {
- worker = new NodeWorker(nano25519_worker, {
- eval: true,
- stderr: false,
- stdout: false
- })
- url = worker.threadId.toString()
- }
- console.log(`nano25519 initialized.`)
- isWorkerReady = true
- } catch (err: any) {
- isWorkerReady = false
- throw new Error('nano25519 initialization failed')
- }
-}
-
-// Reconstruct worker when errors occur
-function reset (): void {
- console.warn(`nano25519 encountered an error. Reinitializing...`)
- isWorkerReady = false
- worker.terminate()
- init()
-}
-
-function onresult (msg: { data: Record<string, unknown> }): void {
- const { data } = msg
- if (data.url !== url) return
- if (!('id' in data) || typeof data.id !== 'string') return
-
- const executor = tasks.get(data.id)
- if (executor == null) return
- const [ok, err] = executor
-
- const { result } = data
- console.log('received result from worker')
- if (typeof result !== 'boolean' && typeof result !== 'string' && !isBytes(result)) {
- err('Invalid return type')
- }
- ok(result)
-}
-
-// Check that the worker is running and listening before sending messages
-async function start (): Promise<void> {
- if (!isWorkerReady) init()
- if (!isWorkerListening) {
- return new Promise(async (resolve, reject): Promise<void> => {
- const onstarted = (msg: { data: Record<string, unknown> }): void => {
- if (msg.data.url !== url) return
- const { result } = msg.data
- if (result === 'started') {
- console.log('worker started successfully')
- BROWSER: { (worker as Worker).onmessage = onresult }
- NODE: { (worker as NodeWorker).on('message', onresult) }
- isWorkerListening = true
- resolve()
- } else {
- isWorkerListening = false
- reject()
- }
- }
- console.log(`starting worker`)
- const id = crypto.randomUUID()
- const data = { url, id, action: 'start' }
- BROWSER: {
- worker = worker as Worker
- worker.onerror = reject
- worker.onmessage = onstarted
- worker.postMessage(data)
- }
- NODE: {
- worker = worker as unknown as NodeWorker
- worker.on('error', reject)
- worker.on('message', onstarted)
- worker.postMessage({ data })
- }
- })
- }
-}
-
-// Send command and relevant data to nano25519 worker
-async function dispatch (data: 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].buffer.slice()
- transfer.push(data[k])
- }
- }
- console.log('sending data to worker')
- data.url = url
- data.id = id
- return new Promise((resolve, reject) => {
- tasks.set(id, [resolve, reject])
- console.log('tasks', tasks)
- BROWSER: { (worker as Worker).postMessage(data, transfer) }
- NODE: { (worker as NodeWorker).postMessage({ data }, transfer) }
- })
-}
-
-// Request that the worker stop listening without terminating
-async function stop (): Promise<void> {
- return new Promise((resolve, reject): void => {
- const onstop = (msg: { data: Record<string, unknown> }): void => {
- const { data } = msg
- if (data.url !== url) return
- const { result } = data
- if (result === 'stopped') {
- console.log('worker stopped successfully')
- isWorkerListening = false
- resolve()
- } else {
- reject(result)
- }
- }
- console.log(`stopping worker`)
- const id = crypto.randomUUID()
- BROWSER: {
- worker = worker as Worker
- worker.onerror = reject
- worker.onmessage = onstop
- worker.postMessage({ url, id, action: 'stop' })
- }
- NODE: {
- worker = worker as unknown as NodeWorker
- worker.on('error', reject)
- worker.on('message', onstop)
- worker.postMessage({ data: { url, id, action: 'stop' } })
- }
- })
-}
-
-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 {
- await start()
- } catch (err: any) {
- throw new Error('Error initializing worker')
- }
- try {
- 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
- } else {
- throw new Error('derive result invalid')
- }
- }
- case 'sign': {
- if ((isBytes(result) && result.byteLength === 64) || (typeof result === 'string' && /^[0-9a-f]{128}$/i.test(result))) {
- return result
- } else {
- throw new Error('sign result invalid')
- }
- }
- case 'verify': {
- if (typeof result === 'boolean') {
- return result
- } else {
- throw new Error('verify result invalid')
- }
- }
- }
- } catch (err: any) {
- try {
- await stop()
- } catch (e: any) {
- console.error('failed to stop worker')
- reset()
- } finally {
- throw new Error('Error dispatching async request')
- }
- }
-}
--- /dev/null
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { MessagePort as NodeMessagePort, Worker as NodeWorker } from 'node:worker_threads'
+//@ts-expect-error
+import nano25519_wasm from '../../build/nano25519.wasm'
+
+import { nano25519_init } from './nano25519'
+
+type Data = {
+ url: string
+ id: string
+ action: string
+ message?: string | ArrayBuffer
+ privateKey?: string
+ publicKey?: string
+ secretKey?: string
+ signature?: string
+}
+
+const nano25519_worker_init = ({ derive, sign, verify }: ReturnType<typeof nano25519_init>) => {
+ let isListening = false
+ let host: NodeMessagePort | null = null
+ let client: string | undefined = globalThis.location?.href
+
+ /**
+ * 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 handleMessage (message: unknown): void {
+ NODE: if (host == null) return queueMicrotask(() => handleMessage(message))
+ if (message == null
+ || typeof message !== 'object'
+ || !('data' in message)
+ || message.data == null
+ || typeof message.data !== 'object'
+ || !('url' in message.data)
+ || typeof message.data.url !== 'string'
+ || !('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 url: undefined | string
+ let id: undefined | string
+ try {
+ const data: Data = message.data as object & { url: string, id: string, action: string }
+ { ({ url, id } = data) }
+ if (url !== client) return
+
+ if (data.action === 'start') {
+ isListening = true
+ result = 'started'
+ } else if (data.action === 'stop') {
+ isListening = false
+ result = 'stopped'
+ } else if (isListening) {
+ const { action } = data
+ if (action === 'derive') {
+ const { privateKey } = data
+ const publicKey = derive(privateKey)
+ if (publicKey == null) {
+ throw new TypeError('Invalid public key from WASM derive()')
+ }
+ result = publicKey
+ } else if (action === 'sign') {
+ const { message, secretKey } = data
+ const signature = sign(message, secretKey)
+ if (signature == null) {
+ throw new TypeError('Invalid signature from WASM sign()')
+ }
+ result = signature
+ } else if (action === '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
+ }
+ }
+ } catch (err: unknown) {
+ if (typeof err === 'object' && err != null) {
+ const { message } = err as { [k: string]: unknown }
+ if (typeof message === 'string' && message !== 'divide by zero') {
+ result = message
+ }
+ } else {
+ result = JSON.stringify(err)
+ }
+ } finally {
+ BROWSER: postMessage({ url, id, result })
+ NODE: host?.postMessage({ data: { url, id, result } })
+ }
+ }
+ BROWSER: addEventListener('message', handleMessage)
+ NODE: {
+ if (host == null) {
+ import('node:worker_threads')
+ .then(({ parentPort, threadId }): void => {
+ host = parentPort
+ client = threadId.toString()
+ host?.on('message', handleMessage)
+ })
+ }
+ }
+}
+
+const nano25519_worker = `;(${nano25519_worker_init})((${nano25519_init})([${nano25519_wasm}]));`
+
+/**
+ * Host code for asynchronous Web Worker
+ */
+let isWorkerReady: boolean = false
+let isWorkerListening: boolean = false
+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: {
+ url = URL.createObjectURL(new Blob([nano25519_worker], { type: 'text/javascript' }))
+ worker = new Worker(url, { type: 'module' })
+ }
+ NODE: {
+ worker = new NodeWorker(nano25519_worker, {
+ eval: true,
+ stderr: false,
+ stdout: false
+ })
+ url = worker.threadId.toString()
+ }
+ console.log(`nano25519 initialized.`)
+ isWorkerReady = true
+ } catch (err: any) {
+ isWorkerReady = false
+ throw new Error('nano25519 initialization failed')
+ }
+}
+
+// Reconstruct worker when errors occur
+function reset (): void {
+ console.warn(`nano25519 encountered an error. Reinitializing...`)
+ isWorkerReady = false
+ worker.terminate()
+ init()
+}
+
+function onresult (msg: { data: Record<string, unknown> }): void {
+ const { data } = msg
+ if (data.url !== url) return
+ if (!('id' in data) || typeof data.id !== 'string') return
+
+ const executor = tasks.get(data.id)
+ if (executor == null) return
+ const [ok, err] = executor
+
+ const { result } = data
+ console.log('received result from worker')
+ if (typeof result !== 'boolean' && typeof result !== 'string' && !isBytes(result)) {
+ err('Invalid return type')
+ }
+ ok(result)
+}
+
+// Check that the worker is running and listening before sending messages
+async function start (): Promise<void> {
+ if (!isWorkerReady) init()
+ if (!isWorkerListening) {
+ return new Promise(async (resolve, reject): Promise<void> => {
+ const onstarted = (msg: { data: Record<string, unknown> }): void => {
+ if (msg.data.url !== url) return
+ const { result } = msg.data
+ if (result === 'started') {
+ console.log('worker started successfully')
+ BROWSER: { (worker as Worker).onmessage = onresult }
+ NODE: { (worker as NodeWorker).on('message', onresult) }
+ isWorkerListening = true
+ resolve()
+ } else {
+ isWorkerListening = false
+ reject()
+ }
+ }
+ console.log(`starting worker`)
+ const id = crypto.randomUUID()
+ const data = { url, id, action: 'start' }
+ BROWSER: {
+ worker = worker as Worker
+ worker.onerror = reject
+ worker.onmessage = onstarted
+ worker.postMessage(data)
+ }
+ NODE: {
+ worker = worker as unknown as NodeWorker
+ worker.on('error', reject)
+ worker.on('message', onstarted)
+ worker.postMessage({ data })
+ }
+ })
+ }
+}
+
+// Send command and relevant data to nano25519 worker
+async function dispatch (data: 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].buffer.slice()
+ transfer.push(data[k])
+ }
+ }
+ console.log('sending data to worker')
+ data.url = url
+ data.id = id
+ return new Promise((resolve, reject) => {
+ tasks.set(id, [resolve, reject])
+ console.log('tasks', tasks)
+ BROWSER: { (worker as Worker).postMessage(data, transfer) }
+ NODE: { (worker as NodeWorker).postMessage({ data }, transfer) }
+ })
+}
+
+// Request that the worker stop listening without terminating
+async function stop (): Promise<void> {
+ return new Promise((resolve, reject): void => {
+ const onstop = (msg: { data: Record<string, unknown> }): void => {
+ const { data } = msg
+ if (data.url !== url) return
+ const { result } = data
+ if (result === 'stopped') {
+ console.log('worker stopped successfully')
+ isWorkerListening = false
+ resolve()
+ } else {
+ reject(result)
+ }
+ }
+ console.log(`stopping worker`)
+ const id = crypto.randomUUID()
+ BROWSER: {
+ worker = worker as Worker
+ worker.onerror = reject
+ worker.onmessage = onstop
+ worker.postMessage({ url, id, action: 'stop' })
+ }
+ NODE: {
+ worker = worker as unknown as NodeWorker
+ worker.on('error', reject)
+ worker.on('message', onstop)
+ worker.postMessage({ data: { url, id, action: 'stop' } })
+ }
+ })
+}
+
+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 {
+ await start()
+ } catch (err: any) {
+ throw new Error('Error initializing worker')
+ }
+ try {
+ 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
+ } else {
+ throw new Error('derive result invalid')
+ }
+ }
+ case 'sign': {
+ if ((isBytes(result) && result.byteLength === 64) || (typeof result === 'string' && /^[0-9a-f]{128}$/i.test(result))) {
+ return result
+ } else {
+ throw new Error('sign result invalid')
+ }
+ }
+ case 'verify': {
+ if (typeof result === 'boolean') {
+ return result
+ } else {
+ throw new Error('verify result invalid')
+ }
+ }
+ }
+ } catch (err: any) {
+ try {
+ await stop()
+ } catch (e: any) {
+ console.error('failed to stop worker')
+ reset()
+ } finally {
+ throw new Error('Error dispatching async request')
+ }
+ }
+}