Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
- A per-operation timeout (default 10s, `startWorker(url, { timeout })`, `0` to
disable) catches a worker that loads but then hangs and never replies.
It is treated as a transport failure and falls back like the others.
- The worker reply protocol now asserts success explicitly (`ok: true`) instead of
inferring it from the absence of an error.
- Worker failures no longer disable offloading for the lifetime of the page.
- New `startWorker(url, { onStatusChange })` reports when operations start or stop
running in the worker.
- New build `libomemo.js/worker-client` (ESM): the same public API as the default
build but with **no bundled WebAssembly**. Its local backend is a stub that
throws until `startWorker(url)` is called, so the worker carries the only copy
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,25 @@ falls back to the main-thread WebAssembly, so crypto keeps working. Errors the
worker reports for a specific operation (an invalid signature, for example) are
propagated unchanged and do not trigger a fallback.

Whether operations are currently offloaded is observable. In the default build
a fallback means private key operations have moved onto the main thread, which
some applications will want to surface or act on:

```js
startWorker("/path/to/libomemo-worker.js", {
onStatusChange: ({ offloaded, error }) => {
if (!offloaded) console.warn("OMEMO crypto is on the main thread", error);
},
});
```

The callback is edge-triggered. It fires when offloading starts or stops, not per
operation, and not for `stopWorker()`.

**Important**: The worker script runs inside the trust boundary and it receives
raw private keys. Serve it as a trusted, same-origin script under your own CSP,
and do not build its URL from remote or user-supplied input.

### The `worker-client` build (no bundled WebAssembly)

For apps that always run a worker, `libomemo.js/worker-client` is a second build
Expand All @@ -293,6 +312,7 @@ const identityKeyPair = await KeyHelper.generateIdentityKeyPair();

Any operation attempted before a worker is started (or while the worker is
unavailable) rejects with a clear error, since there is no main-thread fallback.

This build is ESM-only and intended for the browser; under Node, use the default
build.

Expand Down
21 changes: 16 additions & 5 deletions src/curve25519_worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,36 @@ const ALLOWED_METHODS = new Set<keyof Curve25519>([
"ed25519PubKeyToCurvePubKey",
]);

/**
* Reduce a thrown value to a non-empty message string.
*/
function toErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) return error.message;
if (typeof error === "string" && error) return error;
return "curve25519 worker operation failed";
}

self.onmessage = (e: MessageEvent<WorkerMessage>) => {
const { id, methodName, args } = e.data;

if (!ALLOWED_METHODS.has(methodName)) {
postMessage({ id, error: "Unsupported method." });
postMessage({ id, ok: false, error: "Unsupported method." });
return;
}

const method = curve[methodName];
if (typeof method !== "function") {
postMessage({ id, error: "Unsupported method." });
postMessage({ id, ok: false, error: "Unsupported method." });
return;
}

Promise.resolve((method.bind(curve) as (...a: unknown[]) => Promise<unknown>)(...args))
.then((result: unknown) => {
postMessage({ id, result });
// `ok: true` is what makes success explicit rather than inferred from
// the absence of an error. See the manager's #onMessage.
postMessage({ id, ok: true, result });
})
.catch((error: Error) => {
postMessage({ id, error: error.message });
.catch((error: unknown) => {
postMessage({ id, ok: false, error: toErrorMessage(error) });
});
};
Loading