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.