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.1

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-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 files were copied as they are and only renamed. 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).

⚠️ Seal is not "complete security". Keys are built for every frame from public values (host name, path, part of the User-Agent, timestamp) and a fixed string in the code, so anyone who analyses the browser code can open and forge frames. It gives three things: obfuscation (raises the cost of attacks; not a defence), replay protection (±60 seconds, single-use nonces; real security, because HTTPS cannot do it) and compliance with "encrypt data in transit" requirements. Confidentiality comes from TLS (WSS). The host name is part of the key, so a frame captured on one host does not open on another. It does not replace input validation, authorisation, CSRF protection or rate limits. The only real risk of this feature is the false comfort of "seal is on, so validation can be looser".

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.0): 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/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, httpSealed, currentSealBridge, SealHttpError (D-0062) | x-inertia/client (lessons) | | @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) |

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.
  • GET /dokebi/seal/* (only with dokebi-web): seal.mjs (seal loader + glue), socket.mjs (socket client), http.mjs (HTTP helpers), 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 five exists (404). Missing build output stops setup with SEAL_ASSETS_MISSING (npm run seal:build).
  • 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 same clientId, 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 is not blocked with 4409 while the server still holds the old connection). A fixed value or a function can be given; 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 hub's nonces()). Exceptions: EJS HTML and native forms, 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.
  • Pages without a bundler: /dokebi/seal/http.mjs (map it to @dokebi/seal/http with an import map).
  • It cannot be combined with SSR (SEAL_SSR_CONFLICT): SSR renders props into plain HTML.

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(),          // with several servers or workers the shared store of websocket-hub (cluster); in 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, 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.

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 the Redis store of 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

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), 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).