npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dokebi/seal

v0.4.2

Published

Payload sealing (obfuscation, replay defence, in-transit encryption) for seal hosts: @dokebi/websocket frames and @dokebi/web HTTP bodies, with browser socket, fetch and Inertia clients and their WASM loader

Readme

@dokebi/seal — payload sealing and the browser WebSocket client

  • Decisions: D-0092 (sealed EJS forms) · D-0059 (replaced the old @dokebi/crypto; changed to public on 2026-10-15) · D-0016 (the old private rule, replaced by D-0059) · D-0038 (replay protection) · D-0062 (HTTP sealing)
  • Origin: XFramework packages/x-seal (sealing) and packages/x-socketserver/client and src/wire.ts (browser socket); the initial port preserved the wire protocol; Dokebi has since added HTTP integration, replay-capacity handling and WASM hardening. Provenance is in SEAL-NOTICE.md.
  • A public package (MIT, owner's decision of 2026-10-15). npm install @dokebi/seal (in a Dokebi project, npx dokebi add @dokebi/seal: the @dokebi/cli installed in the project; the npm package dokebi is not this CLI). The npm archive holds only the built JS and types, the WASM (dist/wasm), the wasm-bindgen glue and the browser bundles (dist-client); the Rust source (crate/) and the tests stay in the repository. The release check (scripts/release/prepare.ts) allows seal content (WASM, .rs, the decoy string) only in this archive and refuses it in any other public archive.
  • It is a member of the root workspace, so it is part of npm run build, npm test and npm run check. It builds without Rust (see "Build and checks" below). The playground's sealed.localhost host uses this package (25-playground).
  • When another method is needed, change only the encryption method and plug it into the same codec slot (forConnection → { decode, encode, hello }) (D-0059).

Security boundary: keys derive from public host/path/User-Agent/time values and a fixed string in the browser code. Someone who analyses that code can open and forge frames. Seal provides obfuscation and replay rejection when a nonce store is configured (default drift ±60 seconds). It provides neither confidentiality nor proof of client identity and does not by itself establish compliance with encryption requirements. Use TLS (HTTPS/WSS), authentication, authorization, input validation, CSRF protection and rate limits as required. Host binding prevents accidental cross-host reuse; it is not a secret available only to a trusted site.

seal:   base64( obfuscate( AEAD(nonce || ciphertext || tag) ) )
frame:  E:<ts>:<base64>   (sealed)  ·  P:<plain text>  (sealing off; an explicit signal)
key:    derived from the request's host, path, UA and time (different for every frame)

🔴 The exact construction, constants and derivation are not written in this file (original audit 2026-08-12). The algorithm's source of truth is two places in code: crate/src/crypto.rs (the Rust original) and src/crypto.ts (the JS mirror).

The Rust original (crate/) and the JS mirror (src/crypto.ts) are byte-level compatible, and the known-vector parity gate (test/parity.test.ts) enforces it: changing one side only means client and server cannot open each other's frames. The algorithm, the key-derivation inputs, the decoy string (x-seal-default-decoy-v1; protocol data, so not renamed), the frame formats and the known test values are as in the original. Only the sealed-body content type was renamed from application/vnd.x-seal to application/vnd.dokebi-seal (D-0062; 0.1.0 was never published).

Entry points

| Path | Contents | Origin | |---|---|---| | @dokebi/seal | Node API + the Dokebi plugin createPlugin (core ^0.4.2): socket codec provider, HTTP codec provider and static serving of /dokebi/seal/* (D-0059 §4, D-0062) | (the plugin shell of @dokebi/crypto) | | @dokebi/seal/node | Node API only (no core): deriveKey, getUaSlice, seal, open, openWithNonce, wsSeal, wsOpenWithNonce, SEAL_DECOY_SECRET, SealTerminator, SealError, SealUsageError, MemoryNonceStore, NonceStoreFullError, requiredTtlSeconds, checkTtlCoversDrift, SEALED_CONTENT_TYPE, PREFIX_SEALED, PREFIX_PLAIN, DEFAULT_DRIFT_MS, forConnection, forRequest, httpProvider, SEAL_HELLO | @x/seal | | @dokebi/seal/testing | Node only: SealedClient, a browser-like client of a seal host for tests and concurrent load tests (cookie jar, CSRF, sealed forms, JSON and Inertia visits, its own connections; D-0101, "Testing a seal host" below) | (new) | | @dokebi/seal/browser | the browser seal loader: initSeal({ prefix }), sealFrame, unsealFrame, unsealPage, sealBridge | @x/seal/client | | @dokebi/seal/wasm · /wasm/glue | WASM binary · wasm-bindgen glue | wasm/x_seal* | | @dokebi/seal/client | the browser WebSocket client connectChannel (knows nothing about sealing; the bridge is injected) | @x/socketserver/client | | @dokebi/seal/client/seal | the bridge sealBridge filled by this package's seal loader + initSeal | (new) | | @dokebi/seal/client/vue | Vue 3 useChannel, useChannelEvent (vue is an optional peer) | (new) | | @dokebi/seal/client/react | React useChannel, useChannelEvent (react is an optional peer) | (new) | | @dokebi/seal/http | browser HTTP sealing: sealInertia, sealedFetch, startHttpSeal, armHttpSeal, httpSealed, currentSealBridge, SealHttpError (D-0062) | x-inertia/client (lessons) | | @dokebi/seal/forms | sealed HTML forms of server-drawn pages: starts on import; sealForms, submitSealedForm, and the pure parts formFields, formHasFiles, formRequest, formOutcome, sealedTarget, prefixOf (D-0092) | (new) | | @dokebi/seal/bundle/seal.mjs | the seal loader as a single ESM file (glue included; the wasm is fetched separately) | dist-client/client.mjs | | @dokebi/seal/bundle/socket.mjs | the socket client as a single ESM file (no dependencies) | dist-client/client.mjs | | @dokebi/seal/bundle/http.mjs | the HTTP helpers as a single ESM file (the loader included) | (new) | | @dokebi/seal/bundle/forms.mjs | the forms module as a single ESM file importing ./http.mjs (one instance with it) | (new) |

Browsers fetch the WASM from <prefix>/dokebi/seal/dokebi_seal_bg.wasm (originally /x/seal/). When dokebi-web is registered, this plugin serves that path (see "Server connection" below).

Server connection: sealing per host (D-0059 §4)

# dokebi.yaml — register this plugin (dokebi-seal) as well; without it the start stops with SEAL_PROVIDER_MISSING.
dokebi-app:
  domains:
    secure.example.com: { app: shop, seal: true }
    www.example.com:    { app: shop }
  • Codec provider: forConnection({ host, path, userAgent, nonces, driftMs }) of the plugin API (= @dokebi/seal/node) gives one connection's { decode, encode, hello }: a SealTerminator as is (fail-closed, plain text refused, drift, single-use nonces). It is a structural contract that @dokebi/websocket 0.3 looks up by name (dokebi-seal); public packages never import this package. nonces is hub.nonces(): in a cluster it is shared in Redis, so a frame used on one server is refused when replayed to another. Without a store no connection is created. A store that fails (a hub outage) closes the connection with 1013 Try again later (the client reconnects), never 4400 (D-0096).
  • GET /dokebi/seal/* (only with dokebi-web): seal.mjs (seal loader + glue), socket.mjs (socket client), http.mjs (HTTP helpers), forms.mjs (sealed forms, D-0092), dokebi_seal.js (glue only), dokebi_seal_bg.wasm. Cache-Control: public, max-age=300, must-revalidate + ETag (304 when equal)
    • nosniff. Nothing but these six exists (404). Missing build output stops setup with SEAL_ASSETS_MISSING (npm run seal:build). The routes are public (config.auth: false): an app with dokebi-auth.default: required still serves them to the login page (D-0096 §3).
  • Pages: @dokebi/app provides sealed (EJS data, Inertia shared prop, page.sealed). Load the loader only when sealed, so only seal hosts fetch the wasm (giving the bridge to a plain host works too; see the negotiation below):
<script type="module">
  const sealed = <%- JSON.stringify(data.sealed) %>
  const { connectChannel } = await import('/dokebi/seal/socket.mjs')
  const seal = sealed ? await import('/dokebi/seal/seal.mjs') : undefined
  if (seal) await seal.initSeal({ prefix: '' })
  const channel = connectChannel({ path: '/ws', version: BUILD, ...(seal ? { seal: seal.sealBridge } : {}) })
</script>
  • Negotiation: with a bridge, the subprotocols ['dokebi.seal', 'dokebi.plain'] are offered and the decision is ws.protocol === 'dokebi.seal' only. Seal hosts pick only dokebi.seal (never plain); plain hosts pick dokebi.plain, and the same code runs in plain text. A sealed connection is open only after the server's sealed {"$dokebi":"hello-v1"} arrives; its E:<ts>: gives offset = server ts − Date.now(), and every later frame (pings included) is sealed with Date.now() + offset, so a device clock that is minutes off still lands inside the ±60-second window. Connecting to a seal host without a bridge closes with 4415 Seal required; a frame the server cannot open closes with 4400 Bad frame (the reason is only in the server log). Neither reconnects. A hello that does not open is reported as SEAL_HELLO_INVALID and closed with 4400. When hello does not arrive within helloTimeoutMs (default 10 seconds), SEAL_HELLO_TIMEOUT is reported and the connection closes with 4000 and reconnects with backoff (temporary). channel.sealed() tells whether the current connection is sealed.

Browser socket client

import { connectChannel } from '@dokebi/seal/client'

const channel = connectChannel({
  path: '/ws',                     // the route the server defines; used as is when it starts with ws://
  version: APP_VERSION,            // ?dokebiVersion= — when it differs the server closes with 4426 and the client calls location.reload()
  query: { room: 'lobby' },        // never put authentication tokens in the URL (it ends up in logs)
  onPresence: () => reloadMembers(),          // { tenant, joined, left } delta — read the list from the server again
  onError: (code, fix) => console.warn(code, fix),
})
channel.on('chat.message', (data) => render(data))             // server messages and broadcasts { type, data }
const reply = await channel.request('chat.send', { text }, { timeoutMs: 5000 })   // { type, data, id } → reply.data
channel.send('typing', { on: true })                           // no reply; false when not open (no queue)
channel.close()                                                // does not reconnect
  • Envelope (Dokebi protocol): request {type,data,id} → {type:'reply',id,data} or {type:'error',id?,code}; server messages and broadcasts {type,data} (D-0057); presence {type:'presence',data}; keep-alive {"$dokebi":"ping-v1"} every 15 seconds (pingMs; 0 turns it off; D-0033).
  • request failures are ChannelRequestError.code: a server code (UNKNOWN_TYPE, INVALID_MESSAGE, a handler's code, HANDLER_FAILED) or NOT_CONNECTED (not sent), DISCONNECTED (sent but disconnected before the reply; it may have been processed), TIMEOUT (default 10 seconds, requestTimeoutMs), ABORTED (signal), CLOSED. Whether to resend is the caller's choice.
  • Reconnecting (as in the original): exponential backoff (default 500 ms → 10 seconds) × jitter [0.5, 1). What is sent while disconnected is dropped (no queue; sealed frames are tied to their ts and would be refused for drift anyway). Pending requests get DISCONNECTED.
  • Close codes: permanent (no reconnect) 4426 version differs (the page reloads; replace with onReload), 4409 replaced by a newer connection with the same clientId (or refused as a duplicate by an older server), 4403 kicked by an operator, 1008. Temporary (reconnect) 4408 idle, 1013 hub unavailable or slow consumer, 1012 server restart, 1011 handler error, 1006, 1001.
  • clientId (?clientId=): by default a new one per connection (so a reconnect neither replaces nor collides with the old connection the server may still hold). A fixed value or a function can be given (a newer connection with the same value replaces the older one, closed with 4409); false sends none.
  • In Node, pass an implementation (ws and so on) in the WebSocket option and give path as ws://….
  • Binary broadcasts arrive in onBinary(ArrayBuffer) (plain hosts only).
  • With logger ({ log(level, message, fields) }) connections, closes, reconnects and timeouts are logged at debug. Errors with nowhere to go go to onError → logger error → console.warn, in that order (never dropped silently, D-0047).

Seal hosts

import { connectChannel } from '@dokebi/seal/client'
import { initSeal, sealBridge } from '@dokebi/seal/client/seal'

await initSeal({ prefix: '' })                          // /dokebi/seal/dokebi_seal_bg.wasm
const channel = connectChannel({ path: '/ws', seal: sealBridge })

On a sealed connection (when the server picked dokebi.seal) every outgoing frame (send, request, ping) is sealed as E:<ts>:…, and every incoming frame must open: plain or unopenable frames are refused as SEAL_OPEN_FAILED and binary as SEAL_BINARY (fail-closed). The seal path is the connection path without the query (dokebiVersion and clientId never enter the key). Without a bridge it is plain (like the original, nothing is guessed). See "Server connection" above as well.

Vue · React · ESM

// Vue 3 — closes when the scope (component) ends
import { useChannel, useChannelEvent } from '@dokebi/seal/client/vue'
const { channel, status, closed } = useChannel({ path: '/ws', version: APP_VERSION })   // status and closed are ShallowRefs
useChannelEvent(channel, 'chat.message', (data) => messages.value.push(data))

// React — opens in an effect, closes in its cleanup; reconnects only when path, version or deps change; callbacks are the latest
import { useChannel, useChannelEvent } from '@dokebi/seal/client/react'
const { channel, status } = useChannel({ path: '/ws', version: APP_VERSION })            // channel is undefined on the first render
useChannelEvent(channel, 'chat.message', (data) => setMessages((list) => [...list, data]))
<!-- Pages without a bundler: this plugin serves /dokebi/seal/* through dokebi-web -->
<script type="module">
  import { initSeal, sealBridge } from '/dokebi/seal/seal.mjs'   // seal hosts only (when sealed)
  import { connectChannel } from '/dokebi/seal/socket.mjs'
  await initSeal({ prefix: '' })
  const channel = connectChannel({ path: '/ws', seal: sealBridge })   // the bridge takes the server time (now) as the third argument
</script>

HTTP sealing (D-0062)

On seal hosts the HTTP data paths are sealed too: REST JSON requests and responses, the Inertia first page (data-page) and visits (XHR, 4xx included), and SSE event data. The server-side decision and wiring are @dokebi/web 0.2's seal slot, which this plugin fills (codec httpProvider, nonces from the store nonces.driver picks, below). Exceptions: EJS HTML (page navigations, their error pages included), GET queries, file uploads (multipart), static files, health, seal: false routes, dokebi-web.sealExclude. The decision and its reasons are in D-0062.

// views/app.js (Inertia + Vue)
import { sealInertia } from '@dokebi/seal/http';
import { createInertiaApp, http } from '@inertiajs/vue3';
await sealInertia(http); // sealed page: opens data-page and seals every visit · plain page: does nothing
createInertiaApp({ /* ... */ });

