@smonn/ids 0.0.2 → 0.2.0

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
@@ -78,6 +78,26 @@ The first 6 bytes of the payload are a big-endian millisecond Unix timestamp, so
78
78
  users.extractTimestamp(id); // Date
79
79
  ```
80
80
 
81
+ For time-range queries, `minIdForTime(date)` and `maxIdForTime(date)` build synthetic IDs at the tight lower and upper bounds of a given millisecond — same timestamp bytes, random portion filled with all `0x00` (min) or all `0xFF` (max). No separate `created_at` column needed:
82
+
83
+ ```ts
84
+ const start = new Date("2026-01-01T00:00:00Z");
85
+ const end = new Date("2026-02-01T00:00:00Z");
86
+
87
+ sql`SELECT * FROM users WHERE id BETWEEN ${users.minIdForTime(start)} AND ${users.maxIdForTime(end)}`;
88
+ ```
89
+
90
+ Both validate the date the same way `generate()` does — pre-epoch or past the 48-bit ceiling throws.
91
+
92
+ To mint a real ID (random tail and all) at a timestamp you choose rather than at `now`, use `generateAt(date)`. The timestamp bytes come from the supplied `Date`; the random portion is filled by the codec's `rng`, so the result round-trips through `extractTimestamp` exactly:
93
+
94
+ ```ts
95
+ const id = users.generateAt(new Date("2024-03-15T12:00:00Z")); // Id<"usr">
96
+ users.extractTimestamp(id); // → 2024-03-15T12:00:00.000Z
97
+ ```
98
+
99
+ This is the one-liner for backfilling: migrating from UUIDv7 / ULID / Snowflake is `oldRows.map((r) => users.generateAt(extractTime(r)))`, with no need to spin up a throwaway codec per timestamp. It validates the date exactly like `generate()` — pre-epoch, past the 48-bit ceiling, or an `Invalid Date` throws.
100
+
81
101
  The timestamp layout (millisecond precision, big-endian, Unix epoch) is part of the public contract — see [ADR-0002](./docs/adr/0002-payload-layout.md).
82
102
 
83
103
  Caveat: two IDs generated in the same millisecond by the same process have independent random tails and do **not** sort deterministically relative to each other. If you need stable intra-millisecond ordering, this library isn't the right tool.
@@ -95,6 +115,55 @@ users.generate(); // deterministic snapshot-friendly output
95
115
 
96
116
  Both `Options` fields are optional. Defaults are `Date.now` and an entropy harvester built on `crypto.randomUUID` (faster than `crypto.getRandomValues` for the 10-byte fills this library needs). `now` returns milliseconds since the Unix epoch. `rng` writes random bytes into the provided target (a 10-byte view into the codec's persistent buffer), so a custom RNG never has to allocate.
97
117
 
118
+ ### "Catch a double-registered brand before it bites in production"
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:
121
+
122
+ ```ts
123
+ const users = createId("usr", { allowDuplicateBrand: true });
124
+ ```
125
+
126
+ The check is a heuristic, not a guarantee. Two physical copies of `@smonn/ids` loaded into the same process (the worst-case bundling bug) each keep their own registry, so neither warns — it catches re-imports of a single module copy, not duplicate copies of the module itself.
127
+
128
+ ### "Use with any Standard Schema validator"
129
+
130
+ Each codec implements [Standard Schema v1](https://standardschema.dev/), so it slots directly into any validator-aware library (Zod, Valibot, ArkType, tRPC inputs, Hono, etc.) without rewriting the same `z.string().refine(usr.is)` boilerplate:
131
+
132
+ ```ts
133
+ import { type } from "arktype";
134
+
135
+ const Body = type({ userId: users });
136
+
137
+ const r = Body({ userId: "USR_01H7B3K9RQXN1CW3P9R8T2SGKZ" });
138
+ // → { userId: "usr_01h7b3k9rqxn1cw3p9r8t2sgkz" } typed as Id<"usr">
139
+ ```
140
+
141
+ `validate` is synchronous, wraps `safeParse`, and returns the canonical `Id<Brand>` on success. Each `ParseError` variant maps to a distinct `issues[].message`:
142
+
143
+ | ParseError | message |
144
+ | ---------------- | ------------------------ |
145
+ | `not_string` | `expected string` |
146
+ | `invalid_prefix` | `expected prefix 'usr_'` |
147
+ | `invalid_base32` | `invalid base32 payload` |
148
+
149
+ ### "Describe an ID field in an OpenAPI / JSON Schema spec"
150
+
151
+ ```ts
152
+ users.toJsonSchema();
153
+ // {
154
+ // type: "string",
155
+ // pattern: "^usr_[0-9a-hjkmnp-tv-z]{26}$",
156
+ // description: "Branded ID for 'usr'",
157
+ // example: "usr_01h7b3k9rqxn1cw3p9r8t2sgkz",
158
+ // }
159
+ ```
160
+
161
+ `toJsonSchema()` returns a plain object you can drop straight into an OpenAPI `components.schemas` entry, a JSON Schema document, or any tool that derives sample payloads from `example`. The character class `[0-9a-hjkmnp-tv-z]` is the lowercase Crockford base32 alphabet (excludes `i`, `l`, `o`, `u`).
162
+
163
+ The `pattern` describes the **canonical form only** — it matches `generate()` output and what `is()` accepts, but rejects uppercase and the Crockford aliases (`o`, `i`, `l`) that `safeParse()` tolerates. Normalising lenient input is the codec's job at the boundary; an artefact that describes data at rest describes the canonical wire shape (see [ADR-0003](./docs/adr/0003-canonical-strict-is.md)).
164
+
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
+
98
167
  ## What this is **not** for
99
168
 
100
169
  - **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.
@@ -109,21 +178,45 @@ import {
109
178
  createId, // (brand: string, opts?: Partial<Options>) => Codec<Brand>
110
179
  type Id, // branded string type
111
180
  type Codec, // returned by createId
112
- type Options, // { now, rng } injection points
181
+ type Options, // { now, rng, allowDuplicateBrand } injection points
113
182
  type ParseError, // "not_string" | "invalid_prefix" | "invalid_base32"
114
183
  type ParseResult, // safeParse return type
184
+ type JsonSchema, // toJsonSchema return type
115
185
  } from "@smonn/ids";
116
186
  ```
