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

@norn-ts/codec

v1.0.0

Published

Payload encryption with key rotation and a payload converter for bigint and JS Temporal types.

Readme

@norn-ts/codec

Payload encryption with key rotation, and a payload converter that round-trips bigint and TC39 Temporal API values, for Norn and plain Temporal TypeScript apps.

  • createEncryptionCodec: a Temporal PayloadCodec using AES-256-GCM (WebCrypto only), with key ids in payload metadata so you can rotate keys without breaking existing history.
  • payloadConverter: Temporal's default converter (binary/null, binary/plain, json/plain), plus lossless bigint and Temporal.Instant / PlainDate / PlainDateTime / ZonedDateTime / Duration / PlainTime. For plain JSON it writes the same bytes as Temporal's default converter, and it reads anything the default converter wrote.
  • createDataConverter / clientDataConverter: both together, wired up for workers and clients.

The only runtime dependency is the peer @temporalio/common.

Install

pnpm add @norn-ts/codec

Requires Node 22.18+ (or any runtime with WebCrypto for the codec on its own).

Entry points

| Import | Contents | Runtimes | | --- | --- | --- | | @norn-ts/codec | Everything below, plus createDataConverter, clientDataConverter, resolvePayloadConverterPath and NornCodecError | Node (imports node:url for resolvePayloadConverterPath()) | | @norn-ts/codec/encryption | createEncryptionCodec, generateKey, EncryptionCodecOptions, ENCRYPTED_ENCODING, ENCRYPTION_KEY_ID_METADATA_KEY | Any runtime with WebCrypto: Node, Bun, Deno, Cloudflare Workers, Vercel Edge (type-only imports from @temporalio/common, nothing at runtime) | | @norn-ts/codec/payload-converter | payloadConverter, NornJsonPayloadConverter, TEMPORAL_TYPES | Anywhere @temporalio/common runs, including the workflow sandbox; this is the module payloadConverterPath points workers at |

Use @norn-ts/codec/encryption for codec servers on edge runtimes, so the bundle never pulls in node:url (no nodejs_compat flag needed on Cloudflare Workers):

import { createEncryptionCodec } from '@norn-ts/codec/encryption';

The codec still throws NornCodecError for bad keys and tampered payloads; check error.name === 'NornCodecError' if you need to tell them apart without importing the main entry.

Usage

Generate a key once and store it in your secret manager:

node -e "import('@norn-ts/codec').then(m => console.log(m.generateKey()))"

Worker

import { fileURLToPath } from 'node:url';
import { Worker } from '@temporalio/worker';
import { createDataConverter } from '@norn-ts/codec';

const keys = { '2026-10': process.env.NORN_KEY_2026_10! };

const worker = await Worker.create({
  connection,
  taskQueue: 'orders',
  workflowsPath: fileURLToPath(new URL('./workflows.js', import.meta.url)),
  dataConverter: createDataConverter({ keys, currentKeyId: '2026-10' }),
});

createDataConverter returns { payloadConverterPath, payloadCodecs }. The path makes the worker load the converter inside the workflow sandbox too, so workflows can receive and return bigints. If you prebuild bundles with bundleWorkflowCode, pass it the same path:

import { resolvePayloadConverterPath } from '@norn-ts/codec';
await bundleWorkflowCode({ workflowsPath, payloadConverterPath: resolvePayloadConverterPath() });

Client

import { Client } from '@temporalio/client';
import { clientDataConverter } from '@norn-ts/codec';

const client = new Client({ connection, dataConverter: clientDataConverter({ keys, currentKeyId: '2026-10' }) });
const total: bigint = await client.workflow.execute('sumInvoices', { taskQueue: 'orders', workflowId, args: [[1n, 2n]] });

clientDataConverter passes the converter object rather than a path, so it also works in bundled and edge deployments.

Use createEncryptionCodec directly if you only want encryption (with Temporal's default converter), and createDataConverter() without keys if you only want the converter. A codec option (for example compression) runs before encryption.

Codec server (Temporal UI / CLI)

The Temporal UI and temporal CLI can decode payloads through a codec server. Use codecServer() from @norn-ts/hono, which adds authentication, CORS, namespace checks and body limits:

app.route('/codec', codecServer({
  codec: createEncryptionCodec({ keys, currentKeyId: '2026-10' }),
  auth: (c) => c.req.header('authorization') === `Bearer ${env.CODEC_TOKEN}`,
}));

Key rotation

  1. Generate a new key and add it to keys everywhere (workers, clients, codec server), keeping currentKeyId unchanged. Deploy.
  2. Once every process has the new key, set currentKeyId to it. Deploy. New payloads use the new key; old ones still decrypt with the key named in their metadata.
  3. Keep the old key in keys until no retained history, schedule, or long-running workflow references it (that is, until the namespace's retention period has passed for all workflows that started before step 2, and running workflows have continued-as-new or finished). Then remove it.

Doing step 1 everywhere before step 2 matters: a process that receives a payload encrypted with a key it doesn't have fails with NornCodecError ("encrypted with key ..., which is not configured").

Enabling encryption on an existing namespace is safe: decode passes payloads that aren't binary/encrypted through untouched.

Format

An encrypted payload has metadata encoding: binary/encrypted and encryption-key-id: <id> (UTF-8), and data IV (12 random bytes) || AES-256-GCM ciphertext || tag (16 bytes). The plaintext is the original temporal.api.common.v1.Payload serialized as protobuf (so its metadata, including its original encoding, is encrypted too), and the additional authenticated data is the UTF-8 key id. This is easy to reimplement in another SDK's codec.

Tagged JSON values look like {"$norn":"bigint","v":"123"} and {"$norn":"Temporal.Instant","v":"2026-10-19T00:00:00Z"}. A tag only matches an object with exactly the keys $norn and v; your own objects with a $norn key are escaped when written and unknown tags are left alone when read. Date is not special-cased (it becomes an ISO string, as with the default converter). Temporal API values are revived with globalThis.Temporal.X.from(v); on runtimes without Temporal (Node 22, and the workflow sandbox there), decoding one throws a PayloadConverterError saying so.

Security notes

  • Keys must be exactly 32 random bytes (base64 or Uint8Array); createEncryptionCodec rejects anything else at startup. Error messages name key ids but never include key material. Key ids are stored in plaintext, so don't put secrets in them.
  • Keys are imported as non-extractable WebCrypto keys and the raw bytes are wiped once imported.
  • Each payload gets a fresh random IV. With random 96-bit IVs, NIST recommends at most about 2³² encryptions per key: rotate well before that.
  • GCM authenticates the data and key id, so tampering is detected (NornCodecError: ... could not be decrypted ...). It does not stop a payload from being replayed into a different place in history, and metadata outside the encrypted envelope (like the key id) is visible to the server.
  • Encryption covers payloads (inputs, results, signals, queries, updates, memos, heartbeat details, failure details encoded as payloads). It does not cover workflow ids, types, task queues, search attributes, or failure messages and stack traces, unless you also configure a failure converter with encodeCommonAttributes.
  • Decoding passes payloads that aren't binary/encrypted through unchanged, as Temporal's own codecs do, so a namespace with existing plaintext history keeps working after you turn encryption on. The flip side: a client deployed without the codec writes plaintext that workers accept silently, so configure the codec on every client and worker.
  • A codec server can decrypt everything it is given: put it behind authentication and restrict CORS to your Temporal UI origin.