@smonn/ids 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Public-facing branded IDs for TypeScript apps.
6
6
  pnpm add @smonn/ids
7
7
  ```
8
8
 
9
- Each ID looks like `usr_01h7b3k9rqxn4cw3p9r8t2sgkz`: a three-letter brand, an underscore, then 26 Crockford base32 characters encoding a 48-bit millisecond Unix timestamp followed by 80 random bits. Same byte layout as a [ULID](https://github.com/ulid/spec); see [ADR-0002](./docs/adr/0002-payload-layout.md) for the deliberate divergences.
9
+ Each ID looks like `usr_01h7b3k9rqxn4cw3p9r8t2sgkz`: a three-letter brand, an underscore, then 26 Crockford base32 characters of payload. The default codec encodes a 48-bit millisecond Unix timestamp followed by 80 random bits same byte layout as a [ULID](https://github.com/ulid/spec); see [ADR-0002](./docs/adr/0002-payload-layout.md) for the deliberate divergences. A second codec (`@smonn/ids/opaque`) keeps the same wire shape but encrypts the payload under a key, so the timestamp isn't readable from the ID.
10
10
 
11
11
  ## What this is for
12
12
 
@@ -117,7 +117,7 @@ Both `Options` fields are optional. Defaults are `Date.now` and an entropy harve
117
117
 
118
118
  ### "Catch a double-registered brand before it bites in production"
119
119
 
120
- The intended pattern is one codec per brand per process, constructed at module init. Calling `createId(brand)` a second time for the same brand usually means a bundling or import bug (accidental re-export, a test re-importing without resetting). In development (`process.env.NODE_ENV !== "production"`), the second call emits a one-shot `console.warn`; the brand-tracking registry is skipped in production. Tests that intentionally re-create codecs can opt out:
120
+ The intended pattern is one codec per brand per process, constructed at module init. Calling `createId(brand)` a second time for the same brand usually means a bundling or import bug (accidental re-export, a test re-importing without resetting). In development (`process.env.NODE_ENV !== "production"`), the second call emits a one-shot `console.warn`; the brand-tracking registry is skipped in production. The same registry covers cross-codec collisions: `createId("usr")` followed by `createOpaqueId("usr")` warns too, because codec choice is a per-brand commitment ([ADR-0007](./docs/adr/0007-wire-indistinguishable-codec-variants.md)). Tests that intentionally re-create codecs can opt out:
121
121
 
122
122
  ```ts
123
123
  const users = createId("usr", { allowDuplicateBrand: true });
@@ -164,12 +164,34 @@ The `pattern` describes the **canonical form only** — it matches `generate()`
164
164
 
165
165
  `example` is produced by calling `generate()` on each invocation, so it is fresh (non-deterministic) and always matches the returned `pattern`. One consequence: a codec wired with an injected `now` outside the 48-bit range — the same misconfiguration that breaks `generate()` — makes `toJsonSchema()` throw too.
166
166
 
167
+ ### "Don't leak creation time in IDs that customers can see"
168
+
169
+ The default codec exposes the creation timestamp by design — that's what makes `ORDER BY id` work. If that's a leak you can't accept (invoice IDs revealing billing cadence, signup IDs revealing acquisition velocity), use the opaque codec at `@smonn/ids/opaque`. Same `<brand>_<26 chars>` wire shape, but the payload is AES-encrypted under a key you supply.
170
+
171
+ ```ts
172
+ import { createOpaqueId, importOpaqueKey } from "@smonn/ids/opaque";
173
+
174
+ const key = await importOpaqueKey(new Uint8Array(16)); // 128- or 256-bit raw key
175
+ const invoices = createOpaqueId("inv", { key });
176
+
177
+ const id = await invoices.generate(); // "inv_…", timestamp not extractable without the key
178
+ await invoices.extractTimestamp(id); // Date — same codec, same key required
179
+ ```
180
+
181
+ Three differences from the default codec:
182
+
183
+ - **Async key-dependent methods.** WebCrypto is async-only, so `generate`, `generateAt`, and `extractTimestamp` return `Promise`s. `is`, `parse`, `safeParse`, `toJsonSchema`, and the Standard Schema adapter stay sync — they work on the wire form only ([ADR-0006](./docs/adr/0006-async-keyed-codec-contract.md)).
184
+ - **No `minIdForTime` / `maxIdForTime`.** Encrypted payloads don't sort by time. If you need time-range scans on opaque-coded entities, store the timestamp in a separate column.
185
+ - **Wire-indistinguishable from the default codec.** Codec choice is a per-brand commitment; the brand registry warns if you register the same brand against both in dev ([ADR-0007](./docs/adr/0007-wire-indistinguishable-codec-variants.md)).
186
+
187
+ Encryption is AES-CBC with a zero IV. That's deliberately safe here because the plaintext already carries 80 bits of entropy per ID; see [ADR-0004](./docs/adr/0004-aes-cbc-strip-trick.md) for the full rationale.
188
+
167
189
  ## What this is **not** for
168
190
 
169
191
  - **Internal surrogate primary keys.** If nobody outside your service ever sees the ID, the brand prefix and lenient parsing are dead weight. Use a `bigint` sequence.
170
192
  - **Wire-compatible ULIDs.** The byte layout is ULID-shaped but the encoding is lowercase and wrapped in a brand envelope. Stock ULID parsers will reject these.
171
193
  - **Distributed-trace / request-correlation IDs.** Use OpenTelemetry-format IDs.
172
- - **Hiding when your system launched.** Anyone with one known-time ID can compute the epoch offset. A custom epoch isn't supported, and wouldn't help anyway.
194
+ - **Hiding creation time with the default codec.** Anyone with one ID at a known creation time can compute the epoch offset. A custom epoch wouldn't help and isn't supported. To hide creation time per-ID, use the opaque codec (above).
173
195
 
174
196
  ## API surface
175
197
 
@@ -183,21 +205,28 @@ import {
183
205
  type ParseResult, // safeParse return type
184
206
  type JsonSchema, // toJsonSchema return type
185
207
  } from "@smonn/ids";
