@smonn/ids 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -16
- 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 +2 -36
- 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-CFxcBxi-.mjs +0 -184
- package/dist/id-CFxcBxi-.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Public-facing branded IDs for TypeScript apps.
|
|
|
6
6
|
pnpm add @smonn/ids
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Each ID looks like `usr_01h7b3k9rqxn4cw3p9r8t2sgkz`: a three-letter brand, an underscore, then 26 Crockford base32 characters
|
|
9
|
+
Each ID looks like `usr_01h7b3k9rqxn4cw3p9r8t2sgkz`: a three-letter brand, an underscore, then 26 Crockford base32 characters of payload. The default codec encodes a 48-bit millisecond Unix timestamp followed by 80 random bits — same byte layout as a [ULID](https://github.com/ulid/spec); see [ADR-0002](./docs/adr/0002-payload-layout.md) for the deliberate divergences. A second codec (`@smonn/ids/opaque`) keeps the same wire shape but encrypts the payload under a key, so the timestamp isn't readable from the ID.
|
|
10
10
|
|
|
11
11
|
## What this is for
|
|
12
12
|
|
|
@@ -117,7 +117,7 @@ Both `Options` fields are optional. Defaults are `Date.now` and an entropy harve
|
|
|
117
117
|
|
|
118
118
|
### "Catch a double-registered brand before it bites in production"
|
|
119
119
|
|
|
120
|
-
The intended pattern is one codec per brand per process, constructed at module init. Calling `createId(brand)` a second time for the same brand usually means a bundling or import bug (accidental re-export, a test re-importing without resetting). In development (`process.env.NODE_ENV !== "production"`), the second call emits a one-shot `console.warn`; the brand-tracking registry is skipped in production. Tests that intentionally re-create codecs can opt out:
|
|
120
|
+
The intended pattern is one codec per brand per process, constructed at module init. Calling `createId(brand)` a second time for the same brand usually means a bundling or import bug (accidental re-export, a test re-importing without resetting). In development (`process.env.NODE_ENV !== "production"`), the second call emits a one-shot `console.warn`; the brand-tracking registry is skipped in production. The same registry covers cross-codec collisions: `createId("usr")` followed by `createOpaqueId("usr")` warns too, because codec choice is a per-brand commitment ([ADR-0007](./docs/adr/0007-wire-indistinguishable-codec-variants.md)). Tests that intentionally re-create codecs can opt out:
|
|
121
121
|
|
|
122
122
|
```ts
|
|
123
123
|
const users = createId("usr", { allowDuplicateBrand: true });
|
|
@@ -164,12 +164,34 @@ The `pattern` describes the **canonical form only** — it matches `generate()`
|
|
|
164
164
|
|
|
165
165
|
`example` is produced by calling `generate()` on each invocation, so it is fresh (non-deterministic) and always matches the returned `pattern`. One consequence: a codec wired with an injected `now` outside the 48-bit range — the same misconfiguration that breaks `generate()` — makes `toJsonSchema()` throw too.
|
|
166
166
|
|
|
167
|
+
### "Don't leak creation time in IDs that customers can see"
|
|
168
|
+
|
|
169
|
+
The default codec exposes the creation timestamp by design — that's what makes `ORDER BY id` work. If that's a leak you can't accept (invoice IDs revealing billing cadence, signup IDs revealing acquisition velocity), use the opaque codec at `@smonn/ids/opaque`. Same `<brand>_<26 chars>` wire shape, but the payload is AES-encrypted under a key you supply.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { createOpaqueId, importOpaqueKey } from "@smonn/ids/opaque";
|
|
173
|
+
|
|
174
|
+
const key = await importOpaqueKey(new Uint8Array(16)); // 128- or 256-bit raw key
|
|
175
|
+
const invoices = createOpaqueId("inv", { key });
|
|
176
|
+
|
|
177
|
+
const id = await invoices.generate(); // "inv_…", timestamp not extractable without the key
|
|
178
|
+
await invoices.extractTimestamp(id); // Date — same codec, same key required
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Three differences from the default codec:
|
|
182
|
+
|
|
183
|
+
- **Async key-dependent methods.** WebCrypto is async-only, so `generate`, `generateAt`, and `extractTimestamp` return `Promise`s. `is`, `parse`, `safeParse`, `toJsonSchema`, and the Standard Schema adapter stay sync — they work on the wire form only ([ADR-0006](./docs/adr/0006-async-keyed-codec-contract.md)).
|
|
184
|
+
- **No `minIdForTime` / `maxIdForTime`.** Encrypted payloads don't sort by time. If you need time-range scans on opaque-coded entities, store the timestamp in a separate column.
|
|
185
|
+
- **Wire-indistinguishable from the default codec.** Codec choice is a per-brand commitment; the brand registry warns if you register the same brand against both in dev ([ADR-0007](./docs/adr/0007-wire-indistinguishable-codec-variants.md)).
|
|
186
|
+
|
|
187
|
+
Encryption is AES-CBC with a zero IV. That's deliberately safe here because the plaintext already carries 80 bits of entropy per ID; see [ADR-0004](./docs/adr/0004-aes-cbc-strip-trick.md) for the full rationale.
|
|
188
|
+
|
|
167
189
|
## What this is **not** for
|
|
168
190
|
|
|
169
191
|
- **Internal surrogate primary keys.** If nobody outside your service ever sees the ID, the brand prefix and lenient parsing are dead weight. Use a `bigint` sequence.
|
|
170
192
|
- **Wire-compatible ULIDs.** The byte layout is ULID-shaped but the encoding is lowercase and wrapped in a brand envelope. Stock ULID parsers will reject these.
|
|
171
193
|
- **Distributed-trace / request-correlation IDs.** Use OpenTelemetry-format IDs.
|
|
172
|
-
- **Hiding
|
|
194
|
+
- **Hiding creation time with the default codec.** Anyone with one ID at a known creation time can compute the epoch offset. A custom epoch wouldn't help and isn't supported. To hide creation time per-ID, use the opaque codec (above).
|
|
173
195
|
|
|
174
196
|
## API surface
|
|
175
197
|
|
|
@@ -183,21 +205,28 @@ import {
|
|
|
183
205
|
type ParseResult, // safeParse return type
|
|
184
206
|
type JsonSchema, // toJsonSchema return type
|
|
185
207
|
} from "@smonn/ids";
|
|
208
|
+
|
|
209
|
+
import {
|
|
210
|
+
createOpaqueId, // (brand: string, opts: { key, now?, rng?, allowDuplicateBrand? }) => OpaqueCodec<Brand>
|
|
211
|
+
importOpaqueKey, // (bytes: Uint8Array) => Promise<CryptoKey>
|
|
212
|
+
type OpaqueCodec, // returned by createOpaqueId
|
|
213
|
+
type OpaqueOptions, // { key, now, rng, allowDuplicateBrand } injection points
|
|
214
|
+
} from "@smonn/ids/opaque";
|
|
186
215
|
```
|
|
187
216
|
|
|
188
|
-
###
|
|
189
|
-
|
|
190
|
-
| Method | Description |
|
|
191
|
-
| ---------------------- | ----------------------------------------------------------------------------- |
|
|
192
|
-
| `generate()` | Produce a fresh ID |
|
|
193
|
-
| `generateAt(date)` | Produce a fresh ID with timestamp bytes from `date` (for backfills) |
|
|
194
|
-
| `is(value)` | Strict type guard: `true` only for already-canonical strings |
|
|
195
|
-
| `parse(value)` | Lenient: normalise to canonical, or throw |
|
|
196
|
-
| `safeParse(value)` | Lenient: normalise to canonical, or return `{ ok: false, error }` |
|
|
197
|
-
| `extractTimestamp(id)` | Decode the creation `Date` from an `Id<Brand>` (trusts the type) |
|
|
198
|
-
| `minIdForTime(date)` | Tight lower bound for any ID generated at `date` (for range queries) |
|
|
199
|
-
| `maxIdForTime(date)` | Tight upper bound for any ID generated at `date` (for range queries) |
|
|
200
|
-
| `toJsonSchema()` | JSON Schema (`type`/`pattern`/`description`/`example`) for the canonical form |
|
|
217
|
+
### Codec methods
|
|
218
|
+
|
|
219
|
+
| Method | `Codec<Brand>` | `OpaqueCodec<Brand>` | Description |
|
|
220
|
+
| ---------------------- | -------------- | -------------------- | ----------------------------------------------------------------------------- |
|
|
221
|
+
| `generate()` | sync | async | Produce a fresh ID |
|
|
222
|
+
| `generateAt(date)` | sync | async | Produce a fresh ID with timestamp bytes from `date` (for backfills) |
|
|
223
|
+
| `is(value)` | sync | sync | Strict type guard: `true` only for already-canonical strings |
|
|
224
|
+
| `parse(value)` | sync | sync | Lenient: normalise to canonical, or throw |
|
|
225
|
+
| `safeParse(value)` | sync | sync | Lenient: normalise to canonical, or return `{ ok: false, error }` |
|
|
226
|
+
| `extractTimestamp(id)` | sync | async | Decode the creation `Date` from an `Id<Brand>` (trusts the type) |
|
|
227
|
+
| `minIdForTime(date)` | sync | — | Tight lower bound for any ID generated at `date` (for range queries) |
|
|
228
|
+
| `maxIdForTime(date)` | sync | — | Tight upper bound for any ID generated at `date` (for range queries) |
|
|
229
|
+
| `toJsonSchema()` | sync | sync | JSON Schema (`type`/`pattern`/`description`/`example`) for the canonical form |
|
|
201
230
|
|
|
202
231
|
## CLI
|
|
203
232
|
|
package/dist/cli.mjs
CHANGED
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,45 +1,11 @@
|
|
|
1
|
+
import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, r as ParseError, t as Id } from "./types-Dg2j_zlO.mjs";
|
|
2
|
+
|
|
1
3
|
//#region src/id.d.ts
|
|
2
4
|
type Options = {
|
|
3
5
|
now: () => number;
|
|
4
6
|
rng: (target: Uint8Array) => void;
|
|
5
7
|
allowDuplicateBrand?: boolean;
|
|
6
8
|
};
|
|
7
|
-
type Prefix<Brand extends string> = `${Brand}_`;
|
|
8
|
-
type Id<Brand extends string> = `${Prefix<Brand>}${string}` & {
|
|
9
|
-
readonly __brand: Brand;
|
|
10
|
-
};
|
|
11
|
-
type ParseError = "not_string" | "invalid_prefix" | "invalid_base32";
|
|
12
|
-
type ParseResult<Brand extends string> = {
|
|
13
|
-
ok: true;
|
|
14
|
-
id: Id<Brand>;
|
|
15
|
-
} | {
|
|
16
|
-
ok: false;
|
|
17
|
-
error: ParseError;
|
|
18
|
-
};
|
|
19
|
-
type JsonSchema = {
|
|
20
|
-
readonly type: "string";
|
|
21
|
-
readonly pattern: string;
|
|
22
|
-
readonly description: string;
|
|
23
|
-
readonly example: string;
|
|
24
|
-
};
|
|
25
|
-
type StandardSchemaProps<Brand extends string> = {
|
|
26
|
-
readonly version: 1;
|
|
27
|
-
readonly vendor: "@smonn/ids";
|
|
28
|
-
readonly validate: (value: unknown, options?: {
|
|
29
|
-
readonly libraryOptions?: Record<string, unknown> | undefined;
|
|
30
|
-
}) => {
|
|
31
|
-
readonly value: Id<Brand>;
|
|
32
|
-
readonly issues?: undefined;
|
|
33
|
-
} | {
|
|
34
|
-
readonly issues: ReadonlyArray<{
|
|
35
|
-
readonly message: string;
|
|
36
|
-
}>;
|
|
37
|
-
};
|
|
38
|
-
readonly types?: {
|
|
39
|
-
readonly input: unknown;
|
|
40
|
-
readonly output: Id<Brand>;
|
|
41
|
-
};
|
|
42
|
-
};
|
|
43
9
|
type Codec<Brand extends string> = {
|
|
44
10
|
generate(): Id<Brand>;
|
|
45
11
|
generateAt(date: Date): Id<Brand>;
|
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.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Simon Ingeson (https://github.com/smonn)",
|
|
6
6
|
"repository": {
|
|
@@ -16,19 +16,20 @@
|
|
|
16
16
|
"type": "module",
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./dist/index.mjs",
|
|
19
|
+
"./opaque": "./dist/opaque.mjs",
|
|
19
20
|
"./package.json": "./package.json"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@changesets/cli": "2.31.0",
|
|
23
24
|
"@types/node": "25.9.1",
|
|
24
|
-
"@vitest/coverage-v8": "4.1.
|
|
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-CFxcBxi-.mjs
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
//#region src/base32.ts
|
|
2
|
-
const alphabet = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
3
|
-
const valueToCharCode = new Uint8Array(32);
|
|
4
|
-
for (let i = 0; i < 32; i++) valueToCharCode[i] = alphabet.charCodeAt(i);
|
|
5
|
-
const charCodeToValue = new Uint8Array(256).fill(255);
|
|
6
|
-
for (let i = 0; i < 32; i++) charCodeToValue[alphabet.charCodeAt(i)] = i;
|
|
7
|
-
function encodeBase32(bytes) {
|
|
8
|
-
const codes = new Array(Math.floor(bytes.length * 8 / 5) + 1);
|
|
9
|
-
let chi = 0;
|
|
10
|
-
let bits = 0;
|
|
11
|
-
let value = 0;
|
|
12
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
13
|
-
value = value << 8 | bytes[i];
|
|
14
|
-
bits += 8;
|
|
15
|
-
while (bits >= 5) {
|
|
16
|
-
bits -= 5;
|
|
17
|
-
codes[chi++] = valueToCharCode[value >>> bits & 31];
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
codes[chi] = valueToCharCode[value << 5 - bits & 31];
|
|
21
|
-
return String.fromCharCode.apply(null, codes);
|
|
22
|
-
}
|
|
23
|
-
function decodeBase32(str) {
|
|
24
|
-
const result = new Uint8Array(Math.floor(str.length * 5 / 8));
|
|
25
|
-
let bits = 0;
|
|
26
|
-
let value = 0;
|
|
27
|
-
let index = 0;
|
|
28
|
-
for (let i = 0; i < str.length; i++) {
|
|
29
|
-
const v = charCodeToValue[str.charCodeAt(i)];
|
|
30
|
-
value = value << 5 | v;
|
|
31
|
-
bits += 5;
|
|
32
|
-
if (bits >= 8) {
|
|
33
|
-
bits -= 8;
|
|
34
|
-
result[index++] = value >>> bits & 255;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return result;
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region src/id.ts
|
|
41
|
-
const hexCharCodeToNibble = new Uint8Array(128);
|
|
42
|
-
for (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;
|
|
43
|
-
for (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;
|
|
44
|
-
const defaultOptions = {
|
|
45
|
-
now: Date.now,
|
|
46
|
-
rng: (target) => {
|
|
47
|
-
const s = crypto.randomUUID();
|
|
48
|
-
target[0] = hexCharCodeToNibble[s.charCodeAt(0)] << 4 | hexCharCodeToNibble[s.charCodeAt(1)];
|
|
49
|
-
target[1] = hexCharCodeToNibble[s.charCodeAt(2)] << 4 | hexCharCodeToNibble[s.charCodeAt(3)];
|
|
50
|
-
target[2] = hexCharCodeToNibble[s.charCodeAt(4)] << 4 | hexCharCodeToNibble[s.charCodeAt(5)];
|
|
51
|
-
target[3] = hexCharCodeToNibble[s.charCodeAt(6)] << 4 | hexCharCodeToNibble[s.charCodeAt(7)];
|
|
52
|
-
target[4] = hexCharCodeToNibble[s.charCodeAt(9)] << 4 | hexCharCodeToNibble[s.charCodeAt(10)];
|
|
53
|
-
target[5] = hexCharCodeToNibble[s.charCodeAt(11)] << 4 | hexCharCodeToNibble[s.charCodeAt(12)];
|
|
54
|
-
target[6] = hexCharCodeToNibble[s.charCodeAt(24)] << 4 | hexCharCodeToNibble[s.charCodeAt(25)];
|
|
55
|
-
target[7] = hexCharCodeToNibble[s.charCodeAt(26)] << 4 | hexCharCodeToNibble[s.charCodeAt(27)];
|
|
56
|
-
target[8] = hexCharCodeToNibble[s.charCodeAt(28)] << 4 | hexCharCodeToNibble[s.charCodeAt(29)];
|
|
57
|
-
target[9] = hexCharCodeToNibble[s.charCodeAt(30)] << 4 | hexCharCodeToNibble[s.charCodeAt(31)];
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
const timestampByteLength = 6;
|
|
61
|
-
const randomByteLength = 10;
|
|
62
|
-
const totalByteLength = 16;
|
|
63
|
-
const base32Length = Math.ceil(totalByteLength * 8 / 5);
|
|
64
|
-
const timestampBase32Length = Math.ceil(timestampByteLength * 8 / 5);
|
|
65
|
-
const replacePattern = /[ilo]/g;
|
|
66
|
-
const aliasTestPattern = /[ilo]/;
|
|
67
|
-
const replacer = (match) => match === "o" ? "0" : "1";
|
|
68
|
-
const base32Pattern = new RegExp(`^[${alphabet}]{${base32Length}}$`);
|
|
69
|
-
const brandPattern = /^[a-z]{3}$/;
|
|
70
|
-
const base32CharClass = "[0-9a-hjkmnp-tv-z]";
|
|
71
|
-
const registeredBrands = /* @__PURE__ */ new Set();
|
|
72
|
-
const warnedBrands = /* @__PURE__ */ new Set();
|
|
73
|
-
function createId(brand, opts = {}) {
|
|
74
|
-
if (!brandPattern.test(brand)) throw new Error("invalid brand, expected three lowercase a-z characters");
|
|
75
|
-
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production" && !opts.allowDuplicateBrand) if (registeredBrands.has(brand)) {
|
|
76
|
-
if (!warnedBrands.has(brand)) {
|
|
77
|
-
console.warn(`[@smonn/ids] createId("${brand}") called more than once — this usually indicates a bundling or import bug. Pass { allowDuplicateBrand: true } to silence.`);
|
|
78
|
-
warnedBrands.add(brand);
|
|
79
|
-
}
|
|
80
|
-
} else registeredBrands.add(brand);
|
|
81
|
-
const options = {
|
|
82
|
-
...defaultOptions,
|
|
83
|
-
...opts
|
|
84
|
-
};
|
|
85
|
-
const prefix = `${brand}_`;
|
|
86
|
-
const buffer = new Uint8Array(totalByteLength);
|
|
87
|
-
const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);
|
|
88
|
-
return {
|
|
89
|
-
generate: () => generate(prefix, options, buffer, randomView),
|
|
90
|
-
generateAt: (date) => generate(prefix, options, buffer, randomView, date.getTime()),
|
|
91
|
-
is: (value) => is(prefix, value),
|
|
92
|
-
parse: (value) => parse(prefix, value),
|
|
93
|
-
safeParse: (value) => safeParse(prefix, value),
|
|
94
|
-
extractTimestamp: (id) => extractTimestamp(prefix, id),
|
|
95
|
-
minIdForTime: (date) => sentinelIdForTime(prefix, date, 0, buffer, randomView),
|
|
96
|
-
maxIdForTime: (date) => sentinelIdForTime(prefix, date, 255, buffer, randomView),
|
|
97
|
-
toJsonSchema: () => toJsonSchema(brand, prefix, options, buffer, randomView),
|
|
98
|
-
"~standard": {
|
|
99
|
-
version: 1,
|
|
100
|
-
vendor: "@smonn/ids",
|
|
101
|
-
validate: (value) => standardValidate(prefix, value)
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function toJsonSchema(brand, prefix, options, buffer, randomView) {
|
|
106
|
-
return {
|
|
107
|
-
type: "string",
|
|
108
|
-
pattern: `^${prefix}${base32CharClass}{${base32Length}}$`,
|
|
109
|
-
description: `Branded ID for '${brand}'`,
|
|
110
|
-
example: generate(prefix, options, buffer, randomView)
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function standardValidate(prefix, value) {
|
|
114
|
-
const result = safeParse(prefix, value);
|
|
115
|
-
if (result.ok) return { value: result.id };
|
|
116
|
-
return { issues: [{ message: errorMessage(prefix, result.error) }] };
|
|
117
|
-
}
|
|
118
|
-
function errorMessage(prefix, error) {
|
|
119
|
-
switch (error) {
|
|
120
|
-
case "not_string": return "expected string";
|
|
121
|
-
case "invalid_prefix": return `expected prefix '${prefix}'`;
|
|
122
|
-
case "invalid_base32": return "invalid base32 payload";
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function safeParse(prefix, value) {
|
|
126
|
-
if (typeof value !== "string") return {
|
|
127
|
-
ok: false,
|
|
128
|
-
error: "not_string"
|
|
129
|
-
};
|
|
130
|
-
const lowercase = value.toLowerCase();
|
|
131
|
-
if (!lowercase.startsWith(prefix)) return {
|
|
132
|
-
ok: false,
|
|
133
|
-
error: "invalid_prefix"
|
|
134
|
-
};
|
|
135
|
-
const sliced = lowercase.slice(prefix.length);
|
|
136
|
-
const base32 = aliasTestPattern.test(sliced) ? sliced.replaceAll(replacePattern, replacer) : sliced;
|
|
137
|
-
if (!base32Pattern.test(base32)) return {
|
|
138
|
-
ok: false,
|
|
139
|
-
error: "invalid_base32"
|
|
140
|
-
};
|
|
141
|
-
return {
|
|
142
|
-
ok: true,
|
|
143
|
-
id: prefix + base32
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
function parse(prefix, value) {
|
|
147
|
-
const result = safeParse(prefix, value);
|
|
148
|
-
if (result.ok) return result.id;
|
|
149
|
-
throw new Error(`Invalid ID: ${result.error}`);
|
|
150
|
-
}
|
|
151
|
-
function is(prefix, value) {
|
|
152
|
-
if (typeof value !== "string") return false;
|
|
153
|
-
if (!value.startsWith(prefix)) return false;
|
|
154
|
-
return base32Pattern.test(value.slice(prefix.length));
|
|
155
|
-
}
|
|
156
|
-
function writeTimestamp(ms, buffer) {
|
|
157
|
-
if (Number.isNaN(ms)) throw new Error("timestamp is not a number");
|
|
158
|
-
if (ms < 0) throw new Error("timestamp is negative");
|
|
159
|
-
if (ms >= 2 ** (timestampByteLength * 8)) throw new Error("timestamp exceeds 48-bit range");
|
|
160
|
-
for (let i = timestampByteLength - 1; i >= 0; i--) {
|
|
161
|
-
buffer[i] = ms % 256;
|
|
162
|
-
ms = Math.floor(ms / 256);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
function generate(prefix, options, buffer, randomView, ms = options.now()) {
|
|
166
|
-
writeTimestamp(ms, buffer);
|
|
167
|
-
options.rng(randomView);
|
|
168
|
-
return prefix + encodeBase32(buffer);
|
|
169
|
-
}
|
|
170
|
-
function sentinelIdForTime(prefix, date, fill, buffer, randomView) {
|
|
171
|
-
writeTimestamp(date.getTime(), buffer);
|
|
172
|
-
randomView.fill(fill);
|
|
173
|
-
return prefix + encodeBase32(buffer);
|
|
174
|
-
}
|
|
175
|
-
function extractTimestamp(prefix, id) {
|
|
176
|
-
const bytes = decodeBase32(id.slice(prefix.length, prefix.length + timestampBase32Length));
|
|
177
|
-
let ms = 0;
|
|
178
|
-
for (const byte of bytes) ms = ms * 256 + byte;
|
|
179
|
-
return new Date(ms);
|
|
180
|
-
}
|
|
181
|
-
//#endregion
|
|
182
|
-
export { createId as t };
|
|
183
|
-
|
|
184
|
-
//# sourceMappingURL=id-CFxcBxi-.mjs.map
|
package/dist/id-CFxcBxi-.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"id-CFxcBxi-.mjs","names":[],"sources":["../src/base32.ts","../src/id.ts"],"sourcesContent":["/*\n This is based on Crockford's Base32 spec: https://www.crockford.com/base32.html\n One difference is that it uses lowercase instead of uppercase when encoding.\n\n These functions are internal: callers (id.ts) guarantee that input is a\n 16-byte buffer for encode, or a string of characters drawn from the alphabet\n for decode. Invalid input produces silent garbage rather than a thrown error,\n consistent with the trust-the-type rule in ADR-0003.\n*/\n\nexport const alphabet = \"0123456789abcdefghjkmnpqrstvwxyz\";\n\n// 0–31 → ASCII char code, for write-into-codes-then-fromCharCode encoding.\nconst valueToCharCode = new Uint8Array(32);\nfor (let i = 0; i < 32; i++) valueToCharCode[i] = alphabet.charCodeAt(i);\n\n// charCode → 0–31 value. Canonical lowercase only; upstream resolves case and\n// o/i/l aliases before any string reaches decodeBase32.\nconst INVALID = 0xff;\nconst charCodeToValue = new Uint8Array(256).fill(INVALID);\nfor (let i = 0; i < alphabet.length; i++) charCodeToValue[alphabet.charCodeAt(i)] = i;\n\nexport function encodeBase32(bytes: Uint8Array): string {\n // Build an Array<number> of char codes and pass it to fromCharCode.apply.\n // Faster than `result += char` (avoids cons-string overhead) and than\n // Uint8Array variants (apply has a fast path for plain Arrays).\n // oxlint-disable-next-line no-new-array\n const codes = new Array<number>(Math.floor((bytes.length * 8) / 5) + 1);\n let chi = 0;\n let bits = 0;\n let value = 0;\n\n for (let i = 0; i < bytes.length; i++) {\n value = (value << 8) | bytes[i]!;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n codes[chi++] = valueToCharCode[(value >>> bits) & 0x1f]!;\n }\n }\n codes[chi] = valueToCharCode[(value << (5 - bits)) & 0x1f]!;\n return String.fromCharCode.apply(null, codes);\n}\n\nexport function decodeBase32(str: string): Uint8Array {\n const result = new Uint8Array(Math.floor((str.length * 5) / 8));\n let bits = 0;\n let value = 0;\n let index = 0;\n\n for (let i = 0; i < str.length; i++) {\n const v = charCodeToValue[str.charCodeAt(i)]!;\n value = (value << 5) | v;\n bits += 5;\n if (bits >= 8) {\n bits -= 8;\n result[index++] = (value >>> bits) & 0xff;\n }\n }\n return result;\n}\n","import { alphabet, decodeBase32, encodeBase32 } from \"./base32.js\";\n\nexport type Options = {\n now: () => number;\n rng: (target: Uint8Array) => void;\n allowDuplicateBrand?: boolean;\n};\n\n// hex charCode → 0–15 nibble, for decoding UUIDv4 strings into bytes.\n// Covers ['0'-'9' = 48–57] and ['a'-'f' = 97–102]; UUIDs are lowercase per spec.\nconst hexCharCodeToNibble = new Uint8Array(128);\nfor (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;\nfor (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;\n\nconst defaultOptions: Options = {\n now: Date.now,\n // crypto.randomUUID is ~7× faster than crypto.getRandomValues in Node 24\n // (~84 ns vs ~610 ns for a 16-byte fill — likely because the UUID path has\n // a tight fixed-format fast path). We use the 122 random bits of a UUIDv4\n // string as our entropy source, harvesting 10 fully-random bytes from\n // positions where no version (hex 12) or variant (hex 16) bits sit.\n // String layout: \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\" — bytes 0–5 are\n // string[0..7]+string[9..12], bytes 6–9 are string[24..31].\n rng: (target) => {\n const s = crypto.randomUUID();\n target[0] =\n (hexCharCodeToNibble[s.charCodeAt(0)]! << 4) | hexCharCodeToNibble[s.charCodeAt(1)]!;\n target[1] =\n (hexCharCodeToNibble[s.charCodeAt(2)]! << 4) | hexCharCodeToNibble[s.charCodeAt(3)]!;\n target[2] =\n (hexCharCodeToNibble[s.charCodeAt(4)]! << 4) | hexCharCodeToNibble[s.charCodeAt(5)]!;\n target[3] =\n (hexCharCodeToNibble[s.charCodeAt(6)]! << 4) | hexCharCodeToNibble[s.charCodeAt(7)]!;\n target[4] =\n (hexCharCodeToNibble[s.charCodeAt(9)]! << 4) | hexCharCodeToNibble[s.charCodeAt(10)]!;\n target[5] =\n (hexCharCodeToNibble[s.charCodeAt(11)]! << 4) | hexCharCodeToNibble[s.charCodeAt(12)]!;\n target[6] =\n (hexCharCodeToNibble[s.charCodeAt(24)]! << 4) | hexCharCodeToNibble[s.charCodeAt(25)]!;\n target[7] =\n (hexCharCodeToNibble[s.charCodeAt(26)]! << 4) | hexCharCodeToNibble[s.charCodeAt(27)]!;\n target[8] =\n (hexCharCodeToNibble[s.charCodeAt(28)]! << 4) | hexCharCodeToNibble[s.charCodeAt(29)]!;\n target[9] =\n (hexCharCodeToNibble[s.charCodeAt(30)]! << 4) | hexCharCodeToNibble[s.charCodeAt(31)]!;\n },\n};\n\ntype Prefix<Brand extends string> = `${Brand}_`;\n\nexport type Id<Brand extends string> = `${Prefix<Brand>}${string}` & {\n readonly __brand: Brand;\n};\n\nexport type ParseError = \"not_string\" | \"invalid_prefix\" | \"invalid_base32\";\n\nexport type ParseResult<Brand extends string> =\n | { ok: true; id: Id<Brand> }\n | { ok: false; error: ParseError };\n\nexport type JsonSchema = {\n readonly type: \"string\";\n readonly pattern: string;\n readonly description: string;\n readonly example: string;\n};\n\ntype StandardSchemaProps<Brand extends string> = {\n readonly version: 1;\n readonly vendor: \"@smonn/ids\";\n readonly validate: (\n value: unknown,\n options?: { readonly libraryOptions?: Record<string, unknown> | undefined },\n ) =>\n | { readonly value: Id<Brand>; readonly issues?: undefined }\n | { readonly issues: ReadonlyArray<{ readonly message: string }> };\n readonly types?: { readonly input: unknown; readonly output: Id<Brand> };\n};\n\nexport type Codec<Brand extends string> = {\n generate(): Id<Brand>;\n generateAt(date: Date): Id<Brand>;\n is(value: unknown): value is Id<Brand>;\n parse(value: unknown): Id<Brand>;\n safeParse(value: unknown): ParseResult<Brand>;\n extractTimestamp(id: Id<Brand>): Date;\n minIdForTime(date: Date): Id<Brand>;\n maxIdForTime(date: Date): Id<Brand>;\n toJsonSchema(): JsonSchema;\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\nconst timestampByteLength = 6;\nconst randomByteLength = 10;\nconst totalByteLength = timestampByteLength + randomByteLength;\nconst base32Length = Math.ceil((totalByteLength * 8) / 5);\nconst timestampBase32Length = Math.ceil((timestampByteLength * 8) / 5);\nconst replacePattern = /[ilo]/g;\nconst aliasTestPattern = /[ilo]/;\nconst replacer = (match: string): string => (match === \"o\" ? \"0\" : \"1\");\n\nconst base32Pattern = new RegExp(`^[${alphabet}]{${base32Length}}$`);\nconst brandPattern = /^[a-z]{3}$/;\n// Compact regex character class for the canonical lowercase Crockford alphabet\n// (`0123456789abcdefghjkmnpqrstvwxyz` — excludes i, l, o, u). Used in the JSON\n// Schema `pattern`, which describes the canonical wire form only (ADR-0003).\nconst base32CharClass = \"[0-9a-hjkmnp-tv-z]\";\n\nconst registeredBrands = new Set<string>();\nconst warnedBrands = new Set<string>();\n\nexport function createId<Brand extends string>(\n brand: Brand,\n opts: Partial<Options> = {},\n): Codec<Brand> {\n if (!brandPattern.test(brand)) {\n throw new Error(\"invalid brand, expected three lowercase a-z characters\");\n }\n\n if (\n typeof process !== \"undefined\" &&\n process.env.NODE_ENV !== \"production\" &&\n !opts.allowDuplicateBrand\n ) {\n if (registeredBrands.has(brand)) {\n if (!warnedBrands.has(brand)) {\n console.warn(\n `[@smonn/ids] createId(\"${brand}\") called more than once — this usually indicates a bundling or import bug. Pass { allowDuplicateBrand: true } to silence.`,\n );\n warnedBrands.add(brand);\n }\n } else {\n registeredBrands.add(brand);\n }\n }\n\n const options = {\n ...defaultOptions,\n ...opts,\n } satisfies Options;\n\n const prefix: Prefix<Brand> = `${brand}_`;\n // Per-codec scratch buffer. Shared across generate(), generateAt(),\n // minIdForTime(), and maxIdForTime() — all are synchronous and overwrite both\n // the timestamp and random slices before encoding, so successive callers see\n // their own freshly-written bytes. encodeBase32 reads the buffer and\n // returns an independent string, so the caller never sees the buffer itself.\n const buffer = new Uint8Array(totalByteLength);\n const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);\n\n return {\n generate: () => generate(prefix, options, buffer, randomView),\n generateAt: (date: Date) => generate(prefix, options, buffer, randomView, date.getTime()),\n is: (value: unknown) => is(prefix, value),\n parse: (value: unknown) => parse(prefix, value),\n safeParse: (value: unknown) => safeParse(prefix, value),\n extractTimestamp: (id: Id<Brand>) => extractTimestamp(prefix, id),\n minIdForTime: (date: Date) => sentinelIdForTime(prefix, date, 0x00, buffer, randomView),\n maxIdForTime: (date: Date) => sentinelIdForTime(prefix, date, 0xff, buffer, randomView),\n toJsonSchema: () => toJsonSchema(brand, prefix, options, buffer, randomView),\n \"~standard\": {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n },\n };\n}\n\nfunction toJsonSchema<Brand extends string>(\n brand: Brand,\n prefix: Prefix<Brand>,\n options: Options,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): JsonSchema {\n return {\n type: \"string\",\n pattern: `^${prefix}${base32CharClass}{${base32Length}}$`,\n description: `Branded ID for '${brand}'`,\n example: generate(prefix, options, buffer, randomView),\n };\n}\n\nfunction standardValidate<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n):\n | { readonly value: Id<Brand>; readonly issues?: undefined }\n | { readonly issues: ReadonlyArray<{ readonly message: string }> } {\n const result = safeParse(prefix, value);\n if (result.ok) return { value: result.id };\n return { issues: [{ message: errorMessage(prefix, result.error) }] };\n}\n\nfunction errorMessage<Brand extends string>(prefix: Prefix<Brand>, error: ParseError): string {\n switch (error) {\n case \"not_string\":\n return \"expected string\";\n case \"invalid_prefix\":\n return `expected prefix '${prefix}'`;\n case \"invalid_base32\":\n return \"invalid base32 payload\";\n }\n}\n\nfunction safeParse<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n): ParseResult<Brand> {\n if (typeof value !== \"string\") return { ok: false, error: \"not_string\" };\n const lowercase = value.toLowerCase();\n if (!lowercase.startsWith(prefix)) return { ok: false, error: \"invalid_prefix\" };\n\n const sliced = lowercase.slice(prefix.length);\n const base32 = aliasTestPattern.test(sliced)\n ? sliced.replaceAll(replacePattern, replacer)\n : sliced;\n\n if (!base32Pattern.test(base32)) return { ok: false, error: \"invalid_base32\" };\n\n const id = (prefix + base32) as Id<Brand>;\n return { ok: true, id };\n}\n\nfunction parse<Brand extends string>(prefix: Prefix<Brand>, value: unknown): Id<Brand> {\n const result = safeParse(prefix, value);\n if (result.ok) return result.id;\n throw new Error(`Invalid ID: ${result.error}`);\n}\n\nfunction is<Brand extends string>(prefix: Prefix<Brand>, value: unknown): value is Id<Brand> {\n if (typeof value !== \"string\") return false;\n if (!value.startsWith(prefix)) return false;\n return base32Pattern.test(value.slice(prefix.length));\n}\n\n// write the timestamp in big-endian; encoded via mod-256 to avoid 32-bit bitwise coercion\nfunction writeTimestamp(ms: number, buffer: Uint8Array): void {\n if (Number.isNaN(ms)) throw new Error(\"timestamp is not a number\");\n if (ms < 0) throw new Error(\"timestamp is negative\");\n if (ms >= 2 ** (timestampByteLength * 8)) throw new Error(\"timestamp exceeds 48-bit range\");\n for (let i = timestampByteLength - 1; i >= 0; i--) {\n buffer[i] = ms % 256;\n ms = Math.floor(ms / 256);\n }\n}\n\nfunction generate<Brand extends string>(\n prefix: Prefix<Brand>,\n options: Options,\n buffer: Uint8Array,\n randomView: Uint8Array,\n ms: number = options.now(),\n): Id<Brand> {\n writeTimestamp(ms, buffer);\n options.rng(randomView);\n return (prefix + encodeBase32(buffer)) as Id<Brand>;\n}\n\nfunction sentinelIdForTime<Brand extends string>(\n prefix: Prefix<Brand>,\n date: Date,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): Id<Brand> {\n writeTimestamp(date.getTime(), buffer);\n randomView.fill(fill);\n return (prefix + encodeBase32(buffer)) as Id<Brand>;\n}\n\nfunction extractTimestamp<Brand extends string>(prefix: Prefix<Brand>, id: Id<Brand>): Date {\n const base32 = id.slice(prefix.length, prefix.length + timestampBase32Length);\n const bytes = decodeBase32(base32);\n let ms = 0;\n for (const byte of bytes) {\n ms = ms * 256 + byte;\n }\n return new Date(ms);\n}\n"],"mappings":"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"}
|