117
187
 
118
188
  ### `Codec<Brand>`
119
189
 
120
- | Method | Description |
121
- | ---------------------- | ----------------------------------------------------------------- |
122
- | `generate()` | Produce a fresh ID |
123
- | `is(value)` | Strict type guard: `true` only for already-canonical strings |
124
- | `parse(value)` | Lenient: normalise to canonical, or throw |
125
- | `safeParse(value)` | Lenient: normalise to canonical, or return `{ ok: false, error }` |
126
- | `extractTimestamp(id)` | Decode the creation `Date` from an `Id<Brand>` (trusts the type) |
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 |
201
+
202
+ ## CLI
203
+
204
+ Two brand-agnostic subcommands, no install required:
205
+
206
+ ```bash
207
+ $ npx @smonn/ids inspect usr_01h7b3k9rqxn1cw3p9r8t2sgkz
208
+ brand: usr
209
+ timestamp: 1983-05-27T10:24:22.469Z (43 years ago)
210
+ canonical: usr_01h7b3k9rqxn1cw3p9r8t2sgkz
211
+ input: canonical
212
+
213
+ $ npx @smonn/ids generate usr --count 3
214
+ usr_…
215
+ usr_…
216
+ usr_…
217
+ ```
218
+
219
+ `inspect` accepts non-canonical input (uppercase, Crockford aliases) and shows the canonical form. `generate` prints one ID per line so output is pipeable. Invalid input prints the parse error to stderr and exits non-zero.
127
220
 
128
221
  ## Design