208
+
209
+ import {
210
+ createOpaqueId, // (brand: string, opts: { key, now?, rng?, allowDuplicateBrand? }) => OpaqueCodec<Brand>
211
+ importOpaqueKey, // (bytes: Uint8Array) => Promise<CryptoKey>
212
+ type OpaqueCodec, // returned by createOpaqueId
213
+ type OpaqueOptions, // { key, now, rng, allowDuplicateBrand } injection points
214
+ } from "@smonn/ids/opaque";
186
215
  ```
187
216
 
188
- ### `Codec<Brand>`
189
-
190
- | Method | Description |
191
- | ---------------------- | ----------------------------------------------------------------------------- |
192
- | `generate()` | Produce a fresh ID |
193
- | `generateAt(date)` | Produce a fresh ID with timestamp bytes from `date` (for backfills) |
194
- | `is(value)` | Strict type guard: `true` only for already-canonical strings |
195
- | `parse(value)` | Lenient: normalise to canonical, or throw |
196
- | `safeParse(value)` | Lenient: normalise to canonical, or return `{ ok: false, error }` |
197
- | `extractTimestamp(id)` | Decode the creation `Date` from an `Id<Brand>` (trusts the type) |
198
- | `minIdForTime(date)` | Tight lower bound for any ID generated at `date` (for range queries) |
199
- | `maxIdForTime(date)` | Tight upper bound for any ID generated at `date` (for range queries) |
200
- | `toJsonSchema()` | JSON Schema (`type`/`pattern`/`description`/`example`) for the canonical form |
217
+ ### Codec methods
218
+
219
+ | Method | `Codec<Brand>` | `OpaqueCodec<Brand>` | Description |
220
+ | ---------------------- | -------------- | -------------------- | ----------------------------------------------------------------------------- |
221
+ | `generate()` | sync | async | Produce a fresh ID |
222
+ | `generateAt(date)` | sync | async | Produce a fresh ID with timestamp bytes from `date` (for backfills) |
223
+ | `is(value)` | sync | sync | Strict type guard: `true` only for already-canonical strings |
224
+ | `parse(value)` | sync | sync | Lenient: normalise to canonical, or throw |
225
+ | `safeParse(value)` | sync | sync | Lenient: normalise to canonical, or return `{ ok: false, error }` |
226
+ | `extractTimestamp(id)` | sync | async | Decode the creation `Date` from an `Id<Brand>` (trusts the type) |
227
+ | `minIdForTime(date)` | sync | — | Tight lower bound for any ID generated at `date` (for range queries) |
228
+ | `maxIdForTime(date)` | sync | — | Tight upper bound for any ID generated at `date` (for range queries) |
229
+ | `toJsonSchema()` | sync | sync | JSON Schema (`type`/`pattern`/`description`/`example`) for the canonical form |
201
230
 
202
231
  ## CLI
203
232
 
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as createId } from "./id-CFxcBxi-.mjs";
2
+ import { t as createId } from "./id-B4GiFKYV.mjs";
3
3
  //#region src/cli.ts
4
4
  function run(opts) {
5
5
  const [subcommand, ...rest] = opts.argv;
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts","../bin/cli.ts"],"sourcesContent":["import { createId, type Id, type Options } from \"./id.js\";\n\nexport type RunOpts = {\n argv: ReadonlyArray<string>;\n stdout: (chunk: string) => void;\n stderr: (chunk: string) => void;\n now?: Options[\"now\"];\n rng?: Options[\"rng\"];\n};\n\nexport function run(opts: RunOpts): number {\n const [subcommand, ...rest] = opts.argv;\n if (subcommand === \"generate\" || subcommand === \"g\") return runGenerate(rest, opts);\n if (subcommand === \"inspect\" || subcommand === \"i\") return runInspect(rest, opts);\n if (subcommand === undefined || subcommand === \"--help\" || subcommand === \"-h\") {\n opts.stdout(usage());\n return 0;\n }\n opts.stderr(usage());\n return 1;\n}\n\nfunction usage(): string {\n return [\n \"Usage: ids <subcommand> [args]\",\n \"\",\n \"Subcommands:\",\n \" inspect, i <id> Decode an ID and print brand, timestamp, and canonical form.\",\n \" generate, g <brand> [--count, -c N] Mint one or more canonical IDs for the given brand.\",\n \"\",\n ].join(\"\\n\");\n}\n\nfunction runInspect(args: ReadonlyArray<string>, opts: RunOpts): number {\n const [input] = args;\n if (input === undefined) {\n opts.stderr(usage());\n return 1;\n }\n const brand = input.slice(0, 3).toLowerCase();\n let codec;\n try {\n codec = createId(brand, codecOpts(opts));\n } catch (err) {\n opts.stderr((err as Error).message + \"\\n\");\n return 1;\n }\n const validation = codec[\"~standard\"].validate(input);\n if (validation.issues) {\n opts.stderr(validation.issues[0]!.message + \"\\n\");\n return 1;\n }\n const canonical = validation.value;\n const timestamp = codec.extractTimestamp(canonical);\n const nowMs = (opts.now ?? Date.now)();\n const relative = formatRelative(timestamp.getTime(), nowMs);\n const inputLine = describeInputForm(input, canonical);\n opts.stdout(\n [\n `brand: ${brand}`,\n `timestamp: ${timestamp.toISOString()} (${relative})`,\n `canonical: ${canonical}`,\n `input: ${inputLine}`,\n \"\",\n ].join(\"\\n\"),\n );\n return 0;\n}\n\nfunction describeInputForm(input: string, canonical: Id<string>): string {\n if (input === canonical) return \"canonical\";\n const notes: string[] = [];\n if (input !== input.toLowerCase()) notes.push(\"was uppercase\");\n if (/[ilo]/i.test(input.slice(4))) notes.push(\"used Crockford aliases\");\n return `not canonical (${notes.join(\" + \")})`;\n}\n\nconst msPerSecond = 1000;\nconst msPerMinute = 60 * msPerSecond;\nconst msPerHour = 60 * msPerMinute;\nconst msPerDay = 24 * msPerHour;\nconst daysPerMonth = 30.44;\nconst monthsPerYear = 12;\n\nfunction formatRelative(thenMs: number, nowMs: number): string {\n const diff = nowMs - thenMs;\n const abs = Math.abs(diff);\n const suffix = diff < 0 ? \"from now\" : \"ago\";\n\n const head = headUnits(abs);\n return head === \"\" ? \"just now\" : `${head} ${suffix}`;\n}\n\nfunction headUnits(abs: number): string {\n if (abs < msPerMinute) return \"\";\n if (abs < msPerHour) return unit(Math.round(abs / msPerMinute), \"minute\");\n if (abs < msPerDay) return unit(Math.round(abs / msPerHour), \"hour\");\n if (abs < msPerDay * daysPerMonth) return unit(Math.round(abs / msPerDay), \"day\");\n\n const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));\n if (totalMonths < monthsPerYear) return unit(totalMonths, \"month\");\n\n const years = Math.floor(totalMonths / monthsPerYear);\n const months = totalMonths % monthsPerYear;\n return months === 0 ? unit(years, \"year\") : `${unit(years, \"year\")} ${unit(months, \"month\")}`;\n}\n\nfunction unit(n: number, name: string): string {\n return `${n} ${n === 1 ? name : `${name}s`}`;\n}\n\nfunction runGenerate(args: ReadonlyArray<string>, opts: RunOpts): number {\n const [brand, ...flags] = args;\n const count = parseCount(flags);\n if (typeof count === \"string\") {\n opts.stderr(count + \"\\n\");\n return 1;\n }\n let codec;\n try {\n codec = createId(brand ?? \"\", codecOpts(opts));\n } catch (err) {\n opts.stderr((err as Error).message + \"\\n\");\n return 1;\n }\n for (let i = 0; i < count; i++) opts.stdout(codec.generate() + \"\\n\");\n return 0;\n}\n\nfunction parseCount(flags: ReadonlyArray<string>): number | string {\n const idx = flags.findIndex((f) => f === \"--count\" || f === \"-c\");\n if (idx === -1) return 1;\n const raw = flags[idx + 1];\n if (raw === undefined) return \"--count requires a value\";\n if (!/^[1-9][0-9]*$/.test(raw)) return `--count must be a positive integer, got '${raw}'`;\n return Number(raw);\n}\n\nfunction codecOpts(opts: RunOpts): Partial<Options> {\n // CLI invocations are intentionally ephemeral — one codec per run, never\n // retained — so a repeated `createId(brand)` here is not the bundling/import\n // bug that the duplicate-brand warning is designed to catch.\n const o: Partial<Options> = { allowDuplicateBrand: true };\n if (opts.now !== undefined) o.now = opts.now;\n if (opts.rng !== undefined) o.rng = opts.rng;\n return o;\n}\n","#!/usr/bin/env node\nimport { run } from \"../src/cli.js\";\n\nprocess.exitCode = run({\n argv: process.argv.slice(2),\n stdout: (s) => process.stdout.write(s),\n stderr: (s) => process.stderr.write(s),\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts","../bin/cli.ts"],"sourcesContent":["import { createId, type Options } from \"./id.js\";\nimport type { Id } from \"./types.js\";\n\nexport type RunOpts = {\n argv: ReadonlyArray<string>;\n stdout: (chunk: string) => void;\n stderr: (chunk: string) => void;\n now?: Options[\"now\"];\n rng?: Options[\"rng\"];\n};\n\nexport function run(opts: RunOpts): number {\n const [subcommand, ...rest] = opts.argv;\n if (subcommand === \"generate\" || subcommand === \"g\") return runGenerate(rest, opts);\n if (subcommand === \"inspect\" || subcommand === \"i\") return runInspect(rest, opts);\n if (subcommand === undefined || subcommand === \"--help\" || subcommand === \"-h\") {\n opts.stdout(usage());\n return 0;\n }\n opts.stderr(usage());\n return 1;\n}\n\nfunction usage(): string {\n return [\n \"Usage: ids <subcommand> [args]\",\n \"\",\n \"Subcommands:\",\n \" inspect, i <id> Decode an ID and print brand, timestamp, and canonical form.\",\n \" generate, g <brand> [--count, -c N] Mint one or more canonical IDs for the given brand.\",\n \"\",\n ].join(\"\\n\");\n}\n\nfunction runInspect(args: ReadonlyArray<string>, opts: RunOpts): number {\n const [input] = args;\n if (input === undefined) {\n opts.stderr(usage());\n return 1;\n }\n const brand = input.slice(0, 3).toLowerCase();\n let codec;\n try {\n codec = createId(brand, codecOpts(opts));\n } catch (err) {\n opts.stderr((err as Error).message + \"\\n\");\n return 1;\n }\n const validation = codec[\"~standard\"].validate(input);\n if (validation.issues) {\n opts.stderr(validation.issues[0]!.message + \"\\n\");\n return 1;\n }\n const canonical = validation.value;\n const timestamp = codec.extractTimestamp(canonical);\n const nowMs = (opts.now ?? Date.now)();\n const relative = formatRelative(timestamp.getTime(), nowMs);\n const inputLine = describeInputForm(input, canonical);\n opts.stdout(\n [\n `brand: ${brand}`,\n `timestamp: ${timestamp.toISOString()} (${relative})`,\n `canonical: ${canonical}`,\n `input: ${inputLine}`,\n \"\",\n ].join(\"\\n\"),\n );\n return 0;\n}\n\nfunction describeInputForm(input: string, canonical: Id<string>): string {\n if (input === canonical) return \"canonical\";\n const notes: string[] = [];\n if (input !== input.toLowerCase()) notes.push(\"was uppercase\");\n if (/[ilo]/i.test(input.slice(4))) notes.push(\"used Crockford aliases\");\n return `not canonical (${notes.join(\" + \")})`;\n}\n\nconst msPerSecond = 1000;\nconst msPerMinute = 60 * msPerSecond;\nconst msPerHour = 60 * msPerMinute;\nconst msPerDay = 24 * msPerHour;\nconst daysPerMonth = 30.44;\nconst monthsPerYear = 12;\n\nfunction formatRelative(thenMs: number, nowMs: number): string {\n const diff = nowMs - thenMs;\n const abs = Math.abs(diff);\n const suffix = diff < 0 ? \"from now\" : \"ago\";\n\n const head = headUnits(abs);\n return head === \"\" ? \"just now\" : `${head} ${suffix}`;\n}\n\nfunction headUnits(abs: number): string {\n if (abs < msPerMinute) return \"\";\n if (abs < msPerHour) return unit(Math.round(abs / msPerMinute), \"minute\");\n if (abs < msPerDay) return unit(Math.round(abs / msPerHour), \"hour\");\n if (abs < msPerDay * daysPerMonth) return unit(Math.round(abs / msPerDay), \"day\");\n\n const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));\n if (totalMonths < monthsPerYear) return unit(totalMonths, \"month\");\n\n const years = Math.floor(totalMonths / monthsPerYear);\n const months = totalMonths % monthsPerYear;\n return months === 0 ? unit(years, \"year\") : `${unit(years, \"year\")} ${unit(months, \"month\")}`;\n}\n\nfunction unit(n: number, name: string): string {\n return `${n} ${n === 1 ? name : `${name}s`}`;\n}\n\nfunction runGenerate(args: ReadonlyArray<string>, opts: RunOpts): number {\n const [brand, ...flags] = args;\n const count = parseCount(flags);\n if (typeof count === \"string\") {\n opts.stderr(count + \"\\n\");\n return 1;\n }\n let codec;\n try {\n codec = createId(brand ?? \"\", codecOpts(opts));\n } catch (err) {\n opts.stderr((err as Error).message + \"\\n\");\n return 1;\n }\n for (let i = 0; i < count; i++) opts.stdout(codec.generate() + \"\\n\");\n return 0;\n}\n\nfunction parseCount(flags: ReadonlyArray<string>): number | string {\n const idx = flags.findIndex((f) => f === \"--count\" || f === \"-c\");\n if (idx === -1) return 1;\n const raw = flags[idx + 1];\n if (raw === undefined) return \"--count requires a value\";\n if (!/^[1-9][0-9]*$/.test(raw)) return `--count must be a positive integer, got '${raw}'`;\n return Number(raw);\n}\n\nfunction codecOpts(opts: RunOpts): Partial<Options> {\n // CLI invocations are intentionally ephemeral — one codec per run, never\n // retained — so a repeated `createId(brand)` here is not the bundling/import\n // bug that the duplicate-brand warning is designed to catch.\n const o: Partial<Options> = { allowDuplicateBrand: true };\n if (opts.now !== undefined) o.now = opts.now;\n if (opts.rng !== undefined) o.rng = opts.rng;\n return o;\n}\n","#!/usr/bin/env node\nimport { run } from \"../src/cli.js\";\n\nprocess.exitCode = run({\n argv: process.argv.slice(2),\n stdout: (s) => process.stdout.write(s),\n stderr: (s) => process.stderr.write(s),\n});\n"],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ import { a as readTimestampMs, c as writeTimestamp, d as decodeBase32, f as encodeBase32, i as payloadBase32Length, l as registerBrand, n as is, o as safeParse, r as parse, s as standardValidate, t as base32CharClass, u as validateBrand } from "./shared-CmbAeUdM.mjs";
2
+ //#region src/id.ts
3
+ const hexCharCodeToNibble = new Uint8Array(128);
4
+ for (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;
5
+ for (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;
6
+ const defaultOptions = {
7
+ now: Date.now,
8
+ rng: (target) => {
9
+ const s = crypto.randomUUID();
10
+ target[0] = hexCharCodeToNibble[s.charCodeAt(0)] << 4 | hexCharCodeToNibble[s.charCodeAt(1)];
11
+ target[1] = hexCharCodeToNibble[s.charCodeAt(2)] << 4 | hexCharCodeToNibble[s.charCodeAt(3)];
12
+ target[2] = hexCharCodeToNibble[s.charCodeAt(4)] << 4 | hexCharCodeToNibble[s.charCodeAt(5)];
13
+ target[3] = hexCharCodeToNibble[s.charCodeAt(6)] << 4 | hexCharCodeToNibble[s.charCodeAt(7)];
14
+ target[4] = hexCharCodeToNibble[s.charCodeAt(9)] << 4 | hexCharCodeToNibble[s.charCodeAt(10)];
15
+ target[5] = hexCharCodeToNibble[s.charCodeAt(11)] << 4 | hexCharCodeToNibble[s.charCodeAt(12)];
16
+ target[6] = hexCharCodeToNibble[s.charCodeAt(24)] << 4 | hexCharCodeToNibble[s.charCodeAt(25)];
17
+ target[7] = hexCharCodeToNibble[s.charCodeAt(26)] << 4 | hexCharCodeToNibble[s.charCodeAt(27)];
18
+ target[8] = hexCharCodeToNibble[s.charCodeAt(28)] << 4 | hexCharCodeToNibble[s.charCodeAt(29)];
19
+ target[9] = hexCharCodeToNibble[s.charCodeAt(30)] << 4 | hexCharCodeToNibble[s.charCodeAt(31)];
20
+ }
21
+ };
22
+ const randomByteLength = 10;
23
+ const timestampBase32Length = Math.ceil(48 / 5);
24
+ function createId(brand, opts = {}) {
25
+ validateBrand(brand);
26
+ registerBrand(brand, opts.allowDuplicateBrand);
27
+ const options = {
28
+ ...defaultOptions,
29
+ ...opts
30
+ };
31
+ const prefix = `${brand}_`;
32
+ const buffer = new Uint8Array(16);
33
+ const randomView = new Uint8Array(buffer.buffer, 6, randomByteLength);
34
+ return {
35
+ generate: () => generate(prefix, options, buffer, randomView),
36
+ generateAt: (date) => generate(prefix, options, buffer, randomView, date.getTime()),
37
+ is: (value) => is(prefix, value),
38
+ parse: (value) => parse(prefix, value),
39
+ safeParse: (value) => safeParse(prefix, value),
40
+ extractTimestamp: (id) => extractTimestamp(prefix, id),
41
+ minIdForTime: (date) => sentinelIdForTime(prefix, date, 0, buffer, randomView),
42
+ maxIdForTime: (date) => sentinelIdForTime(prefix, date, 255, buffer, randomView),
43
+ toJsonSchema: () => toJsonSchema(brand, prefix, options, buffer, randomView),
44
+ "~standard": {
45
+ version: 1,
46
+ vendor: "@smonn/ids",
47
+ validate: (value) => standardValidate(prefix, value)
48
+ }
49
+ };
50
+ }
51
+ function toJsonSchema(brand, prefix, options, buffer, randomView) {
52
+ return {
53
+ type: "string",
54
+ pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,
55
+ description: `Branded ID for '${brand}'`,
56
+ example: generate(prefix, options, buffer, randomView)
57
+ };
58
+ }
59
+ function generate(prefix, options, buffer, randomView, ms = options.now()) {
60
+ writeTimestamp(ms, buffer);
61
+ options.rng(randomView);
62
+ return prefix + encodeBase32(buffer);
63
+ }
64
+ function sentinelIdForTime(prefix, date, fill, buffer, randomView) {
65
+ writeTimestamp(date.getTime(), buffer);
66
+ randomView.fill(fill);
67
+ return prefix + encodeBase32(buffer);
68
+ }
69
+ function extractTimestamp(prefix, id) {
70
+ const base32 = id.slice(prefix.length, prefix.length + timestampBase32Length);
71
+ return new Date(readTimestampMs(decodeBase32(base32)));
72
+ }
73
+ //#endregion
74
+ export { createId as t };
75
+
76
+ //# sourceMappingURL=id-B4GiFKYV.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-B4GiFKYV.mjs","names":[],"sources":["../src/id.ts"],"sourcesContent":["import { decodeBase32, encodeBase32 } from \"./base32.js\";\nimport { registerBrand, validateBrand } from \"./registry.js\";\nimport {\n base32CharClass,\n is,\n parse,\n payloadBase32Length,\n payloadByteLength,\n readTimestampMs,\n safeParse,\n standardValidate,\n timestampByteLength,\n writeTimestamp,\n} from \"./shared.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\n\nexport type Options = {\n now: () => number;\n rng: (target: Uint8Array) => void;\n allowDuplicateBrand?: boolean;\n};\n\nexport type Codec<Brand extends string> = {\n generate(): Id<Brand>;\n generateAt(date: Date): Id<Brand>;\n is(value: unknown): value is Id<Brand>;\n parse(value: unknown): Id<Brand>;\n safeParse(value: unknown): ParseResult<Brand>;\n extractTimestamp(id: Id<Brand>): Date;\n minIdForTime(date: Date): Id<Brand>;\n maxIdForTime(date: Date): Id<Brand>;\n toJsonSchema(): JsonSchema;\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\n// hex charCode → 0–15 nibble, for decoding UUIDv4 strings into bytes.\n// Covers ['0'-'9' = 48–57] and ['a'-'f' = 97–102]; UUIDs are lowercase per spec.\nconst hexCharCodeToNibble = new Uint8Array(128);\nfor (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;\nfor (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;\n\nconst defaultOptions: Options = {\n now: Date.now,\n // crypto.randomUUID is ~7× faster than crypto.getRandomValues in Node 24\n // (~84 ns vs ~610 ns for a 16-byte fill — likely because the UUID path has\n // a tight fixed-format fast path). We use the 122 random bits of a UUIDv4\n // string as our entropy source, harvesting 10 fully-random bytes from\n // positions where no version (hex 12) or variant (hex 16) bits sit.\n // String layout: \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\" — bytes 0–5 are\n // string[0..7]+string[9..12], bytes 6–9 are string[24..31].\n rng: (target) => {\n const s = crypto.randomUUID();\n target[0] =\n (hexCharCodeToNibble[s.charCodeAt(0)]! << 4) | hexCharCodeToNibble[s.charCodeAt(1)]!;\n target[1] =\n (hexCharCodeToNibble[s.charCodeAt(2)]! << 4) | hexCharCodeToNibble[s.charCodeAt(3)]!;\n target[2] =\n (hexCharCodeToNibble[s.charCodeAt(4)]! << 4) | hexCharCodeToNibble[s.charCodeAt(5)]!;\n target[3] =\n (hexCharCodeToNibble[s.charCodeAt(6)]! << 4) | hexCharCodeToNibble[s.charCodeAt(7)]!;\n target[4] =\n (hexCharCodeToNibble[s.charCodeAt(9)]! << 4) | hexCharCodeToNibble[s.charCodeAt(10)]!;\n target[5] =\n (hexCharCodeToNibble[s.charCodeAt(11)]! << 4) | hexCharCodeToNibble[s.charCodeAt(12)]!;\n target[6] =\n (hexCharCodeToNibble[s.charCodeAt(24)]! << 4) | hexCharCodeToNibble[s.charCodeAt(25)]!;\n target[7] =\n (hexCharCodeToNibble[s.charCodeAt(26)]! << 4) | hexCharCodeToNibble[s.charCodeAt(27)]!;\n target[8] =\n (hexCharCodeToNibble[s.charCodeAt(28)]! << 4) | hexCharCodeToNibble[s.charCodeAt(29)]!;\n target[9] =\n (hexCharCodeToNibble[s.charCodeAt(30)]! << 4) | hexCharCodeToNibble[s.charCodeAt(31)]!;\n },\n};\n\nconst randomByteLength = payloadByteLength - timestampByteLength;\nconst timestampBase32Length = Math.ceil((timestampByteLength * 8) / 5);\n\nexport function createId<Brand extends string>(\n brand: Brand,\n opts: Partial<Options> = {},\n): Codec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const options = {\n ...defaultOptions,\n ...opts,\n } satisfies Options;\n\n const prefix: Prefix<Brand> = `${brand}_`;\n // Per-codec scratch buffer. Shared across generate(), generateAt(),\n // minIdForTime(), and maxIdForTime() — all are synchronous and overwrite both\n // the timestamp and random slices before encoding, so successive callers see\n // their own freshly-written bytes. encodeBase32 reads the buffer and\n // returns an independent string, so the caller never sees the buffer itself.\n const buffer = new Uint8Array(payloadByteLength);\n const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);\n\n return {\n generate: () => generate(prefix, options, buffer, randomView),\n generateAt: (date: Date) => generate(prefix, options, buffer, randomView, date.getTime()),\n is: (value: unknown) => is(prefix, value),\n parse: (value: unknown) => parse(prefix, value),\n safeParse: (value: unknown) => safeParse(prefix, value),\n extractTimestamp: (id: Id<Brand>) => extractTimestamp(prefix, id),\n minIdForTime: (date: Date) => sentinelIdForTime(prefix, date, 0x00, buffer, randomView),\n maxIdForTime: (date: Date) => sentinelIdForTime(prefix, date, 0xff, buffer, randomView),\n toJsonSchema: () => toJsonSchema(brand, prefix, options, buffer, randomView),\n \"~standard\": {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n },\n };\n}\n\nfunction toJsonSchema<Brand extends string>(\n brand: Brand,\n prefix: Prefix<Brand>,\n options: Options,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): JsonSchema {\n return {\n type: \"string\",\n pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,\n description: `Branded ID for '${brand}'`,\n example: generate(prefix, options, buffer, randomView),\n };\n}\n\nfunction generate<Brand extends string>(\n prefix: Prefix<Brand>,\n options: Options,\n buffer: Uint8Array,\n randomView: Uint8Array,\n ms: number = options.now(),\n): Id<Brand> {\n writeTimestamp(ms, buffer);\n options.rng(randomView);\n return (prefix + encodeBase32(buffer)) as Id<Brand>;\n}\n\nfunction sentinelIdForTime<Brand extends string>(\n prefix: Prefix<Brand>,\n date: Date,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): Id<Brand> {\n writeTimestamp(date.getTime(), buffer);\n randomView.fill(fill);\n return (prefix + encodeBase32(buffer)) as Id<Brand>;\n}\n\nfunction extractTimestamp<Brand extends string>(prefix: Prefix<Brand>, id: Id<Brand>): Date {\n const base32 = id.slice(prefix.length, prefix.length + timestampBase32Length);\n return new Date(readTimestampMs(decodeBase32(base32)));\n}\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,45 +1,11 @@
1
+ import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, r as ParseError, t as Id } from "./types-Dg2j_zlO.mjs";
2
+
1
3
  //#region src/id.d.ts
2
4
  type Options = {
3
5
  now: () => number;
4
6
  rng: (target: Uint8Array) => void;
5
7
  allowDuplicateBrand?: boolean;
6
8
  };
7
- type Prefix<Brand extends string> = `${Brand}_`;
8
- type Id<Brand extends string> = `${Prefix<Brand>}${string}` & {
9
- readonly __brand: Brand;
10
- };
11
- type ParseError = "not_string" | "invalid_prefix" | "invalid_base32";
12
- type ParseResult<Brand extends string> = {
13
- ok: true;
14
- id: Id<Brand>;
15
- } | {
16
- ok: false;
17
- error: ParseError;
18
- };
19
- type JsonSchema = {
20
- readonly type: "string";
21
- readonly pattern: string;
22
- readonly description: string;
23
- readonly example: string;
24
- };
25
- type StandardSchemaProps<Brand extends string> = {
26
- readonly version: 1;
27
- readonly vendor: "@smonn/ids";
28
- readonly validate: (value: unknown, options?: {
29
- readonly libraryOptions?: Record<string, unknown> | undefined;
30
- }) => {
31
- readonly value: Id<Brand>;
32
- readonly issues?: undefined;
33
- } | {
34
- readonly issues: ReadonlyArray<{
35
- readonly message: string;
36
- }>;
37
- };
38
- readonly types?: {
39
- readonly input: unknown;
40
- readonly output: Id<Brand>;
41
- };
42
- };
43
9
  type Codec<Brand extends string> = {
44
10
  generate(): Id<Brand>;
45
11
  generateAt(date: Date): Id<Brand>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/id.ts"],"mappings":";KAEY,OAAA;EACV,GAAA;EACA,GAAA,GAAM,MAAA,EAAQ,UAAA;EACd,mBAAA;AAAA;AAAA,KA2CG,MAAA,4BAAkC,KAAA;AAAA,KAE3B,EAAA,4BAA8B,MAAA,CAAO,KAAA;EAAA,SACtC,OAAA,EAAS,KAAA;AAAA;AAAA,KAGR,UAAA;AAAA,KAEA,WAAA;EACN,EAAA;EAAU,EAAA,EAAI,EAAA,CAAG,KAAA;AAAA;EACjB,EAAA;EAAW,KAAA,EAAO,UAAA;AAAA;AAAA,KAEZ,UAAA;EAAA,SACD,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;EAAA,SACA,OAAA;AAAA;AAAA,KAGN,mBAAA;EAAA,SACM,OAAA;EAAA,SACA,MAAA;EAAA,SACA,QAAA,GACP,KAAA,WACA,OAAA;IAAA,SAAqB,cAAA,GAAiB,MAAA;EAAA;IAAA,SAEzB,KAAA,EAAO,EAAA,CAAG,KAAA;IAAA,SAAiB,MAAA;EAAA;IAAA,SAC3B,MAAA,EAAQ,aAAA;MAAA,SAAyB,OAAA;IAAA;EAAA;EAAA,SACvC,KAAA;IAAA,SAAmB,KAAA;IAAA,SAAyB,MAAA,EAAQ,EAAA,CAAG,KAAA;EAAA;AAAA;AAAA,KAGtD,KAAA;EACV,QAAA,IAAY,EAAA,CAAG,KAAA;EACf,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAC3B,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;EAChC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EAC1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;EACvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;EACjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAC7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAC7B,YAAA,IAAgB,UAAA;EAAA,SACP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;AAAA,iBAsB5B,QAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,GAAM,OAAA,CAAQ,OAAA,IACb,KAAA,CAAM,KAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/id.ts"],"mappings":";;;KAgBY,OAAA;EACV,GAAA;EACA,GAAA,GAAM,MAAA,EAAQ,UAAA;EACd,mBAAA;AAAA;AAAA,KAGU,KAAA;EACV,QAAA,IAAY,EAAA,CAAG,KAAA;EACf,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAC3B,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;EAChC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EAC1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;EACvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;EACjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAC7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAC7B,YAAA,IAAgB,UAAA;EAAA,SACP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;AAAA,iBA8C5B,QAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,GAAM,OAAA,CAAQ,OAAA,IACb,KAAA,CAAM,KAAA"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as createId } from "./id-CFxcBxi-.mjs";
1
+ import { t as createId } from "./id-B4GiFKYV.mjs";
2
2
  export { createId };
@@ -0,0 +1,26 @@
1
+ import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, t as Id } from "./types-Dg2j_zlO.mjs";
2
+
3
+ //#region src/opaque.d.ts
4
+ type OpaqueOptions = {
5
+ key: CryptoKey;
6
+ now: () => number;
7
+ rng: (target: Uint8Array) => void;
8
+ allowDuplicateBrand?: boolean;
9
+ };
10
+ type OpaqueCodec<Brand extends string> = {
11
+ generate(): Promise<Id<Brand>>;
12
+ generateAt(date: Date): Promise<Id<Brand>>;
13
+ is(value: unknown): value is Id<Brand>;
14
+ parse(value: unknown): Id<Brand>;
15
+ safeParse(value: unknown): ParseResult<Brand>;
16
+ extractTimestamp(id: Id<Brand>): Promise<Date>;
17
+ toJsonSchema(): JsonSchema;
18
+ readonly "~standard": StandardSchemaProps<Brand>;
19
+ };
20
+ declare function importOpaqueKey(bytes: Uint8Array): Promise<CryptoKey>;
21
+ declare function createOpaqueId<Brand extends string>(brand: Brand, opts: {
22
+ key: CryptoKey;
23
+ } & Partial<Omit<OpaqueOptions, "key">>): OpaqueCodec<Brand>;
24
+ //#endregion
25
+ export { OpaqueCodec, OpaqueOptions, createOpaqueId, importOpaqueKey };
26
+ //# sourceMappingURL=opaque.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/opaque.ts"],"mappings":";;;KAgBY,aAAA;EACV,GAAA,EAAK,SAAA;EACL,GAAA;EACA,GAAA,GAAM,MAAA,EAAQ,UAAA;EACd,mBAAA;AAAA;AAAA,KAGU,WAAA;EACV,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA;EACvB,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;EACnC,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;EAChC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EAC1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;EACvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA,CAAQ,IAAA;EACzC,YAAA,IAAgB,UAAA;EAAA,SACP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;AAAA,iBAU5B,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;AAAA,iBAO5C,cAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA;EAAQ,GAAA,EAAK,SAAA;AAAA,IAAc,OAAA,CAAQ,IAAA,CAAK,aAAA,YACvC,WAAA,CAAY,KAAA"}
@@ -0,0 +1,70 @@
1
+ import { a as readTimestampMs, c as writeTimestamp, d as decodeBase32, f as encodeBase32, i as payloadBase32Length, l as registerBrand, n as is, o as safeParse, r as parse, s as standardValidate, t as base32CharClass, u as validateBrand } from "./shared-CmbAeUdM.mjs";
2
+ //#region src/opaque.ts
3
+ const zeroIv = new Uint8Array(16);
4
+ const pkcsPad = 16;
5
+ function defaultRng(target) {
6
+ crypto.getRandomValues(target);
7
+ }
8
+ function importOpaqueKey(bytes) {
9
+ return crypto.subtle.importKey("raw", bytes, "AES-CBC", false, ["encrypt", "decrypt"]);
10
+ }
11
+ function createOpaqueId(brand, opts) {
12
+ validateBrand(brand);
13
+ registerBrand(brand, opts.allowDuplicateBrand);
14
+ const key = opts.key;
15
+ const now = opts.now ?? Date.now;
16
+ const rng = opts.rng ?? defaultRng;
17
+ const prefix = `${brand}_`;
18
+ return {
19
+ generate: () => generate(prefix, key, rng, now()),
20
+ generateAt: (date) => generate(prefix, key, rng, date.getTime()),
21
+ is: (value) => is(prefix, value),
22
+ parse: (value) => parse(prefix, value),
23
+ safeParse: (value) => safeParse(prefix, value),
24
+ extractTimestamp: (id) => extractTimestamp(prefix, key, id),
25
+ toJsonSchema: () => toJsonSchema(brand, prefix),
26
+ "~standard": {
27
+ version: 1,
28
+ vendor: "@smonn/ids",
29
+ validate: (value) => standardValidate(prefix, value)
30
+ }
31
+ };
32
+ }
33
+ async function generate(prefix, key, rng, ms) {
34
+ const plaintext = new Uint8Array(16);
35
+ writeTimestamp(ms, plaintext);
36
+ rng(plaintext.subarray(6, 16));
37
+ return prefix + encodeBase32(new Uint8Array(await crypto.subtle.encrypt({
38
+ name: "AES-CBC",
39
+ iv: zeroIv
40
+ }, key, plaintext)).subarray(0, 16));
41
+ }
42
+ async function extractTimestamp(prefix, key, id) {
43
+ const c1 = decodeBase32(id.slice(prefix.length));
44
+ const c2Input = new Uint8Array(16);
45
+ for (let i = 0; i < 16; i++) c2Input[i] = pkcsPad ^ c1[i];
46
+ const c2Encrypted = new Uint8Array(await crypto.subtle.encrypt({
47
+ name: "AES-CBC",
48
+ iv: zeroIv
49
+ }, key, c2Input));
50
+ const ciphertext = new Uint8Array(32);
51
+ ciphertext.set(c1, 0);
52
+ ciphertext.set(c2Encrypted.subarray(0, 16), 16);
53
+ const plaintext = new Uint8Array(await crypto.subtle.decrypt({
54
+ name: "AES-CBC",
55
+ iv: zeroIv
56
+ }, key, ciphertext));
57
+ return new Date(readTimestampMs(plaintext));
58
+ }
59
+ function toJsonSchema(brand, prefix) {
60
+ return {
61
+ type: "string",
62
+ pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,
63
+ description: `Branded ID for '${brand}'`,
64
+ example: prefix + "0".repeat(payloadBase32Length)
65
+ };
66
+ }
67
+ //#endregion
68
+ export { createOpaqueId, importOpaqueKey };
69
+
70
+ //# sourceMappingURL=opaque.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opaque.mjs","names":[],"sources":["../src/opaque.ts"],"sourcesContent":["import { decodeBase32, encodeBase32 } from \"./base32.js\";\nimport { registerBrand, validateBrand } from \"./registry.js\";\nimport {\n base32CharClass,\n is,\n parse,\n payloadBase32Length,\n payloadByteLength,\n readTimestampMs,\n safeParse,\n standardValidate,\n timestampByteLength,\n writeTimestamp,\n} from \"./shared.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\n\nexport type OpaqueOptions = {\n key: CryptoKey;\n now: () => number;\n rng: (target: Uint8Array) => void;\n allowDuplicateBrand?: boolean;\n};\n\nexport type OpaqueCodec<Brand extends string> = {\n generate(): Promise<Id<Brand>>;\n generateAt(date: Date): Promise<Id<Brand>>;\n is(value: unknown): value is Id<Brand>;\n parse(value: unknown): Id<Brand>;\n safeParse(value: unknown): ParseResult<Brand>;\n extractTimestamp(id: Id<Brand>): Promise<Date>;\n toJsonSchema(): JsonSchema;\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\nconst zeroIv = new Uint8Array(payloadByteLength);\nconst pkcsPad = 0x10;\n\nfunction defaultRng(target: Uint8Array): void {\n crypto.getRandomValues(target as Uint8Array<ArrayBuffer>);\n}\n\nexport function importOpaqueKey(bytes: Uint8Array): Promise<CryptoKey> {\n return crypto.subtle.importKey(\"raw\", bytes as Uint8Array<ArrayBuffer>, \"AES-CBC\", false, [\n \"encrypt\",\n \"decrypt\",\n ]);\n}\n\nexport function createOpaqueId<Brand extends string>(\n brand: Brand,\n opts: { key: CryptoKey } & Partial<Omit<OpaqueOptions, \"key\">>,\n): OpaqueCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const key = opts.key;\n const now = opts.now ?? Date.now;\n const rng = opts.rng ?? defaultRng;\n const prefix: Prefix<Brand> = `${brand}_`;\n\n return {\n generate: () => generate(prefix, key, rng, now()),\n generateAt: (date: Date) => generate(prefix, key, rng, date.getTime()),\n is: (value: unknown) => is(prefix, value),\n parse: (value: unknown) => parse(prefix, value),\n safeParse: (value: unknown) => safeParse(prefix, value),\n extractTimestamp: (id: Id<Brand>) => extractTimestamp(prefix, key, id),\n toJsonSchema: () => toJsonSchema(brand, prefix),\n \"~standard\": {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n },\n };\n}\n\n// Per-call buffers, unlike id.ts's codec-shared scratch. Reuse would be safe\n// (subtle.encrypt/decrypt snapshot inputs synchronously, per WebCrypto step 2\n// before the Promise returns) but subtle dominates this path — the allocation\n// is <1% of total cost, not worth pinning the design to that spec detail.\nasync function generate<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n rng: (target: Uint8Array) => void,\n ms: number,\n): Promise<Id<Brand>> {\n const plaintext = new Uint8Array(payloadByteLength);\n writeTimestamp(ms, plaintext);\n rng(plaintext.subarray(timestampByteLength, payloadByteLength));\n const encrypted = new Uint8Array(\n await crypto.subtle.encrypt({ name: \"AES-CBC\", iv: zeroIv }, key, plaintext),\n );\n return (prefix + encodeBase32(encrypted.subarray(0, payloadByteLength))) as Id<Brand>;\n}\n\nasync function extractTimestamp<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n id: Id<Brand>,\n): Promise<Date> {\n const c1 = decodeBase32(id.slice(prefix.length));\n // Reconstruct C2 = AES_K(P2 XOR C1) where P2 is the PKCS#7 pad block (0x10×16).\n // CBC encrypt of (P2 XOR C1) with IV=0 yields AES_K(P2 XOR C1) as the first 16 bytes.\n const c2Input = new Uint8Array(payloadByteLength);\n for (let i = 0; i < payloadByteLength; i++) c2Input[i] = pkcsPad ^ c1[i]!;\n const c2Encrypted = new Uint8Array(\n await crypto.subtle.encrypt({ name: \"AES-CBC\", iv: zeroIv }, key, c2Input),\n );\n const ciphertext = new Uint8Array(payloadByteLength * 2);\n ciphertext.set(c1, 0);\n ciphertext.set(c2Encrypted.subarray(0, payloadByteLength), payloadByteLength);\n const plaintext = new Uint8Array(\n await crypto.subtle.decrypt({ name: \"AES-CBC\", iv: zeroIv }, key, ciphertext),\n );\n return new Date(readTimestampMs(plaintext));\n}\n\nfunction toJsonSchema<Brand extends string>(brand: Brand, prefix: Prefix<Brand>): JsonSchema {\n return {\n type: \"string\",\n pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,\n description: `Branded ID for '${brand}'`,\n // The Opaque codec cannot synchronously produce a real example (encrypt is\n // async). A deterministic structurally-valid placeholder satisfies the\n // JSON Schema contract without requiring the key at schema time.\n example: prefix + \"0\".repeat(payloadBase32Length),\n };\n}\n"],"mappings":";;AAkCA,MAAM,SAAS,IAAI,WAAA,EAA4B;AAC/C,MAAM,UAAU;AAEhB,SAAS,WAAW,QAA0B;CAC5C,OAAO,gBAAgB,MAAiC;AAC1D;AAEA,SAAgB,gBAAgB,OAAuC;CACrE,OAAO,OAAO,OAAO,UAAU,OAAO,OAAkC,WAAW,OAAO,CACxF,WACA,SACF,CAAC;AACH;AAEA,SAAgB,eACd,OACA,MACoB;CACpB,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAE7C,MAAM,MAAM,KAAK;CACjB,MAAM,MAAM,KAAK,OAAO,KAAK;CAC7B,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,SAAwB,GAAG,MAAM;CAEvC,OAAO;EACL,gBAAgB,SAAS,QAAQ,KAAK,KAAK,IAAI,CAAC;EAChD,aAAa,SAAe,SAAS,QAAQ,KAAK,KAAK,KAAK,QAAQ,CAAC;EACrE,KAAK,UAAmB,GAAG,QAAQ,KAAK;EACxC,QAAQ,UAAmB,MAAM,QAAQ,KAAK;EAC9C,YAAY,UAAmB,UAAU,QAAQ,KAAK;EACtD,mBAAmB,OAAkB,iBAAiB,QAAQ,KAAK,EAAE;EACrE,oBAAoB,aAAa,OAAO,MAAM;EAC9C,aAAa;GACX,SAAS;GACT,QAAQ;GACR,WAAW,UAAmB,iBAAiB,QAAQ,KAAK;EAC9D;CACF;AACF;AAMA,eAAe,SACb,QACA,KACA,KACA,IACoB;CACpB,MAAM,YAAY,IAAI,WAAA,EAA4B;CAClD,eAAe,IAAI,SAAS;CAC5B,IAAI,UAAU,SAAA,GAAA,EAA+C,CAAC;CAI9D,OAAQ,SAAS,aAAa,IAHR,WACpB,MAAM,OAAO,OAAO,QAAQ;EAAE,MAAM;EAAW,IAAI;CAAO,GAAG,KAAK,SAAS,CAEvC,EAAE,SAAS,GAAA,EAAoB,CAAC;AACxE;AAEA,eAAe,iBACb,QACA,KACA,IACe;CACf,MAAM,KAAK,aAAa,GAAG,MAAM,OAAO,MAAM,CAAC;CAG/C,MAAM,UAAU,IAAI,WAAA,EAA4B;CAChD,KAAK,IAAI,IAAI,GAAG,IAAA,IAAuB,KAAK,QAAQ,KAAK,UAAU,GAAG;CACtE,MAAM,cAAc,IAAI,WACtB,MAAM,OAAO,OAAO,QAAQ;EAAE,MAAM;EAAW,IAAI;CAAO,GAAG,KAAK,OAAO,CAC3E;CACA,MAAM,aAAa,IAAI,WAAA,EAAgC;CACvD,WAAW,IAAI,IAAI,CAAC;CACpB,WAAW,IAAI,YAAY,SAAS,GAAA,EAAoB,GAAA,EAAoB;CAC5E,MAAM,YAAY,IAAI,WACpB,MAAM,OAAO,OAAO,QAAQ;EAAE,MAAM;EAAW,IAAI;CAAO,GAAG,KAAK,UAAU,CAC9E;CACA,OAAO,IAAI,KAAK,gBAAgB,SAAS,CAAC;AAC5C;AAEA,SAAS,aAAmC,OAAc,QAAmC;CAC3F,OAAO;EACL,MAAM;EACN,SAAS,IAAI,SAAS,gBAAgB,GAAG,oBAAoB;EAC7D,aAAa,mBAAmB,MAAM;EAItC,SAAS,SAAS,IAAI,OAAO,mBAAmB;CAClD;AACF"}
@@ -0,0 +1,122 @@
1
+ //#region src/base32.ts
2
+ const alphabet = "0123456789abcdefghjkmnpqrstvwxyz";
3
+ const valueToCharCode = new Uint8Array(32);
4
+ for (let i = 0; i < 32; i++) valueToCharCode[i] = alphabet.charCodeAt(i);
5
+ const charCodeToValue = new Uint8Array(256).fill(255);
6
+ for (let i = 0; i < 32; i++) charCodeToValue[alphabet.charCodeAt(i)] = i;
7
+ function encodeBase32(bytes) {
8
+ const codes = new Array(Math.floor(bytes.length * 8 / 5) + 1);
9
+ let chi = 0;
10
+ let bits = 0;
11
+ let value = 0;
12
+ for (let i = 0; i < bytes.length; i++) {
13
+ value = value << 8 | bytes[i];
14
+ bits += 8;
15
+ while (bits >= 5) {
16
+ bits -= 5;
17
+ codes[chi++] = valueToCharCode[value >>> bits & 31];
18
+ }
19
+ }
20
+ codes[chi] = valueToCharCode[value << 5 - bits & 31];
21
+ return String.fromCharCode.apply(null, codes);
22
+ }
23
+ function decodeBase32(str) {
24
+ const result = new Uint8Array(Math.floor(str.length * 5 / 8));
25
+ let bits = 0;
26
+ let value = 0;
27
+ let index = 0;
28
+ for (let i = 0; i < str.length; i++) {
29
+ const v = charCodeToValue[str.charCodeAt(i)];
30
+ value = value << 5 | v;
31
+ bits += 5;
32
+ if (bits >= 8) {
33
+ bits -= 8;
34
+ result[index++] = value >>> bits & 255;
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ //#endregion
40
+ //#region src/registry.ts
41
+ const brandPattern = /^[a-z]{3}$/;
42
+ const registeredBrands = /* @__PURE__ */ new Set();
43
+ const warnedBrands = /* @__PURE__ */ new Set();
44
+ function validateBrand(brand) {
45
+ if (!brandPattern.test(brand)) throw new Error("invalid brand, expected three lowercase a-z characters");
46
+ }
47
+ function registerBrand(brand, allowDuplicateBrand) {
48
+ if (typeof process === "undefined" || process.env.NODE_ENV === "production" || allowDuplicateBrand) return;
49
+ if (registeredBrands.has(brand)) {
50
+ if (!warnedBrands.has(brand)) {
51
+ console.warn(`[@smonn/ids] brand "${brand}" was registered more than once — this usually indicates a bundling or import bug, or that more than one codec variant is using the same brand. Pass { allowDuplicateBrand: true } to silence.`);
52
+ warnedBrands.add(brand);
53
+ }
54
+ } else registeredBrands.add(brand);
55
+ }
56
+ const payloadBase32Length = Math.ceil(128 / 5);
57
+ const base32CharClass = "[0-9a-hjkmnp-tv-z]";
58
+ const replacePattern = /[ilo]/g;
59
+ const aliasTestPattern = /[ilo]/;
60
+ const replacer = (match) => match === "o" ? "0" : "1";
61
+ const base32Pattern = new RegExp(`^[${alphabet}]{${payloadBase32Length}}$`);
62
+ function safeParse(prefix, value) {
63
+ if (typeof value !== "string") return {
64
+ ok: false,
65
+ error: "not_string"
66
+ };
67
+ const lowercase = value.toLowerCase();
68
+ if (!lowercase.startsWith(prefix)) return {
69
+ ok: false,
70
+ error: "invalid_prefix"
71
+ };
72
+ const sliced = lowercase.slice(prefix.length);
73
+ const base32 = aliasTestPattern.test(sliced) ? sliced.replaceAll(replacePattern, replacer) : sliced;
74
+ if (!base32Pattern.test(base32)) return {
75
+ ok: false,
76
+ error: "invalid_base32"
77
+ };
78
+ return {
79
+ ok: true,
80
+ id: prefix + base32
81
+ };
82
+ }
83
+ function parse(prefix, value) {
84
+ const result = safeParse(prefix, value);
85
+ if (result.ok) return result.id;
86
+ throw new Error(`Invalid ID: ${result.error}`);
87
+ }
88
+ function is(prefix, value) {
89
+ if (typeof value !== "string") return false;
90
+ if (!value.startsWith(prefix)) return false;
91
+ return base32Pattern.test(value.slice(prefix.length));
92
+ }
93
+ function errorMessage(prefix, error) {
94
+ switch (error) {
95
+ case "not_string": return "expected string";
96
+ case "invalid_prefix": return `expected prefix '${prefix}'`;
97
+ case "invalid_base32": return "invalid base32 payload";
98
+ }
99
+ }
100
+ function writeTimestamp(ms, buffer) {
101
+ if (Number.isNaN(ms)) throw new Error("timestamp is not a number");
102
+ if (ms < 0) throw new Error("timestamp is negative");
103
+ if (ms >= 2 ** 48) throw new Error("timestamp exceeds 48-bit range");
104
+ for (let i = 5; i >= 0; i--) {
105
+ buffer[i] = ms % 256;
106
+ ms = Math.floor(ms / 256);
107
+ }
108
+ }
109
+ function readTimestampMs(buffer) {
110
+ let ms = 0;
111
+ for (let i = 0; i < 6; i++) ms = ms * 256 + buffer[i];
112
+ return ms;
113
+ }
114
+ function standardValidate(prefix, value) {
115
+ const result = safeParse(prefix, value);
116
+ if (result.ok) return { value: result.id };
117
+ return { issues: [{ message: errorMessage(prefix, result.error) }] };
118
+ }
119
+ //#endregion
120
+ export { readTimestampMs as a, writeTimestamp as c, decodeBase32 as d, encodeBase32 as f, payloadBase32Length as i, registerBrand as l, is as n, safeParse as o, parse as r, standardValidate as s, base32CharClass as t, validateBrand as u };
121
+
122
+ //# sourceMappingURL=shared-CmbAeUdM.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared-CmbAeUdM.mjs","names":[],"sources":["../src/base32.ts","../src/registry.ts","../src/shared.ts"],"sourcesContent":["/*\n This is based on Crockford's Base32 spec: https://www.crockford.com/base32.html\n One difference is that it uses lowercase instead of uppercase when encoding.\n\n These functions are internal: callers (id.ts) guarantee that input is a\n 16-byte buffer for encode, or a string of characters drawn from the alphabet\n for decode. Invalid input produces silent garbage rather than a thrown error,\n consistent with the trust-the-type rule in ADR-0003.\n*/\n\nexport const alphabet = \"0123456789abcdefghjkmnpqrstvwxyz\";\n\n// 0–31 → ASCII char code, for write-into-codes-then-fromCharCode encoding.\nconst valueToCharCode = new Uint8Array(32);\nfor (let i = 0; i < 32; i++) valueToCharCode[i] = alphabet.charCodeAt(i);\n\n// charCode → 0–31 value. Canonical lowercase only; upstream resolves case and\n// o/i/l aliases before any string reaches decodeBase32.\nconst INVALID = 0xff;\nconst charCodeToValue = new Uint8Array(256).fill(INVALID);\nfor (let i = 0; i < alphabet.length; i++) charCodeToValue[alphabet.charCodeAt(i)] = i;\n\nexport function encodeBase32(bytes: Uint8Array): string {\n // Build an Array<number> of char codes and pass it to fromCharCode.apply.\n // Faster than `result += char` (avoids cons-string overhead) and than\n // Uint8Array variants (apply has a fast path for plain Arrays).\n // oxlint-disable-next-line no-new-array\n const codes = new Array<number>(Math.floor((bytes.length * 8) / 5) + 1);\n let chi = 0;\n let bits = 0;\n let value = 0;\n\n for (let i = 0; i < bytes.length; i++) {\n value = (value << 8) | bytes[i]!;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n codes[chi++] = valueToCharCode[(value >>> bits) & 0x1f]!;\n }\n }\n codes[chi] = valueToCharCode[(value << (5 - bits)) & 0x1f]!;\n return String.fromCharCode.apply(null, codes);\n}\n\nexport function decodeBase32(str: string): Uint8Array {\n const result = new Uint8Array(Math.floor((str.length * 5) / 8));\n let bits = 0;\n let value = 0;\n let index = 0;\n\n for (let i = 0; i < str.length; i++) {\n const v = charCodeToValue[str.charCodeAt(i)]!;\n value = (value << 5) | v;\n bits += 5;\n if (bits >= 8) {\n bits -= 8;\n result[index++] = (value >>> bits) & 0xff;\n }\n }\n return result;\n}\n","const brandPattern = /^[a-z]{3}$/;\nconst registeredBrands = new Set<string>();\nconst warnedBrands = new Set<string>();\n\nexport function validateBrand(brand: string): void {\n if (!brandPattern.test(brand)) {\n throw new Error(\"invalid brand, expected three lowercase a-z characters\");\n }\n}\n\nexport function registerBrand(brand: string, allowDuplicateBrand: boolean | undefined): void {\n if (\n typeof process === \"undefined\" ||\n process.env.NODE_ENV === \"production\" ||\n allowDuplicateBrand\n ) {\n return;\n }\n\n if (registeredBrands.has(brand)) {\n if (!warnedBrands.has(brand)) {\n console.warn(\n `[@smonn/ids] brand \"${brand}\" was registered more than once — this usually indicates a bundling or import bug, or that more than one codec variant is using the same brand. Pass { allowDuplicateBrand: true } to silence.`,\n );\n warnedBrands.add(brand);\n }\n } else {\n registeredBrands.add(brand);\n }\n}\n","import { alphabet } from \"./base32.js\";\nimport type { Id, ParseError, ParseResult, Prefix } from \"./types.js\";\n\n// Payload is always 16 bytes on the wire (every codec). 16 bytes → 26 Crockford\n// base32 chars. ADR-0002 codifies this as the shared wire-format invariant.\nexport const payloadByteLength: number = 16;\nexport const payloadBase32Length: number = Math.ceil((payloadByteLength * 8) / 5);\n\n// Compact regex character class for the canonical lowercase Crockford alphabet\n// (`0123456789abcdefghjkmnpqrstvwxyz` — excludes i, l, o, u). Used in the JSON\n// Schema `pattern`, which describes the canonical wire form only (ADR-0003).\nexport const base32CharClass: string = \"[0-9a-hjkmnp-tv-z]\";\n\nconst replacePattern = /[ilo]/g;\nconst aliasTestPattern = /[ilo]/;\nconst replacer = (match: string): string => (match === \"o\" ? \"0\" : \"1\");\nconst base32Pattern = new RegExp(`^[${alphabet}]{${payloadBase32Length}}$`);\n\nexport function safeParse<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n): ParseResult<Brand> {\n if (typeof value !== \"string\") return { ok: false, error: \"not_string\" };\n const lowercase = value.toLowerCase();\n if (!lowercase.startsWith(prefix)) return { ok: false, error: \"invalid_prefix\" };\n\n const sliced = lowercase.slice(prefix.length);\n const base32 = aliasTestPattern.test(sliced)\n ? sliced.replaceAll(replacePattern, replacer)\n : sliced;\n\n if (!base32Pattern.test(base32)) return { ok: false, error: \"invalid_base32\" };\n\n const id = (prefix + base32) as Id<Brand>;\n return { ok: true, id };\n}\n\nexport function parse<Brand extends string>(prefix: Prefix<Brand>, value: unknown): Id<Brand> {\n const result = safeParse(prefix, value);\n if (result.ok) return result.id;\n throw new Error(`Invalid ID: ${result.error}`);\n}\n\nexport function is<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n): value is Id<Brand> {\n if (typeof value !== \"string\") return false;\n if (!value.startsWith(prefix)) return false;\n return base32Pattern.test(value.slice(prefix.length));\n}\n\nfunction errorMessage<Brand extends string>(prefix: Prefix<Brand>, error: ParseError): string {\n switch (error) {\n case \"not_string\":\n return \"expected string\";\n case \"invalid_prefix\":\n return `expected prefix '${prefix}'`;\n case \"invalid_base32\":\n return \"invalid base32 payload\";\n }\n}\n\n// Timestamp byte layout: first N bytes of the plaintext payload encode a\n// big-endian Unix-ms timestamp. Shared by every codec whose plaintext begins\n// with a timestamp (Timestamp, Opaque, Signed, Reverse). The Derived codec\n// does not use this.\nexport const timestampByteLength: number = 6;\n\n// Write the timestamp in big-endian; encoded via mod-256 to avoid 32-bit bitwise coercion.\nexport function writeTimestamp(ms: number, buffer: Uint8Array): void {\n if (Number.isNaN(ms)) throw new Error(\"timestamp is not a number\");\n if (ms < 0) throw new Error(\"timestamp is negative\");\n if (ms >= 2 ** (timestampByteLength * 8)) {\n throw new Error(\"timestamp exceeds 48-bit range\");\n }\n for (let i = timestampByteLength - 1; i >= 0; i--) {\n buffer[i] = ms % 256;\n ms = Math.floor(ms / 256);\n }\n}\n\n// Decode the first `timestampByteLength` bytes of a buffer as a big-endian\n// unsigned millisecond timestamp.\nexport function readTimestampMs(buffer: Uint8Array): number {\n let ms = 0;\n for (let i = 0; i < timestampByteLength; i++) ms = ms * 256 + buffer[i]!;\n return ms;\n}\n\nexport function standardValidate<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n):\n | { readonly value: Id<Brand>; readonly issues?: undefined }\n | { readonly issues: ReadonlyArray<{ readonly message: string }> } {\n const result = safeParse(prefix, value);\n if (result.ok) return { value: result.id };\n return { issues: [{ message: errorMessage(prefix, result.error) }] };\n}\n"],"mappings":";AAUA,MAAa,WAAW;AAGxB,MAAM,kBAAkB,IAAI,WAAW,EAAE;AACzC,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,gBAAgB,KAAK,SAAS,WAAW,CAAC;AAKvE,MAAM,kBAAkB,IAAI,WAAW,GAAG,EAAE,KAAK,GAAO;AACxD,KAAK,IAAI,IAAI,GAAG,IAAI,IAAiB,KAAK,gBAAgB,SAAS,WAAW,CAAC,KAAK;AAEpF,SAAgB,aAAa,OAA2B;CAKtD,MAAM,QAAQ,IAAI,MAAc,KAAK,MAAO,MAAM,SAAS,IAAK,CAAC,IAAI,CAAC;CACtE,IAAI,MAAM;CACV,IAAI,OAAO;CACX,IAAI,QAAQ;CAEZ,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,QAAS,SAAS,IAAK,MAAM;EAC7B,QAAQ;EACR,OAAO,QAAQ,GAAG;GAChB,QAAQ;GACR,MAAM,SAAS,gBAAiB,UAAU,OAAQ;EACpD;CACF;CACA,MAAM,OAAO,gBAAiB,SAAU,IAAI,OAAS;CACrD,OAAO,OAAO,aAAa,MAAM,MAAM,KAAK;AAC9C;AAEA,SAAgB,aAAa,KAAyB;CACpD,MAAM,SAAS,IAAI,WAAW,KAAK,MAAO,IAAI,SAAS,IAAK,CAAC,CAAC;CAC9D,IAAI,OAAO;CACX,IAAI,QAAQ;CACZ,IAAI,QAAQ;CAEZ,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;EACnC,MAAM,IAAI,gBAAgB,IAAI,WAAW,CAAC;EAC1C,QAAS,SAAS,IAAK;EACvB,QAAQ;EACR,IAAI,QAAQ,GAAG;GACb,QAAQ;GACR,OAAO,WAAY,UAAU,OAAQ;EACvC;CACF;CACA,OAAO;AACT;;;AC5DA,MAAM,eAAe;AACrB,MAAM,mCAAmB,IAAI,IAAY;AACzC,MAAM,+BAAe,IAAI,IAAY;AAErC,SAAgB,cAAc,OAAqB;CACjD,IAAI,CAAC,aAAa,KAAK,KAAK,GAC1B,MAAM,IAAI,MAAM,wDAAwD;AAE5E;AAEA,SAAgB,cAAc,OAAe,qBAAgD;CAC3F,IACE,OAAO,YAAY,eACnB,QAAQ,IAAI,aAAa,gBACzB,qBAEA;CAGF,IAAI,iBAAiB,IAAI,KAAK;MACxB,CAAC,aAAa,IAAI,KAAK,GAAG;GAC5B,QAAQ,KACN,uBAAuB,MAAM,+LAC/B;GACA,aAAa,IAAI,KAAK;EACxB;QAEA,iBAAiB,IAAI,KAAK;AAE9B;ACvBA,MAAa,sBAA8B,KAAK,KAAA,MAA+B,CAAC;AAKhF,MAAa,kBAA0B;AAEvC,MAAM,iBAAiB;AACvB,MAAM,mBAAmB;AACzB,MAAM,YAAY,UAA2B,UAAU,MAAM,MAAM;AACnE,MAAM,gBAAgB,IAAI,OAAO,KAAK,SAAS,IAAI,oBAAoB,GAAG;AAE1E,SAAgB,UACd,QACA,OACoB;CACpB,IAAI,OAAO,UAAU,UAAU,OAAO;EAAE,IAAI;EAAO,OAAO;CAAa;CACvE,MAAM,YAAY,MAAM,YAAY;CACpC,IAAI,CAAC,UAAU,WAAW,MAAM,GAAG,OAAO;EAAE,IAAI;EAAO,OAAO;CAAiB;CAE/E,MAAM,SAAS,UAAU,MAAM,OAAO,MAAM;CAC5C,MAAM,SAAS,iBAAiB,KAAK,MAAM,IACvC,OAAO,WAAW,gBAAgB,QAAQ,IAC1C;CAEJ,IAAI,CAAC,cAAc,KAAK,MAAM,GAAG,OAAO;EAAE,IAAI;EAAO,OAAO;CAAiB;CAG7E,OAAO;EAAE,IAAI;EAAM,IADP,SAAS;CACC;AACxB;AAEA,SAAgB,MAA4B,QAAuB,OAA2B;CAC5F,MAAM,SAAS,UAAU,QAAQ,KAAK;CACtC,IAAI,OAAO,IAAI,OAAO,OAAO;CAC7B,MAAM,IAAI,MAAM,eAAe,OAAO,OAAO;AAC/C;AAEA,SAAgB,GACd,QACA,OACoB;CACpB,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,CAAC,MAAM,WAAW,MAAM,GAAG,OAAO;CACtC,OAAO,cAAc,KAAK,MAAM,MAAM,OAAO,MAAM,CAAC;AACtD;AAEA,SAAS,aAAmC,QAAuB,OAA2B;CAC5F,QAAQ,OAAR;EACE,KAAK,cACH,OAAO;EACT,KAAK,kBACH,OAAO,oBAAoB,OAAO;EACpC,KAAK,kBACH,OAAO;CACX;AACF;AASA,SAAgB,eAAe,IAAY,QAA0B;CACnE,IAAI,OAAO,MAAM,EAAE,GAAG,MAAM,IAAI,MAAM,2BAA2B;CACjE,IAAI,KAAK,GAAG,MAAM,IAAI,MAAM,uBAAuB;CACnD,IAAI,MAAM,KAAA,IACR,MAAM,IAAI,MAAM,gCAAgC;CAElD,KAAK,IAAI,IAAA,GAA6B,KAAK,GAAG,KAAK;EACjD,OAAO,KAAK,KAAK;EACjB,KAAK,KAAK,MAAM,KAAK,GAAG;CAC1B;AACF;AAIA,SAAgB,gBAAgB,QAA4B;CAC1D,IAAI,KAAK;CACT,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KAAK,KAAK,KAAK,MAAM,OAAO;CACrE,OAAO;AACT;AAEA,SAAgB,iBACd,QACA,OAGmE;CACnE,MAAM,SAAS,UAAU,QAAQ,KAAK;CACtC,IAAI,OAAO,IAAI,OAAO,EAAE,OAAO,OAAO,GAAG;CACzC,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,aAAa,QAAQ,OAAO,KAAK,EAAE,CAAC,EAAE;AACrE"}
@@ -0,0 +1,40 @@
1
+ //#region src/types.d.ts
2
+ type Prefix<Brand extends string> = `${Brand}_`;
3
+ type Id<Brand extends string> = `${Prefix<Brand>}${string}` & {
4
+ readonly __brand: Brand;
5
+ };
6
+ type ParseError = "not_string" | "invalid_prefix" | "invalid_base32";
7
+ type ParseResult<Brand extends string> = {
8
+ ok: true;
9
+ id: Id<Brand>;
10
+ } | {
11
+ ok: false;
12
+ error: ParseError;
13
+ };
14
+ type JsonSchema = {
15
+ readonly type: "string";
16
+ readonly pattern: string;
17
+ readonly description: string;
18
+ readonly example: string;
19
+ };
20
+ type StandardSchemaProps<Brand extends string> = {
21
+ readonly version: 1;
22
+ readonly vendor: "@smonn/ids";
23
+ readonly validate: (value: unknown, options?: {
24
+ readonly libraryOptions?: Record<string, unknown> | undefined;
25
+ }) => {
26
+ readonly value: Id<Brand>;
27
+ readonly issues?: undefined;
28
+ } | {
29
+ readonly issues: ReadonlyArray<{
30
+ readonly message: string;
31
+ }>;
32
+ };
33
+ readonly types?: {
34
+ readonly input: unknown;
35
+ readonly output: Id<Brand>;
36
+ };
37
+ };
38
+ //#endregion
39
+ export { StandardSchemaProps as a, ParseResult as i, JsonSchema as n, ParseError as r, Id as t };
40
+ //# sourceMappingURL=types-Dg2j_zlO.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-Dg2j_zlO.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";KAAY,MAAA,4BAAkC,KAAA;AAAA,KAElC,EAAA,4BAA8B,MAAA,CAAO,KAAA;EAAA,SACtC,OAAA,EAAS,KAAA;AAAA;AAAA,KAGR,UAAA;AAAA,KAEA,WAAA;EACN,EAAA;EAAU,EAAA,EAAI,EAAA,CAAG,KAAA;AAAA;EACjB,EAAA;EAAW,KAAA,EAAO,UAAA;AAAA;AAAA,KAEZ,UAAA;EAAA,SACD,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;EAAA,SACA,OAAA;AAAA;AAAA,KAGC,mBAAA;EAAA,SACD,OAAA;EAAA,SACA,MAAA;EAAA,SACA,QAAA,GACP,KAAA,WACA,OAAA;IAAA,SAAqB,cAAA,GAAiB,MAAA;EAAA;IAAA,SAEzB,KAAA,EAAO,EAAA,CAAG,KAAA;IAAA,SAAiB,MAAA;EAAA;IAAA,SAC3B,MAAA,EAAQ,aAAA;MAAA,SAAyB,OAAA;IAAA;EAAA;EAAA,SACvC,KAAA;IAAA,SAAmB,KAAA;IAAA,SAAyB,MAAA,EAAQ,EAAA,CAAG,KAAA;EAAA;AAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smonn/ids",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "license": "MIT",
5
5
  "author": "Simon Ingeson (https://github.com/smonn)",
6
6
  "repository": {
@@ -16,19 +16,20 @@
16
16
  "type": "module",
17
17
  "exports": {
18
18
  ".": "./dist/index.mjs",
19
+ "./opaque": "./dist/opaque.mjs",
19
20
  "./package.json": "./package.json"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@changesets/cli": "2.31.0",
23
24
  "@types/node": "25.9.1",
24
- "@vitest/coverage-v8": "4.1.7",
25
+ "@vitest/coverage-v8": "4.1.8",
25
26
  "knip": "6.15.0",
26
27
  "mitata": "1.0.34",
27
- "oxfmt": "0.52.0",
28
- "oxlint": "1.67.0",
28
+ "oxfmt": "0.53.0",
29
+ "oxlint": "1.68.0",
29
30
  "tsdown": "0.22.1",
30
31
  "typescript": "6.0.3",
31
- "vitest": "4.1.7"
32
+ "vitest": "4.1.8"
32
33
  },
33
34
  "engines": {
34
35
  "node": ">=24.0.0"
@@ -1,184 +0,0 @@
1
- //#region src/base32.ts
2
- const alphabet = "0123456789abcdefghjkmnpqrstvwxyz";
3
- const valueToCharCode = new Uint8Array(32);
4
- for (let i = 0; i < 32; i++) valueToCharCode[i] = alphabet.charCodeAt(i);
5
- const charCodeToValue = new Uint8Array(256).fill(255);
6
- for (let i = 0; i < 32; i++) charCodeToValue[alphabet.charCodeAt(i)] = i;
7
- function encodeBase32(bytes) {
8
- const codes = new Array(Math.floor(bytes.length * 8 / 5) + 1);
9
- let chi = 0;
10
- let bits = 0;
11
- let value = 0;
12
- for (let i = 0; i < bytes.length; i++) {
13
- value = value << 8 | bytes[i];
14
- bits += 8;
15
- while (bits >= 5) {
16
- bits -= 5;
17
- codes[chi++] = valueToCharCode[value >>> bits & 31];
18
- }
19
- }
20
- codes[chi] = valueToCharCode[value << 5 - bits & 31];
21
- return String.fromCharCode.apply(null, codes);
22
- }
23
- function decodeBase32(str) {
24
- const result = new Uint8Array(Math.floor(str.length * 5 / 8));
25
- let bits = 0;
26
- let value = 0;
27
- let index = 0;
28
- for (let i = 0; i < str.length; i++) {
29
- const v = charCodeToValue[str.charCodeAt(i)];
30
- value = value << 5 | v;
31
- bits += 5;
32
- if (bits >= 8) {
33
- bits -= 8;
34
- result[index++] = value >>> bits & 255;
35
- }
36
- }
37
- return result;
38
- }
39
- //#endregion
40
- //#region src/id.ts
41
- const hexCharCodeToNibble = new Uint8Array(128);
42
- for (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;
43
- for (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;
44
- const defaultOptions = {
45
- now: Date.now,
46
- rng: (target) => {
47
- const s = crypto.randomUUID();
48
- target[0] = hexCharCodeToNibble[s.charCodeAt(0)] << 4 | hexCharCodeToNibble[s.charCodeAt(1)];
49
- target[1] = hexCharCodeToNibble[s.charCodeAt(2)] << 4 | hexCharCodeToNibble[s.charCodeAt(3)];
50
- target[2] = hexCharCodeToNibble[s.charCodeAt(4)] << 4 | hexCharCodeToNibble[s.charCodeAt(5)];
51
- target[3] = hexCharCodeToNibble[s.charCodeAt(6)] << 4 | hexCharCodeToNibble[s.charCodeAt(7)];
52
- target[4] = hexCharCodeToNibble[s.charCodeAt(9)] << 4 | hexCharCodeToNibble[s.charCodeAt(10)];
53
- target[5] = hexCharCodeToNibble[s.charCodeAt(11)] << 4 | hexCharCodeToNibble[s.charCodeAt(12)];
54
- target[6] = hexCharCodeToNibble[s.charCodeAt(24)] << 4 | hexCharCodeToNibble[s.charCodeAt(25)];
55
- target[7] = hexCharCodeToNibble[s.charCodeAt(26)] << 4 | hexCharCodeToNibble[s.charCodeAt(27)];
56
- target[8] = hexCharCodeToNibble[s.charCodeAt(28)] << 4 | hexCharCodeToNibble[s.charCodeAt(29)];
57
- target[9] = hexCharCodeToNibble[s.charCodeAt(30)] << 4 | hexCharCodeToNibble[s.charCodeAt(31)];
58
- }
59
- };
60
- const timestampByteLength = 6;
61
- const randomByteLength = 10;
62
- const totalByteLength = 16;
63
- const base32Length = Math.ceil(totalByteLength * 8 / 5);
64
- const timestampBase32Length = Math.ceil(timestampByteLength * 8 / 5);
65
- const replacePattern = /[ilo]/g;
66
- const aliasTestPattern = /[ilo]/;
67
- const replacer = (match) => match === "o" ? "0" : "1";
68
- const base32Pattern = new RegExp(`^[${alphabet}]{${base32Length}}$`);
69
- const brandPattern = /^[a-z]{3}$/;
70
- const base32CharClass = "[0-9a-hjkmnp-tv-z]";
71
- const registeredBrands = /* @__PURE__ */ new Set();
72
- const warnedBrands = /* @__PURE__ */ new Set();
73
- function createId(brand, opts = {}) {
74
- if (!brandPattern.test(brand)) throw new Error("invalid brand, expected three lowercase a-z characters");
75
- if (typeof process !== "undefined" && process.env.NODE_ENV !== "production" && !opts.allowDuplicateBrand) if (registeredBrands.has(brand)) {
76
- if (!warnedBrands.has(brand)) {
77
- console.warn(`[@smonn/ids] createId("${brand}") called more than once — this usually indicates a bundling or import bug. Pass { allowDuplicateBrand: true } to silence.`);
78
- warnedBrands.add(brand);
79
- }
80
- } else registeredBrands.add(brand);
81
- const options = {
82
- ...defaultOptions,
83
- ...opts
84
- };
85
- const prefix = `${brand}_`;
86
- const buffer = new Uint8Array(totalByteLength);
87
- const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);
88
- return {
89
- generate: () => generate(prefix, options, buffer, randomView),
90
- generateAt: (date) => generate(prefix, options, buffer, randomView, date.getTime()),
91
- is: (value) => is(prefix, value),
92
- parse: (value) => parse(prefix, value),
93
- safeParse: (value) => safeParse(prefix, value),
94
- extractTimestamp: (id) => extractTimestamp(prefix, id),
95
- minIdForTime: (date) => sentinelIdForTime(prefix, date, 0, buffer, randomView),
96
- maxIdForTime: (date) => sentinelIdForTime(prefix, date, 255, buffer, randomView),
97
- toJsonSchema: () => toJsonSchema(brand, prefix, options, buffer, randomView),
98
- "~standard": {
99
- version: 1,
100
- vendor: "@smonn/ids",
101
- validate: (value) => standardValidate(prefix, value)
102
- }
103
- };
104
- }
105
- function toJsonSchema(brand, prefix, options, buffer, randomView) {
106
- return {
107
- type: "string",
108
- pattern: `^${prefix}${base32CharClass}{${base32Length}}$`,
109
- description: `Branded ID for '${brand}'`,
110
- example: generate(prefix, options, buffer, randomView)
111
- };
112
- }
113
- function standardValidate(prefix, value) {
114
- const result = safeParse(prefix, value);
115
- if (result.ok) return { value: result.id };
116
- return { issues: [{ message: errorMessage(prefix, result.error) }] };
117
- }
118
- function errorMessage(prefix, error) {
119
- switch (error) {
120
- case "not_string": return "expected string";
121
- case "invalid_prefix": return `expected prefix '${prefix}'`;
122
- case "invalid_base32": return "invalid base32 payload";
123
- }
124
- }
125
- function safeParse(prefix, value) {
126
- if (typeof value !== "string") return {
127
- ok: false,
128
- error: "not_string"
129
- };
130
- const lowercase = value.toLowerCase();
131
- if (!lowercase.startsWith(prefix)) return {
132
- ok: false,
133
- error: "invalid_prefix"
134
- };
135
- const sliced = lowercase.slice(prefix.length);
136
- const base32 = aliasTestPattern.test(sliced) ? sliced.replaceAll(replacePattern, replacer) : sliced;
137
- if (!base32Pattern.test(base32)) return {
138
- ok: false,
139
- error: "invalid_base32"
140
- };
141
- return {
142
- ok: true,
143
- id: prefix + base32
144
- };
145
- }
146
- function parse(prefix, value) {
147
- const result = safeParse(prefix, value);
148
- if (result.ok) return result.id;
149
- throw new Error(`Invalid ID: ${result.error}`);
150
- }
151
- function is(prefix, value) {
152
- if (typeof value !== "string") return false;
153
- if (!value.startsWith(prefix)) return false;
154
- return base32Pattern.test(value.slice(prefix.length));
155
- }
156
- function writeTimestamp(ms, buffer) {
157
- if (Number.isNaN(ms)) throw new Error("timestamp is not a number");
158
- if (ms < 0) throw new Error("timestamp is negative");
159
- if (ms >= 2 ** (timestampByteLength * 8)) throw new Error("timestamp exceeds 48-bit range");
160
- for (let i = timestampByteLength - 1; i >= 0; i--) {
161
- buffer[i] = ms % 256;
162
- ms = Math.floor(ms / 256);
163
- }
164
- }
165
- function generate(prefix, options, buffer, randomView, ms = options.now()) {
166
- writeTimestamp(ms, buffer);
167
- options.rng(randomView);
168
- return prefix + encodeBase32(buffer);
169
- }
170
- function sentinelIdForTime(prefix, date, fill, buffer, randomView) {
171
- writeTimestamp(date.getTime(), buffer);
172
- randomView.fill(fill);
173
- return prefix + encodeBase32(buffer);
174
- }
175
- function extractTimestamp(prefix, id) {
176
- const bytes = decodeBase32(id.slice(prefix.length, prefix.length + timestampBase32Length));
177
- let ms = 0;
178
- for (const byte of bytes) ms = ms * 256 + byte;
179
- return new Date(ms);
180
- }
181
- //#endregion
182
- export { createId as t };
183
-
184
- //# sourceMappingURL=id-CFxcBxi-.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"id-CFxcBxi-.mjs","names":[],"sources":["../src/base32.ts","../src/id.ts"],"sourcesContent":["/*\n This is based on Crockford's Base32 spec: https://www.crockford.com/base32.html\n One difference is that it uses lowercase instead of uppercase when encoding.\n\n These functions are internal: callers (id.ts) guarantee that input is a\n 16-byte buffer for encode, or a string of characters drawn from the alphabet\n for decode. Invalid input produces silent garbage rather than a thrown error,\n consistent with the trust-the-type rule in ADR-0003.\n*/\n\nexport const alphabet = \"0123456789abcdefghjkmnpqrstvwxyz\";\n\n// 0–31 → ASCII char code, for write-into-codes-then-fromCharCode encoding.\nconst valueToCharCode = new Uint8Array(32);\nfor (let i = 0; i < 32; i++) valueToCharCode[i] = alphabet.charCodeAt(i);\n\n// charCode → 0–31 value. Canonical lowercase only; upstream resolves case and\n// o/i/l aliases before any string reaches decodeBase32.\nconst INVALID = 0xff;\nconst charCodeToValue = new Uint8Array(256).fill(INVALID);\nfor (let i = 0; i < alphabet.length; i++) charCodeToValue[alphabet.charCodeAt(i)] = i;\n\nexport function encodeBase32(bytes: Uint8Array): string {\n // Build an Array<number> of char codes and pass it to fromCharCode.apply.\n // Faster than `result += char` (avoids cons-string overhead) and than\n // Uint8Array variants (apply has a fast path for plain Arrays).\n // oxlint-disable-next-line no-new-array\n const codes = new Array<number>(Math.floor((bytes.length * 8) / 5) + 1);\n let chi = 0;\n let bits = 0;\n let value = 0;\n\n for (let i = 0; i < bytes.length; i++) {\n value = (value << 8) | bytes[i]!;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n codes[chi++] = valueToCharCode[(value >>> bits) & 0x1f]!;\n }\n }\n codes[chi] = valueToCharCode[(value << (5 - bits)) & 0x1f]!;\n return String.fromCharCode.apply(null, codes);\n}\n\nexport function decodeBase32(str: string): Uint8Array {\n const result = new Uint8Array(Math.floor((str.length * 5) / 8));\n let bits = 0;\n let value = 0;\n let index = 0;\n\n for (let i = 0; i < str.length; i++) {\n const v = charCodeToValue[str.charCodeAt(i)]!;\n value = (value << 5) | v;\n bits += 5;\n if (bits >= 8) {\n bits -= 8;\n result[index++] = (value >>> bits) & 0xff;\n }\n }\n return result;\n}\n","import { alphabet, decodeBase32, encodeBase32 } from \"./base32.js\";\n\nexport type Options = {\n now: () => number;\n rng: (target: Uint8Array) => void;\n allowDuplicateBrand?: boolean;\n};\n\n// hex charCode → 0–15 nibble, for decoding UUIDv4 strings into bytes.\n// Covers ['0'-'9' = 48–57] and ['a'-'f' = 97–102]; UUIDs are lowercase per spec.\nconst hexCharCodeToNibble = new Uint8Array(128);\nfor (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;\nfor (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;\n\nconst defaultOptions: Options = {\n now: Date.now,\n // crypto.randomUUID is ~7× faster than crypto.getRandomValues in Node 24\n // (~84 ns vs ~610 ns for a 16-byte fill — likely because the UUID path has\n // a tight fixed-format fast path). We use the 122 random bits of a UUIDv4\n // string as our entropy source, harvesting 10 fully-random bytes from\n // positions where no version (hex 12) or variant (hex 16) bits sit.\n // String layout: \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\" — bytes 0–5 are\n // string[0..7]+string[9..12], bytes 6–9 are string[24..31].\n rng: (target) => {\n const s = crypto.randomUUID();\n target[0] =\n (hexCharCodeToNibble[s.charCodeAt(0)]! << 4) | hexCharCodeToNibble[s.charCodeAt(1)]!;\n target[1] =\n (hexCharCodeToNibble[s.charCodeAt(2)]! << 4) | hexCharCodeToNibble[s.charCodeAt(3)]!;\n target[2] =\n (hexCharCodeToNibble[s.charCodeAt(4)]! << 4) | hexCharCodeToNibble[s.charCodeAt(5)]!;\n target[3] =\n (hexCharCodeToNibble[s.charCodeAt(6)]! << 4) | hexCharCodeToNibble[s.charCodeAt(7)]!;\n target[4] =\n (hexCharCodeToNibble[s.charCodeAt(9)]! << 4) | hexCharCodeToNibble[s.charCodeAt(10)]!;\n target[5] =\n (hexCharCodeToNibble[s.charCodeAt(11)]! << 4) | hexCharCodeToNibble[s.charCodeAt(12)]!;\n target[6] =\n (hexCharCodeToNibble[s.charCodeAt(24)]! << 4) | hexCharCodeToNibble[s.charCodeAt(25)]!;\n target[7] =\n (hexCharCodeToNibble[s.charCodeAt(26)]! << 4) | hexCharCodeToNibble[s.charCodeAt(27)]!;\n target[8] =\n (hexCharCodeToNibble[s.charCodeAt(28)]! << 4) | hexCharCodeToNibble[s.charCodeAt(29)]!;\n target[9] =\n (hexCharCodeToNibble[s.charCodeAt(30)]! << 4) | hexCharCodeToNibble[s.charCodeAt(31)]!;\n },\n};\n\ntype Prefix<Brand extends string> = `${Brand}_`;\n\nexport type Id<Brand extends string> = `${Prefix<Brand>}${string}` & {\n readonly __brand: Brand;\n};\n\nexport type ParseError = \"not_string\" | \"invalid_prefix\" | \"invalid_base32\";\n\nexport type ParseResult<Brand extends string> =\n | { ok: true; id: Id<Brand> }\n | { ok: false; error: ParseError };\n\nexport type JsonSchema = {\n readonly type: \"string\";\n readonly pattern: string;\n readonly description: string;\n readonly example: string;\n};\n\ntype StandardSchemaProps<Brand extends string> = {\n readonly version: 1;\n readonly vendor: \"@smonn/ids\";\n readonly validate: (\n value: unknown,\n options?: { readonly libraryOptions?: Record<string, unknown> | undefined },\n ) =>\n | { readonly value: Id<Brand>; readonly issues?: undefined }\n | { readonly issues: ReadonlyArray<{ readonly message: string }> };\n readonly types?: { readonly input: unknown; readonly output: Id<Brand> };\n};\n\nexport type Codec<Brand extends string> = {\n generate(): Id<Brand>;\n generateAt(date: Date): Id<Brand>;\n is(value: unknown): value is Id<Brand>;\n parse(value: unknown): Id<Brand>;\n safeParse(value: unknown): ParseResult<Brand>;\n extractTimestamp(id: Id<Brand>): Date;\n minIdForTime(date: Date): Id<Brand>;\n maxIdForTime(date: Date): Id<Brand>;\n toJsonSchema(): JsonSchema;\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\nconst timestampByteLength = 6;\nconst randomByteLength = 10;\nconst totalByteLength = timestampByteLength + randomByteLength;\nconst base32Length = Math.ceil((totalByteLength * 8) / 5);\nconst timestampBase32Length = Math.ceil((timestampByteLength * 8) / 5);\nconst replacePattern = /[ilo]/g;\nconst aliasTestPattern = /[ilo]/;\nconst replacer = (match: string): string => (match === \"o\" ? \"0\" : \"1\");\n\nconst base32Pattern = new RegExp(`^[${alphabet}]{${base32Length}}$`);\nconst brandPattern = /^[a-z]{3}$/;\n// Compact regex character class for the canonical lowercase Crockford alphabet\n// (`0123456789abcdefghjkmnpqrstvwxyz` — excludes i, l, o, u). Used in the JSON\n// Schema `pattern`, which describes the canonical wire form only (ADR-0003).\nconst base32CharClass = \"[0-9a-hjkmnp-tv-z]\";\n\nconst registeredBrands = new Set<string>();\nconst warnedBrands = new Set<string>();\n\nexport function createId<Brand extends string>(\n brand: Brand,\n opts: Partial<Options> = {},\n): Codec<Brand> {\n if (!brandPattern.test(brand)) {\n throw new Error(\"invalid brand, expected three lowercase a-z characters\");\n }\n\n if (\n typeof process !== \"undefined\" &&\n process.env.NODE_ENV !== \"production\" &&\n !opts.allowDuplicateBrand\n ) {\n if (registeredBrands.has(brand)) {\n if (!warnedBrands.has(brand)) {\n console.warn(\n `[@smonn/ids] createId(\"${brand}\") called more than once — this usually indicates a bundling or import bug. Pass { allowDuplicateBrand: true } to silence.`,\n );\n warnedBrands.add(brand);\n }\n } else {\n registeredBrands.add(brand);\n }\n }\n\n const options = {\n ...defaultOptions,\n ...opts,\n } satisfies Options;\n\n const prefix: Prefix<Brand> = `${brand}_`;\n // Per-codec scratch buffer. Shared across generate(), generateAt(),\n // minIdForTime(), and maxIdForTime() — all are synchronous and overwrite both\n // the timestamp and random slices before encoding, so successive callers see\n // their own freshly-written bytes. encodeBase32 reads the buffer and\n // returns an independent string, so the caller never sees the buffer itself.\n const buffer = new Uint8Array(totalByteLength);\n const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);\n\n return {\n generate: () => generate(prefix, options, buffer, randomView),\n generateAt: (date: Date) => generate(prefix, options, buffer, randomView, date.getTime()),\n is: (value: unknown) => is(prefix, value),\n parse: (value: unknown) => parse(prefix, value),\n safeParse: (value: unknown) => safeParse(prefix, value),\n extractTimestamp: (id: Id<Brand>) => extractTimestamp(prefix, id),\n minIdForTime: (date: Date) => sentinelIdForTime(prefix, date, 0x00, buffer, randomView),\n maxIdForTime: (date: Date) => sentinelIdForTime(prefix, date, 0xff, buffer, randomView),\n toJsonSchema: () => toJsonSchema(brand, prefix, options, buffer, randomView),\n \"~standard\": {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n },\n };\n}\n\nfunction toJsonSchema<Brand extends string>(\n brand: Brand,\n prefix: Prefix<Brand>,\n options: Options,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): JsonSchema {\n return {\n type: \"string\",\n pattern: `^${prefix}${base32CharClass}{${base32Length}}$`,\n description: `Branded ID for '${brand}'`,\n example: generate(prefix, options, buffer, randomView),\n };\n}\n\nfunction standardValidate<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n):\n | { readonly value: Id<Brand>; readonly issues?: undefined }\n | { readonly issues: ReadonlyArray<{ readonly message: string }> } {\n const result = safeParse(prefix, value);\n if (result.ok) return { value: result.id };\n return { issues: [{ message: errorMessage(prefix, result.error) }] };\n}\n\nfunction errorMessage<Brand extends string>(prefix: Prefix<Brand>, error: ParseError): string {\n switch (error) {\n case \"not_string\":\n return \"expected string\";\n case \"invalid_prefix\":\n return `expected prefix '${prefix}'`;\n case \"invalid_base32\":\n return \"invalid base32 payload\";\n }\n}\n\nfunction safeParse<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n): ParseResult<Brand> {\n if (typeof value !== \"string\") return { ok: false, error: \"not_string\" };\n const lowercase = value.toLowerCase();\n if (!lowercase.startsWith(prefix)) return { ok: false, error: \"invalid_prefix\" };\n\n const sliced = lowercase.slice(prefix.length);\n const base32 = aliasTestPattern.test(sliced)\n ? sliced.replaceAll(replacePattern, replacer)\n : sliced;\n\n if (!base32Pattern.test(base32)) return { ok: false, error: \"invalid_base32\" };\n\n const id = (prefix + base32) as Id<Brand>;\n return { ok: true, id };\n}\n\nfunction parse<Brand extends string>(prefix: Prefix<Brand>, value: unknown): Id<Brand> {\n const result = safeParse(prefix, value);\n if (result.ok) return result.id;\n throw new Error(`Invalid ID: ${result.error}`);\n}\n\nfunction is<Brand extends string>(prefix: Prefix<Brand>, value: unknown): value is Id<Brand> {\n if (typeof value !== \"string\") return false;\n if (!value.startsWith(prefix)) return false;\n return base32Pattern.test(value.slice(prefix.length));\n}\n\n// write the timestamp in big-endian; encoded via mod-256 to avoid 32-bit bitwise coercion\nfunction writeTimestamp(ms: number, buffer: Uint8Array): void {\n if (Number.isNaN(ms)) throw new Error(\"timestamp is not a number\");\n if (ms < 0) throw new Error(\"timestamp is negative\");\n if (ms >= 2 ** (timestampByteLength * 8)) throw new Error(\"timestamp exceeds 48-bit range\");\n for (let i = timestampByteLength - 1; i >= 0; i--) {\n buffer[i] = ms % 256;\n ms = Math.floor(ms / 256);\n }\n}\n\nfunction generate<Brand extends string>(\n prefix: Prefix<Brand>,\n options: Options,\n buffer: Uint8Array,\n randomView: Uint8Array,\n ms: number = options.now(),\n): Id<Brand> {\n writeTimestamp(ms, buffer);\n options.rng(randomView);\n return (prefix + encodeBase32(buffer)) as Id<Brand>;\n}\n\nfunction sentinelIdForTime<Brand extends string>(\n prefix: Prefix<Brand>,\n date: Date,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): Id<Brand> {\n writeTimestamp(date.getTime(), buffer);\n randomView.fill(fill);\n return (prefix + encodeBase32(buffer)) as Id<Brand>;\n}\n\nfunction extractTimestamp<Brand extends string>(prefix: Prefix<Brand>, id: Id<Brand>): Date {\n const base32 = id.slice(prefix.length, prefix.length + timestampBase32Length);\n const bytes = decodeBase32(base32);\n let ms = 0;\n for (const byte of bytes) {\n ms = ms * 256 + byte;\n }\n return new Date(ms);\n}\n"],"mappings":"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"}