// REST — the same code on seal hosts and plain hosts
import { currentSealBridge, sealedFetch } from '@dokebi/seal/http';
const answer = await sealedFetch('/api/notes', { method: 'POST', body: { text } });
await answer.json();
// SSE (@dokebi/sse/client)
connectEvents({ path: '/events', seal: currentSealBridge() });
  • Request body: Content-Type: application/vnd.dokebi-seal, E:<ts>:<b64> (key = host, request path, UA, ts). Response: the same format + x-dokebi-sealed-path (the final path after redirects, which XHR does not report). 4xx responses are opened too. A failed opening is SealHttpError('SEAL_OPEN_FAILED') (no plain-text fallback).
  • On plain hosts sealedFetch is plain fetch, and sealInertia does not fetch the WASM (the loader is a dynamic import).
  • sealedFetch refuses requests to another origin (SEAL_CROSS_ORIGIN: the key belongs to this host) and bodies inside a Request object (SEAL_REQUEST_BODY: pass { body }). A sealed response without sealing on is SEAL_BRIDGE_MISSING.
  • On seal hosts sealedFetch adds x-dokebi-sealed: 1 to same-origin requests (a script: its errors come back as sealed JSON even when it asks for HTML, D-0092).
  • Pages without a bundler: /dokebi/seal/http.mjs (map it to @dokebi/seal/http with an import map; with @dokebi/app data.seal.head writes that map).
  • Inertia apps (Vite): dokebiInertia() of @dokebi/template-inertia/vite in vite.config.ts (the CLI writes it) makes every import … from '@dokebi/seal/http' (and @dokebi/seal/forms) of the app's browser code load the served /dokebi/seal/http.mjs, in the build and the dev server. The bootstrap (sealInertia(http)) and sealedFetch in pages are then one instance, with a plain static import in the page.
  • It cannot be combined with SSR (SEAL_SSR_CONFLICT): SSR renders props into plain HTML.