129
222
 
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import { t as createId } from "./id-CFxcBxi-.mjs";
3
+ //#region src/cli.ts
4
+ function run(opts) {
5
+ const [subcommand, ...rest] = opts.argv;
6
+ if (subcommand === "generate" || subcommand === "g") return runGenerate(rest, opts);
7
+ if (subcommand === "inspect" || subcommand === "i") return runInspect(rest, opts);
8
+ if (subcommand === void 0 || subcommand === "--help" || subcommand === "-h") {
9
+ opts.stdout(usage());
10
+ return 0;
11
+ }
12
+ opts.stderr(usage());
13
+ return 1;
14
+ }
15
+ function usage() {
16
+ return [
17
+ "Usage: ids <subcommand> [args]",
18
+ "",
19
+ "Subcommands:",
20
+ " inspect, i <id> Decode an ID and print brand, timestamp, and canonical form.",
21
+ " generate, g <brand> [--count, -c N] Mint one or more canonical IDs for the given brand.",
22
+ ""
23
+ ].join("\n");
24
+ }
25
+ function runInspect(args, opts) {
26
+ const [input] = args;
27
+ if (input === void 0) {
28
+ opts.stderr(usage());
29
+ return 1;
30
+ }
31
+ const brand = input.slice(0, 3).toLowerCase();
32
+ let codec;
33
+ try {
34
+ codec = createId(brand, codecOpts(opts));
35
+ } catch (err) {
36
+ opts.stderr(err.message + "\n");
37
+ return 1;
38
+ }
39
+ const validation = codec["~standard"].validate(input);
40
+ if (validation.issues) {
41
+ opts.stderr(validation.issues[0].message + "\n");
42
+ return 1;
43
+ }
44
+ const canonical = validation.value;
45
+ const timestamp = codec.extractTimestamp(canonical);
46
+ const nowMs = (opts.now ?? Date.now)();
47
+ const relative = formatRelative(timestamp.getTime(), nowMs);
48
+ const inputLine = describeInputForm(input, canonical);
49
+ opts.stdout([
50
+ `brand: ${brand}`,
51
+ `timestamp: ${timestamp.toISOString()} (${relative})`,
52
+ `canonical: ${canonical}`,
53
+ `input: ${inputLine}`,
54
+ ""
55
+ ].join("\n"));
56
+ return 0;
57
+ }
58
+ function describeInputForm(input, canonical) {
59
+ if (input === canonical) return "canonical";
60
+ const notes = [];
61
+ if (input !== input.toLowerCase()) notes.push("was uppercase");
62
+ if (/[ilo]/i.test(input.slice(4))) notes.push("used Crockford aliases");
63
+ return `not canonical (${notes.join(" + ")})`;
64
+ }
65
+ const msPerMinute = 60 * 1e3;
66
+ const msPerHour = 60 * msPerMinute;
67
+ const msPerDay = 24 * msPerHour;
68
+ const daysPerMonth = 30.44;
69
+ const monthsPerYear = 12;
70
+ function formatRelative(thenMs, nowMs) {
71
+ const diff = nowMs - thenMs;
72
+ const abs = Math.abs(diff);
73
+ const suffix = diff < 0 ? "from now" : "ago";
74
+ const head = headUnits(abs);
75
+ return head === "" ? "just now" : `${head} ${suffix}`;
76
+ }
77
+ function headUnits(abs) {
78
+ if (abs < msPerMinute) return "";
79
+ if (abs < msPerHour) return unit(Math.round(abs / msPerMinute), "minute");
80
+ if (abs < msPerDay) return unit(Math.round(abs / msPerHour), "hour");
81
+ if (abs < msPerDay * daysPerMonth) return unit(Math.round(abs / msPerDay), "day");
82
+ const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));
83
+ if (totalMonths < monthsPerYear) return unit(totalMonths, "month");
84
+ const years = Math.floor(totalMonths / monthsPerYear);
85
+ const months = totalMonths % monthsPerYear;
86
+ return months === 0 ? unit(years, "year") : `${unit(years, "year")} ${unit(months, "month")}`;
87
+ }
88
+ function unit(n, name) {
89
+ return `${n} ${n === 1 ? name : `${name}s`}`;
90
+ }
91
+ function runGenerate(args, opts) {
92
+ const [brand, ...flags] = args;
93
+ const count = parseCount(flags);
94
+ if (typeof count === "string") {
95
+ opts.stderr(count + "\n");
96
+ return 1;
97
+ }
98
+ let codec;
99
+ try {
100
+ codec = createId(brand ?? "", codecOpts(opts));
101
+ } catch (err) {
102
+ opts.stderr(err.message + "\n");
103
+ return 1;
104
+ }
105
+ for (let i = 0; i < count; i++) opts.stdout(codec.generate() + "\n");
106
+ return 0;
107
+ }
108
+ function parseCount(flags) {
109
+ const idx = flags.findIndex((f) => f === "--count" || f === "-c");
110
+ if (idx === -1) return 1;
111
+ const raw = flags[idx + 1];
112
+ if (raw === void 0) return "--count requires a value";
113
+ if (!/^[1-9][0-9]*$/.test(raw)) return `--count must be a positive integer, got '${raw}'`;
114
+ return Number(raw);
115
+ }
116
+ function codecOpts(opts) {
117
+ const o = { allowDuplicateBrand: true };
118
+ if (opts.now !== void 0) o.now = opts.now;
119
+ if (opts.rng !== void 0) o.rng = opts.rng;
120
+ return o;
121
+ }
122
+ //#endregion
123
+ //#region bin/cli.ts
124
+ process.exitCode = run({
125
+ argv: process.argv.slice(2),
126
+ stdout: (s) => process.stdout.write(s),
127
+ stderr: (s) => process.stderr.write(s)
128
+ });
129
+ //#endregion
130
+ export {};
131
+
132
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,184 @@
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
@@ -0,0 +1 @@
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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"}
package/dist/index.d.mts CHANGED
@@ -2,6 +2,7 @@
2
2
  type Options = {
3
3
  now: () => number;
4
4
  rng: (target: Uint8Array) => void;
5
+ allowDuplicateBrand?: boolean;
5
6
  };
