Skip to content

ScopeMask (JS/TS)

ScopeMask converts internal identifiers: database keys, emails, UUIDs, etc into short, opaque strings that are safe to expose in URLs and APIs, and decodes them back to the original value on demand. Each id is bound to a scope and a secret, with a keyed integrity check.

Runs in Node, Deno, Bun, and edge runtimes such as Cloudflare Workers and Vercel Edge.

Install

npm install scopemask
yarn add scopemask
pnpm add scopemask

Configuration

Create a ScopeMask with a secret. The secret is required and is the key every id is derived from; keep it private and stable.

import { ScopeMask } from "scopemask";

const scopeMask = new ScopeMask("parity-secret");

Optional settings:

Option Type Default Description
minLength number 16 Pad every id to at least this many characters.
baseAlphabet string ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 Characters ids are built from; must be unique.
previousSecrets (Uint8Array \| string)[] [] Extra secrets accepted when decoding only, so ids made with an old secret keep working after being rotated.

Encode and decode

import { ScopeMask } from "scopemask";

const scopeMask = new ScopeMask("parity-secret");

scopeMask.encode("user", 42);                    // "xgFeePgoWUZHCNLo"
scopeMask.decode("user", "xgFeePgoWUZHCNLo");    // 42

Value types

Numbers, bigints, strings, byte arrays, and UUIDs are supported. The original type is restored on decode.

import { UUID } from "scopemask";

scopeMask.encode("user", "hello");                  // "yqBiRnZBIdqXslkrXM"
scopeMask.encode("user", Uint8Array.of(0, 1, 255)); // "RLDIyRQmFljZ1gBD"
scopeMask.encode("user", UUID.parse("12345678-1234-5678-1234-567812345678"));
// "miQAnixf6TYaACwhThxDJ973X5vSuKqjp2W"

Scopes

The same value produces a different id in each scope.

scopeMask.encode("user", 42);     // "xgFeePgoWUZHCNLo"
scopeMask.encode("order", 42);    // "8DGttE8msCZHsJVG"

Prefixes

Add a prefix for readable ids. Pass the same prefix when decoding.

scopeMask.encode("user", 42, "id_");       // "id_xgFeePgoWUZHCNLo"
scopeMask.encode("webhook", 42, "whs_");   // "whs_jU5IIH0OxGnQg5u1"
scopeMask.decode("user", "id_xgFeePgoWUZHCNLo", "id_");   // 42

Bound scope

Bind a scope and prefix once, then call the same methods without repeating them.

const users = scopeMask.scope("user", "id_");

users.encode(42);                     // "id_xgFeePgoWUZHCNLo"
users.decode("id_xgFeePgoWUZHCNLo");  // 42
users.tryDecode("not-a-real-id");     // null

const ids = users.encodeMany([1, 2, 3]) as string[];
users.decodeMany(ids);                // [1, 2, 3]
users.tryDecodeMany(ids);             // [1, 2, 3]

Batch operations

const ids = scopeMask.encodeMany("user", [1, 2, 3]) as string[];
scopeMask.decodeMany("user", ids);   // [1, 2, 3]

Safe decoding

decode throws InvalidId on an invalid id. Use tryDecode to get null instead.

scopeMask.tryDecode("user", "not-a-real-id");          // null
scopeMask.tryDecodeMany("user", ["not-a-real-id"]);    // [null]
scopeMask.encode("user", null);                        // null

Custom configuration

import { ScopeMask } from "scopemask";

const scopeMask = new ScopeMask("parity-secret", {
  minLength: 24,
  baseAlphabet: "ABCDEFGHJKLMNPQRSTUVWXYZ23456789",
});
scopeMask.encode("user", 42);   // "527M4BZ6EU4YX3CYWDQ2GRAE"

// secret rotation: ids minted under an old secret still decode
const old = new ScopeMask("old-secret");
const enc = old.encode("user", 99);   // "CeAUI5UM6CeUJISr"

const rotated = new ScopeMask("new-secret", { previousSecrets: ["old-secret"] });
rotated.decode("user", enc!);   // 99

Additional resources

See Overview for more details.