@smonn/ids 0.1.0 → 0.3.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 +39 -9
- package/dist/cli.mjs +1 -1
- package/dist/cli.mjs.map +1 -1
- package/dist/id-B4GiFKYV.mjs +76 -0
- package/dist/id-B4GiFKYV.mjs.map +1 -0
- package/dist/index.d.mts +5 -31
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/opaque.d.mts +26 -0
- package/dist/opaque.d.mts.map +1 -0
- package/dist/opaque.mjs +70 -0
- package/dist/opaque.mjs.map +1 -0
- package/dist/shared-CmbAeUdM.mjs +122 -0
- package/dist/shared-CmbAeUdM.mjs.map +1 -0
- package/dist/types-Dg2j_zlO.d.mts +40 -0
- package/dist/types-Dg2j_zlO.d.mts.map +1 -0
- package/package.json +6 -5
- package/dist/id-DCj81Fkm.mjs +0 -172
- package/dist/id-DCj81Fkm.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -89,6 +89,15 @@ sql`SELECT * FROM users WHERE id BETWEEN ${users.minIdForTime(start)} AND ${user
|
|
|
89
89
|
|
|
90
90
|
Both validate the date the same way `generate()` does — pre-epoch or past the 48-bit ceiling throws.
|
|
91
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
|
+
|
|
92
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).
|
|
93
102
|
|
|
94
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.
|
|
@@ -137,6 +146,24 @@ const r = Body({ userId: "USR_01H7B3K9RQXN1CW3P9R8T2SGKZ" });
|
|
|
137
146
|
| `invalid_prefix` | `expected prefix 'usr_'` |
|
|
138
147
|
| `invalid_base32` | `invalid base32 payload` |
|
|
139
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
|
+
|
|
140
167
|
## What this is **not** for
|
|
141
168
|
|
|
142
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.
|
|
@@ -154,20 +181,23 @@ import {
|
|
|
154
181
|
type Options, // { now, rng, allowDuplicateBrand } injection points
|
|
155
182
|
type ParseError, // "not_string" | "invalid_prefix" | "invalid_base32"
|
|
156
183
|
type ParseResult, // safeParse return type
|
|
184
|
+
type JsonSchema, // toJsonSchema return type
|
|
157
185
|
} from "@smonn/ids";
|
|
158
186
|
```
|
|
159
187
|
|
|
160
188
|
### `Codec<Brand>`
|
|
161
189
|
|
|
162
|
-
| Method | Description
|
|
163
|
-
| ---------------------- |
|
|
164
|
-
| `generate()` | Produce a fresh ID
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
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 |
|
|
171
201
|
|
|
172
202
|
## CLI
|
|
173
203
|
|
package/dist/cli.mjs
CHANGED
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
|
|
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,50 +1,24 @@
|
|
|
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 StandardSchemaProps<Brand extends string> = {
|
|
20
|
-
readonly version: 1;
|
|
21
|
-
readonly vendor: "@smonn/ids";
|
|
22
|
-
readonly validate: (value: unknown, options?: {
|
|
23
|
-
readonly libraryOptions?: Record<string, unknown> | undefined;
|
|
24
|
-
}) => {
|
|
25
|
-
readonly value: Id<Brand>;
|
|
26
|
-
readonly issues?: undefined;
|
|
27
|
-
} | {
|
|
28
|
-
readonly issues: ReadonlyArray<{
|
|
29
|
-
readonly message: string;
|
|
30
|
-
}>;
|
|
31
|
-
};
|
|
32
|
-
readonly types?: {
|
|
33
|
-
readonly input: unknown;
|
|
34
|
-
readonly output: Id<Brand>;
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
9
|
type Codec<Brand extends string> = {
|
|
38
10
|
generate(): Id<Brand>;
|
|
11
|
+
generateAt(date: Date): Id<Brand>;
|
|
39
12
|
is(value: unknown): value is Id<Brand>;
|
|
40
13
|
parse(value: unknown): Id<Brand>;
|
|
41
14
|
safeParse(value: unknown): ParseResult<Brand>;
|
|
42
15
|
extractTimestamp(id: Id<Brand>): Date;
|
|
43
16
|
minIdForTime(date: Date): Id<Brand>;
|
|
44
17
|
maxIdForTime(date: Date): Id<Brand>;
|
|
18
|
+
toJsonSchema(): JsonSchema;
|
|
45
19
|
readonly "~standard": StandardSchemaProps<Brand>;
|
|
46
20
|
};
|
|
47
21
|
declare function createId<Brand extends string>(brand: Brand, opts?: Partial<Options>): Codec<Brand>;
|
|
48
22
|
//#endregion
|
|
49
|
-
export { type Codec, type Id, type Options, type ParseError, type ParseResult, createId };
|
|
23
|
+
export { type Codec, type Id, type JsonSchema, type Options, type ParseError, type ParseResult, createId };
|
|
50
24
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/id.ts"],"mappings":"
|
|
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-
|
|
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"}
|
package/dist/opaque.mjs
ADDED
|
@@ -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":"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"}
|
|
@@ -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.
|
|
3
|
+
"version": "0.3.0",
|
|
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.
|
|
25
|
+
"@vitest/coverage-v8": "4.1.8",
|
|
25
26
|
"knip": "6.15.0",
|
|
26
27
|
"mitata": "1.0.34",
|
|
27
|
-
"oxfmt": "0.
|
|
28
|
-
"oxlint": "1.
|
|
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.
|
|
32
|
+
"vitest": "4.1.8"
|
|
32
33
|
},
|
|
33
34
|
"engines": {
|
|
34
35
|
"node": ">=24.0.0"
|
package/dist/id-DCj81Fkm.mjs
DELETED
|
@@ -1,172 +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 registeredBrands = /* @__PURE__ */ new Set();
|
|
71
|
-
const warnedBrands = /* @__PURE__ */ new Set();
|
|
72
|
-
function createId(brand, opts = {}) {
|
|
73
|
-
if (!brandPattern.test(brand)) throw new Error("invalid brand, expected three lowercase a-z characters");
|
|
74
|
-
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production" && !opts.allowDuplicateBrand) if (registeredBrands.has(brand)) {
|
|
75
|
-
if (!warnedBrands.has(brand)) {
|
|
76
|
-
console.warn(`[@smonn/ids] createId("${brand}") called more than once — this usually indicates a bundling or import bug. Pass { allowDuplicateBrand: true } to silence.`);
|
|
77
|
-
warnedBrands.add(brand);
|
|
78
|
-
}
|
|
79
|
-
} else registeredBrands.add(brand);
|
|
80
|
-
const options = {
|
|
81
|
-
...defaultOptions,
|
|
82
|
-
...opts
|
|
83
|
-
};
|
|
84
|
-
const prefix = `${brand}_`;
|
|
85
|
-
const buffer = new Uint8Array(totalByteLength);
|
|
86
|
-
const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);
|
|
87
|
-
return {
|
|
88
|
-
generate: () => generate(prefix, options, buffer, randomView),
|
|
89
|
-
is: (value) => is(prefix, value),
|
|
90
|
-
parse: (value) => parse(prefix, value),
|
|
91
|
-
safeParse: (value) => safeParse(prefix, value),
|
|
92
|
-
extractTimestamp: (id) => extractTimestamp(prefix, id),
|
|
93
|
-
minIdForTime: (date) => sentinelIdForTime(prefix, date, 0, buffer, randomView),
|
|
94
|
-
maxIdForTime: (date) => sentinelIdForTime(prefix, date, 255, buffer, randomView),
|
|
95
|
-
"~standard": {
|
|
96
|
-
version: 1,
|
|
97
|
-
vendor: "@smonn/ids",
|
|
98
|
-
validate: (value) => standardValidate(prefix, value)
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
function standardValidate(prefix, value) {
|
|
103
|
-
const result = safeParse(prefix, value);
|
|
104
|
-
if (result.ok) return { value: result.id };
|
|
105
|
-
return { issues: [{ message: errorMessage(prefix, result.error) }] };
|
|
106
|
-
}
|
|
107
|
-
function errorMessage(prefix, error) {
|
|
108
|
-
switch (error) {
|
|
109
|
-
case "not_string": return "expected string";
|
|
110
|
-
case "invalid_prefix": return `expected prefix '${prefix}'`;
|
|
111
|
-
case "invalid_base32": return "invalid base32 payload";
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
function safeParse(prefix, value) {
|
|
115
|
-
if (typeof value !== "string") return {
|
|
116
|
-
ok: false,
|
|
117
|
-
error: "not_string"
|
|
118
|
-
};
|
|
119
|
-
const lowercase = value.toLowerCase();
|
|
120
|
-
if (!lowercase.startsWith(prefix)) return {
|
|
121
|
-
ok: false,
|
|
122
|
-
error: "invalid_prefix"
|
|
123
|
-
};
|
|
124
|
-
const sliced = lowercase.slice(prefix.length);
|
|
125
|
-
const base32 = aliasTestPattern.test(sliced) ? sliced.replaceAll(replacePattern, replacer) : sliced;
|
|
126
|
-
if (!base32Pattern.test(base32)) return {
|
|
127
|
-
ok: false,
|
|
128
|
-
error: "invalid_base32"
|
|
129
|
-
};
|
|
130
|
-
return {
|
|
131
|
-
ok: true,
|
|
132
|
-
id: prefix + base32
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function parse(prefix, value) {
|
|
136
|
-
const result = safeParse(prefix, value);
|
|
137
|
-
if (result.ok) return result.id;
|
|
138
|
-
throw new Error(`Invalid ID: ${result.error}`);
|
|
139
|
-
}
|
|
140
|
-
function is(prefix, value) {
|
|
141
|
-
if (typeof value !== "string") return false;
|
|
142
|
-
if (!value.startsWith(prefix)) return false;
|
|
143
|
-
return base32Pattern.test(value.slice(prefix.length));
|
|
144
|
-
}
|
|
145
|
-
function writeTimestamp(ms, buffer) {
|
|
146
|
-
if (ms < 0) throw new Error("timestamp is negative");
|
|
147
|
-
if (ms >= 2 ** (timestampByteLength * 8)) throw new Error("timestamp exceeds 48-bit range");
|
|
148
|
-
for (let i = timestampByteLength - 1; i >= 0; i--) {
|
|
149
|
-
buffer[i] = ms % 256;
|
|
150
|
-
ms = Math.floor(ms / 256);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
function generate(prefix, options, buffer, randomView) {
|
|
154
|
-
writeTimestamp(options.now(), buffer);
|
|
155
|
-
options.rng(randomView);
|
|
156
|
-
return prefix + encodeBase32(buffer);
|
|
157
|
-
}
|
|
158
|
-
function sentinelIdForTime(prefix, date, fill, buffer, randomView) {
|
|
159
|
-
writeTimestamp(date.getTime(), buffer);
|
|
160
|
-
randomView.fill(fill);
|
|
161
|
-
return prefix + encodeBase32(buffer);
|
|
162
|
-
}
|
|
163
|
-
function extractTimestamp(prefix, id) {
|
|
164
|
-
const bytes = decodeBase32(id.slice(prefix.length, prefix.length + timestampBase32Length));
|
|
165
|
-
let ms = 0;
|
|
166
|
-
for (const byte of bytes) ms = ms * 256 + byte;
|
|
167
|
-
return new Date(ms);
|
|
168
|
-
}
|
|
169
|
-
//#endregion
|
|
170
|
-
export { createId as t };
|
|
171
|
-
|
|
172
|
-
//# sourceMappingURL=id-DCj81Fkm.mjs.map
|
package/dist/id-DCj81Fkm.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"id-DCj81Fkm.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\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 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 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\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(), minIdForTime(), and\n // maxIdForTime() — all three are synchronous and overwrite both the\n // 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 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 \"~standard\": {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n },\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 (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): Id<Brand> {\n writeTimestamp(options.now(), 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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"}
|