6
7
  type Prefix<Brand extends string> = `${Brand}_`;
7
8
  type Id<Brand extends string> = `${Prefix<Brand>}${string}` & {
@@ -15,14 +16,43 @@ type ParseResult<Brand extends string> = {
15
16
  ok: false;
16
17
  error: ParseError;
17
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
+ };
18
43
  type Codec<Brand extends string> = {
19
44
  generate(): Id<Brand>;
45
+ generateAt(date: Date): Id<Brand>;
20
46
  is(value: unknown): value is Id<Brand>;
21
47
  parse(value: unknown): Id<Brand>;
22
48
  safeParse(value: unknown): ParseResult<Brand>;
23
49
  extractTimestamp(id: Id<Brand>): Date;
50
+ minIdForTime(date: Date): Id<Brand>;
51
+ maxIdForTime(date: Date): Id<Brand>;
52
+ toJsonSchema(): JsonSchema;
53
+ readonly "~standard": StandardSchemaProps<Brand>;
24
54
  };
25
55
  declare function createId<Brand extends string>(brand: Brand, opts?: Partial<Options>): Codec<Brand>;
26
56
  //#endregion
27
- export { type Codec, type Id, type Options, type ParseError, type ParseResult, createId };
57
+ export { type Codec, type Id, type JsonSchema, type Options, type ParseError, type ParseResult, createId };
28
58
  //# sourceMappingURL=index.d.mts.map
@@ -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;AAAA;AAAA,KA2CX,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,KAAA;EACV,QAAA,IAAY,EAAA,CAAG,KAAA;EACf,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;AAAA;AAAA,iBAenB,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":";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"}
package/dist/index.mjs CHANGED
@@ -1,145 +1,2 @@
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++) {
7
- const code = alphabet.charCodeAt(i);
8
- charCodeToValue[code] = i;
9
- if (code >= 97 && code <= 122) charCodeToValue[code - 32] = i;
10
- }
11
- charCodeToValue["o".charCodeAt(0)] = charCodeToValue["O".charCodeAt(0)] = 0;
12
- charCodeToValue["i".charCodeAt(0)] = charCodeToValue["I".charCodeAt(0)] = 1;
13
- charCodeToValue["l".charCodeAt(0)] = charCodeToValue["L".charCodeAt(0)] = 1;
14
- function encodeBase32(bytes) {
15
- const codes = new Array(Math.floor(bytes.length * 8 / 5) + 1);
16
- let chi = 0;
17
- let bits = 0;
18
- let value = 0;
19
- for (let i = 0; i < bytes.length; i++) {
20
- value = value << 8 | bytes[i];
21
- bits += 8;
22
- while (bits >= 5) {
23
- bits -= 5;
24
- codes[chi++] = valueToCharCode[value >>> bits & 31];
25
- }
26
- }
27
- codes[chi] = valueToCharCode[value << 5 - bits & 31];
28
- return String.fromCharCode.apply(null, codes);
29
- }
30
- function decodeBase32(str) {
31
- const result = new Uint8Array(Math.floor(str.length * 5 / 8));
32
- let bits = 0;
33
- let value = 0;
34
- let index = 0;
35
- for (let i = 0; i < str.length; i++) {
36
- const v = charCodeToValue[str.charCodeAt(i)];
37
- value = value << 5 | v;
38
- bits += 5;
39
- if (bits >= 8) {
40
- bits -= 8;
41
- result[index++] = value >>> bits & 255;
42
- }
43
- }
44
- return result;
45
- }
46
- //#endregion
47
- //#region src/id.ts
48
- const hexCharCodeToNibble = new Uint8Array(128);
49
- for (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;
50
- for (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;
51
- const defaultOptions = {
52
- now: Date.now,
53
- rng: (target) => {
54
- const s = crypto.randomUUID();
55
- target[0] = hexCharCodeToNibble[s.charCodeAt(0)] << 4 | hexCharCodeToNibble[s.charCodeAt(1)];
56
- target[1] = hexCharCodeToNibble[s.charCodeAt(2)] << 4 | hexCharCodeToNibble[s.charCodeAt(3)];
57
- target[2] = hexCharCodeToNibble[s.charCodeAt(4)] << 4 | hexCharCodeToNibble[s.charCodeAt(5)];
58
- target[3] = hexCharCodeToNibble[s.charCodeAt(6)] << 4 | hexCharCodeToNibble[s.charCodeAt(7)];
59
- target[4] = hexCharCodeToNibble[s.charCodeAt(9)] << 4 | hexCharCodeToNibble[s.charCodeAt(10)];
60
- target[5] = hexCharCodeToNibble[s.charCodeAt(11)] << 4 | hexCharCodeToNibble[s.charCodeAt(12)];
61
- target[6] = hexCharCodeToNibble[s.charCodeAt(24)] << 4 | hexCharCodeToNibble[s.charCodeAt(25)];
62
- target[7] = hexCharCodeToNibble[s.charCodeAt(26)] << 4 | hexCharCodeToNibble[s.charCodeAt(27)];
63
- target[8] = hexCharCodeToNibble[s.charCodeAt(28)] << 4 | hexCharCodeToNibble[s.charCodeAt(29)];
64
- target[9] = hexCharCodeToNibble[s.charCodeAt(30)] << 4 | hexCharCodeToNibble[s.charCodeAt(31)];
65
- }
66
- };
67
- const timestampByteLength = 6;
68
- const randomByteLength = 10;
69
- const totalByteLength = 16;
70
- const base32Length = Math.ceil(totalByteLength * 8 / 5);
71
- const timestampBase32Length = Math.ceil(timestampByteLength * 8 / 5);
72
- const replacePattern = /[ilo]/g;
73
- const aliasTestPattern = /[ilo]/;
74
- const replacer = (match) => match === "o" ? "0" : "1";
75
- const base32Pattern = new RegExp(`^[${alphabet}]{${base32Length}}$`);
76
- const brandPattern = /^[a-z]{3}$/;
77
- function createId(brand, opts = {}) {
78
- if (!brandPattern.test(brand)) throw new Error("invalid brand, expected three lowercase a-z characters");
79
- const options = {
80
- ...defaultOptions,
81
- ...opts
82
- };
83
- const prefix = `${brand}_`;
84
- const buffer = new Uint8Array(totalByteLength);
85
- const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);
86
- return {
87
- generate: () => generate(prefix, options, buffer, randomView),
88
- is: (value) => is(prefix, value),
89
- parse: (value) => parse(prefix, value),
90
- safeParse: (value) => safeParse(prefix, value),
91
- extractTimestamp: (id) => extractTimestamp(prefix, id)
92
- };
93
- }
94
- function safeParse(prefix, value) {
95
- if (typeof value !== "string") return {
96
- ok: false,
97
- error: "not_string"
98
- };
99
- const lowercase = value.toLowerCase();
100
- if (!lowercase.startsWith(prefix)) return {
101
- ok: false,
102
- error: "invalid_prefix"
103
- };
104
- const sliced = lowercase.slice(prefix.length);
105
- const base32 = aliasTestPattern.test(sliced) ? sliced.replaceAll(replacePattern, replacer) : sliced;
106
- if (!base32Pattern.test(base32)) return {
107
- ok: false,
108
- error: "invalid_base32"
109
- };
110
- return {
111
- ok: true,
112
- id: prefix + base32
113
- };
114
- }
115
- function parse(prefix, value) {
116
- const result = safeParse(prefix, value);
117
- if (result.ok) return result.id;
118
- throw new Error(`Invalid ID: ${result.error}`);
119
- }
120
- function is(prefix, value) {
121
- if (typeof value !== "string") return false;
122
- if (!value.startsWith(prefix)) return false;
123
- return base32Pattern.test(value.slice(prefix.length));
124
- }
125
- function generate(prefix, options, buffer, randomView) {
126
- let ms = options.now();
127
- if (ms < 0) throw new Error("timestamp is negative");
128
- if (ms >= 2 ** (timestampByteLength * 8)) throw new Error("timestamp exceeds 48-bit range");
129
- for (let i = timestampByteLength - 1; i >= 0; i--) {
130
- buffer[i] = ms % 256;
131
- ms = Math.floor(ms / 256);
132
- }
133
- options.rng(randomView);
134
- return prefix + encodeBase32(buffer);
135
- }
136
- function extractTimestamp(prefix, id) {
137
- const bytes = decodeBase32(id.slice(prefix.length, prefix.length + timestampBase32Length));
138
- let ms = 0;
139
- for (const byte of bytes) ms = ms * 256 + byte;
140
- return new Date(ms);
141
- }
142
- //#endregion
1
+ import { t as createId } from "./id-CFxcBxi-.mjs";
143
2
  export { createId };
144
-
145
- //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@smonn/ids",
3
- "version": "0.0.2",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "author": "Simon Ingeson (https://github.com/smonn)",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/smonn/ids.git"
9
9
  },