EJS forms on seal hosts (@dokebi/seal/forms, D-0092)

A seal host refuses a plain form body (415 SEAL_REQUIRED), so server-drawn pages (EJS) load the forms module, which sends every POST form sealed. With @dokebi/app the layout writes one line in <head>, before other module scripts:

<%- data.seal.head %>   <!-- seal host: the import map of @dokebi/seal/http and @dokebi/seal/forms + the forms module; elsewhere '' -->
  • What it sends: every same-origin <form method="post"> (the submitter's formaction, formmethod and formtarget win) unless data-seal="off", another target than this window, or another origin. The form data with the submitter's name/value becomes an object of the urlencoded shape (strings; a repeated name an array of strings in form order; an empty file input its empty file name) and goes through sealedFetch with x-dokebi-form: 1. The CSRF field _csrf stays in the sealed body (@dokebi/csrf reads it after the body is opened). With a chosen file the form goes as multipart (file bytes are not sealed, D-0062) with the same header, the _csrf field moved into the x-csrf-token header (the field name: sealForms({ csrfField })).
  • The server (@dokebi/web 0.4.3) treats the marked request as a form post: the handler gets the same request.body a urlencoded form gives (checked server-side), errors take the HTML form flow (422 back with errors and old, other errors the HTML error page), and every redirect is told as 204 + x-dokebi-location.
  • The answer: a told redirect (or one the fetch followed) → location.assign; HTML → the document is replaced with it (document.open/write/close; the listener is added again); a sealed JSON error → a short message in a <p role="alert" data-seal-form-error> at the top of the form; a failure to send → the same message, console.error and a rejected submitSealedForm. A form in flight (aria-busy="true") is not sent again.
  • Start: importing the module listens for submits at once and arms sealedFetch (armHttpSeal, the prefix taken from its own address <prefix>/dokebi/seal/forms.mjs). It starts HTTP sealing (startHttpSeal, the WASM) at load only when the page has a POST form, else on the first sealed submit or the first same-origin sealedFetch; a submit or a fetch before the WASM arrived waits for it, so nothing goes out plain. A read-only page loads no WASM. sealForms(options?) returns the same start; submitSealedForm(form, submitter?) sends a form yourself — form.submit() fires no submit event and goes out plain (415); use form.requestSubmit() or submitSealedForm.
  • One module instance: /dokebi/seal/forms.mjs imports ./http.mjs, the file the import map names @dokebi/seal/http, so the page's sealedFetch shares the bridge the forms module turned on. Bundled apps import @dokebi/seal/forms and @dokebi/seal/http from the package (one copy), or keep them out of the bundle (Inertia: dokebiInertia(), above). Do not mix the served files with a bundled copy.
  • A layout with its own import map: write data.seal.head first. Browsers without multiple import maps (Chrome before 133 and the like) take only the first map: then add the two entries to your own map and load /dokebi/seal/forms.mjs yourself. A Content-Security-Policy with script nonces must allow the inline import map.

Replay nonces of sealed HTTP bodies (D-0096)

A captured sealed body is accepted once. With several node:cluster workers or servers that holds only when they share one nonce set, so the plugin takes the store as an option:

dokebi-seal:
  nonces: { driver: redis, connection: main }   # a dokebi-redis connection; the WebSocket hub is not needed

| Option | Default | Meaning | |---|---|---| | nonces.driver | auto | auto (the hub's nonces() when @dokebi/websocket-hub is registered, otherwise this process), memory, redis or hub | | nonces.connection | — | redis: the connection name in dokebi-redis.connections (required) | | nonces.namespace | dokebi | redis: key prefix (<namespace>:seal:nonce:<nonce>, and the epoch key <namespace>:seal:epoch) | | nonces.timeoutMs | 2000 | a shared store slower than this refuses the body as 503 | | nonces.max | 100000 | live nonces of the in-process store | | nonces.allowInCluster | false | accept per-worker nonces in a cluster worker (warning instead of a failed start) |

  • Redis: one atomic SET NX PX per nonce, expiring just after 2 x drift (requiredTtlSeconds: 121 s for 60 s), on a Redis that never evicts keys (an evicting one logs REDIS_EVICTION_UNSAFE at start).
  • In a node:cluster worker, per-process nonces with anything sealed fail the start (SEAL_NONCES_NOT_SHARED): a replay sent to another worker would be accepted there. nonces.allowInCluster: true accepts that knowingly.
  • A store that fails or is too slow (Redis down, its pending-command limit, a timeout) refuses the body with 503 SEAL_NONCE_STORE_UNAVAILABLE + Retry-After and an error log with the cause — never a client 400. Only a body proven bad (no prefix, plain, drift, replay, does not open) is 400 SEAL_BODY_REFUSED.

Testing a seal host (@dokebi/seal/testing, D-0101)

A seal host refuses plain request bodies (415) and seals its JSON answers, so fetch and curl cannot test it. SealedClient sends what the browser runtime sends — the page and its CSRF token (and the seal wasm), sealed forms (x-dokebi-form: 1, _csrf inside the sealed body), sealed JSON (x-dokebi-sealed: 1, x-xsrf-token from the XSRF-TOKEN cookie) and Inertia visits — and opens every sealed answer. One client is one browser identity (cookie jar, token, clock offset, its own keep-alive http.Agent with connections sockets), so a load test makes one client per user and runs them concurrently:

import { SealedClient } from '@dokebi/seal/testing'
const clients = Array.from({ length: 200 }, () => new SealedClient({ origin: 'http://shop.test:3000', connect: 'http://127.0.0.1:3000', connections: 2 }))
await Promise.all(clients.map(async (client, index) => {
  await client.navigate('/login')
  await client.submit('/login', { email: `user-${index}@shop.test`, password })
}))
const answers = await Promise.all(clients.map((client) => client.fetch('/api/items/1/reserve', { method: 'POST', json: {} })))
// exactly one 200 for the last item, 409 for the rest, a 503 only with Retry-After, never a 500
await Promise.all(clients.map((client) => client.close()))

The full reference is AI.md §5.5; a copyable script is examples/49-sealed-load-test. The cipher is this package's Node mirror of the WASM (wsSeal/wsOpenWithNonce), every request is sealed anew (fresh nonce), and an answer that does not open is an error (SEAL_CLIENT_OPEN_FAILED), never plain text.

Server side (Node)

import { MemoryNonceStore, SealTerminator } from '@dokebi/seal/node'
const terminator = new SealTerminator({
  domain: 'app.a.com', path: '/ws', ua: request.headers['user-agent'] ?? '',
  nonces: hub.nonces(),          // shared: hub.nonces() of websocket-hub (cluster) or new RedisNonceStore(redisConnection); one process: new MemoryNonceStore()
})
const frame = terminator.encode(JSON.stringify(message))   // 'E:<ts>:…'
const plain = await terminator.decode(incoming)            // failures are SealError (reason: no-prefix, plain-rejected, drift, replay, capacity, unavailable, open-failed)

The global registry of the original x-seal (sealNonces) was not ported; the store is a constructor argument (D-0038, as in @dokebi/crypto). Without nonces, replay protection is off. A store that rejects (other than full) makes decode reject with SealError SEAL_NONCE_STORE_UNAVAILABLE (reason unavailable, retryable: true, the store error as cause): the frame is refused, and it is the server's fault, not the client's.

Capacity (D-0075, fail closed). new MemoryNonceStore({ max: 100_000, logger }) keeps at most max live nonces. It never forgets a live one to make room (that would let a captured frame be replayed): expired nonces go first (all of them, earliest expiry first, through an expiry heap), and when every held nonce is still live the new frame is refused (NonceStoreFullError, NONCE_STORE_FULL → SealError SEAL_REPLAY_CAPACITY, reason capacity). Clients see a retryable answer (web 503 SEAL_BUSY, websocket 1013 Try again later), and the store warns NONCE_STORE_FULL once per 10 seconds with max, live and repeated. This trades availability under a flood of valid frames for replay protection. Size max above the sealed frames and bodies one process accepts within 2 × drift (120 s by default; 100000 ≈ 830 per second); beyond that use a Redis store (nonces.driver: redis, or websocket-hub mode: cluster) or a smaller drift. websocket-hub's local store behaves the same.

Build and checks

The built WASM and glue (wasm/: four files after the glue and exposure post-processing) are committed (like the original x-seal wasm/). So npm run build only compiles TypeScript, copies wasm/ → dist/wasm and builds the single-file vite bundles, without Rust (CI: Node 24 slim, no Rust). Only after changing crate/ is it rebuilt with Rust and wasm/ committed with it. The build is reproducible (2026-10-15: the four files rebuilt from an empty target folder were byte-identical).

npm run seal:build          # (root) tsc + wasm/ → dist/wasm copy + vite single-file bundles (no Rust needed)
npm run seal:build:wasm     # (root) Rust 1.98.0 + wasm-pack 0.13.1: crate/ → wasm/ (--target web, --locked, path folding, glue and exposure post-processing) → dist/wasm
npm run seal:test           # (root) build + type check + node --test test/*.test.ts (also part of the root npm test)
npm run seal:test:rust      # (root) cargo test --locked (known vectors of the Rust original); not part of npm test
npm run seal:browser-check  # (root) real Chromium (Playwright) against a real Dokebi app with sealed and plain hosts: loading /dokebi/seal/*,
                            # Node ↔ browser, every frame E: on the seal host, the plain host, broadcasts, 4415,
                            # sealed EJS forms (data.seal.head, sealed wire, told redirect, 422 page, multipart, HTML 415/404)

test/: parity (Rust ↔ JS known vectors), terminator (fail-closed, replay) and exposure (no paths, dependency versions, decoy or key-input names in the wasm and glue), as in the original; node, client, context and replay, moved from the @dokebi/crypto tests; socket (fake WebSocket: negotiation, hello, offset, 4400/4415), adapters (real Vue; React with a minimal hook runner), http (HTTP sealing), nonces (store choice, Redis store, 503 for a failing or slow store), public-routes (the assets under dokebi-auth.default: required, a sealed login), cluster-nonces (four node:cluster workers + Docker Redis: a replay to another worker refused, overload and a Redis outage answered 503, per-process nonces refused at start), testing-client (SealedClient against a real sealed app in one process: page token, sealed login form, sealed JSON and CSRF, Inertia visit and post, 40 clients racing one ticket, connections per client, failures closed), sealed-client-cluster (D-0101: four workers + Docker Redis and PostgreSQL, 200 concurrent clients: one winner of a conditional update, 503 DB_UNAVAILABLE from a saturated pool, never 500), forms (the forms module's shape, request, target and answer; a sealed form on the wire), integration (a real @dokebi/websocket server, plain) and sealed-server (D-0059 §4: two real servers + a disposable Docker Redis: raw frames E: with no plain text, cross-server replay refused, drift refused, a device clock ±5 minutes off (worker), 4415, the plain host. Needs Docker; skipped with DOKEBI_DOCKER=off).