10
+ "bin": {
11
+ "ids": "./dist/cli.mjs"
12
+ },
10
13
  "files": [
11
14
  "dist"
12
15
  ],
@@ -19,7 +22,7 @@
19
22
  "@changesets/cli": "2.31.0",
20
23
  "@types/node": "25.9.1",
21
24
  "@vitest/coverage-v8": "4.1.7",
22
- "knip": "6.14.2",
25
+ "knip": "6.15.0",
23
26
  "mitata": "1.0.34",
24
27
  "oxfmt": "0.52.0",
25
28
  "oxlint": "1.67.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.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. Covers both cases and the Crockford o/i/l aliases.\nconst INVALID = 0xff;\nconst charCodeToValue = new Uint8Array(256).fill(INVALID);\nfor (let i = 0; i < alphabet.length; i++) {\n const code = alphabet.charCodeAt(i);\n charCodeToValue[code] = i;\n if (code >= 97 && code <= 122) charCodeToValue[code - 32] = i;\n}\ncharCodeToValue[\"o\".charCodeAt(0)] = charCodeToValue[\"O\".charCodeAt(0)] = 0;\ncharCodeToValue[\"i\".charCodeAt(0)] = charCodeToValue[\"I\".charCodeAt(0)] = 1;\ncharCodeToValue[\"l\".charCodeAt(0)] = charCodeToValue[\"L\".charCodeAt(0)] = 1;\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};\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 Codec<Brand extends string> = {\n generate(): 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};\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\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 const options = {\n ...defaultOptions,\n ...opts,\n } satisfies Options;\n\n const prefix: Prefix<Brand> = `${brand}_`;\n // Per-codec scratch buffer. Reused across generate() calls — generate is\n // synchronous, so successive callers see the buffer overwritten, not the\n // previous result. encodeBase32 reads the buffer and returns an independent\n // 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 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 };\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\nfunction generate<Brand extends string>(\n prefix: Prefix<Brand>,\n options: Options,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): Id<Brand> {\n let ms = options.now();\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 // write the timestamp in big-endian; encoded via mod-256 to avoid 32-bit bitwise coercion\n for (let i = timestampByteLength - 1; i >= 0; i--) {\n buffer[i] = ms % 256;\n ms = Math.floor(ms / 256);\n }\n options.rng(randomView);\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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"}