@smonn/ids 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Public-facing branded IDs for TypeScript apps.
6
6
  pnpm add @smonn/ids
7
7
  ```
8
8
 
9
- Each ID looks like `usr_01h7b3k9rqxn4cw3p9r8t2sgkz`: a three-letter brand, an underscore, then 26 Crockford base32 characters 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.
9
+ Each ID looks like `usr_01h7b3k9rqxn4cw3p9r8t2sgkz`: a three-letter brand, an underscore, then 26 Crockford base32 characters of payload. The Timestamp 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. The Opaque 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
 
@@ -166,7 +166,7 @@ The `pattern` describes the **canonical form only** — it matches `generate()`
166
166
 
167
167
  ### "Don't leak creation time in IDs that customers can see"
168
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.
169
+ The Timestamp 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
170
 
171
171
  ```ts
172
172
  import { createOpaqueId, importOpaqueKey } from "@smonn/ids/opaque";
@@ -178,20 +178,22 @@ const id = await invoices.generate(); // "inv_…", timestamp not extractable wi
178
178
  await invoices.extractTimestamp(id); // Date — same codec, same key required
179
179
  ```
180
180
 
181
- Three differences from the default codec:
181
+ Three differences from the Timestamp codec:
182
182
 
183
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)).
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 Timestamp 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
186
 
187
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
188
 
189
+ To store or transport key material outside the library, `encodeOpaqueKey` / `decodeOpaqueKey` round-trip raw bytes in `hex` or `base64url` — distinct from the Crockford base32 used in ID payloads. The CLI's `keygen` subcommand emits keys in this format (see [CLI](#cli)).
190
+
189
191
  ## What this is **not** for
190
192
 
191
193
  - **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.
192
194
  - **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.
193
195
  - **Distributed-trace / request-correlation IDs.** Use OpenTelemetry-format IDs.
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).
196
+ - **Hiding creation time with the Timestamp 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).
195
197
 
196
198
  ## API surface
197
199
 
@@ -209,8 +211,11 @@ import {
209
211
  import {
210
212
  createOpaqueId, // (brand: string, opts: { key, now?, rng?, allowDuplicateBrand? }) => OpaqueCodec<Brand>
211
213
  importOpaqueKey, // (bytes: Uint8Array) => Promise<CryptoKey>
214
+ encodeOpaqueKey, // (bytes: Uint8Array, format: OpaqueKeyFormat) => string
215
+ decodeOpaqueKey, // (encoded: string, format: OpaqueKeyFormat) => Uint8Array
212
216
  type OpaqueCodec, // returned by createOpaqueId
213
217
  type OpaqueOptions, // { key, now, rng, allowDuplicateBrand } injection points
218
+ type OpaqueKeyFormat, // "hex" | "base64url"
214
219
  } from "@smonn/ids/opaque";
215
220
  ```
216
221
 
@@ -230,7 +235,11 @@ import {
230
235
 
231
236
  ## CLI
232
237
 
233
- Two brand-agnostic subcommands, no install required:
238
+ Brand-agnostic subcommands, no install required. Run `npx @smonn/ids --help` for the full flag list.
239
+
240
+ ### `inspect` (`i`)
241
+
242
+ Decode an ID and print brand, timestamp, canonical form, and whether the input was already canonical.
234
243
 
235
244
  ```bash
236
245
  $ npx @smonn/ids inspect usr_01h7b3k9rqxn1cw3p9r8t2sgkz
@@ -238,14 +247,54 @@ brand: usr
238
247
  timestamp: 1983-05-27T10:24:22.469Z (43 years ago)
239
248
  canonical: usr_01h7b3k9rqxn1cw3p9r8t2sgkz
240
249
  input: canonical
250
+ ```
241
251
 
252
+ Accepts non-canonical input (uppercase, Crockford aliases). Assumes the **Timestamp codec** — if the brand uses the **Opaque codec**, pass `--opaque` and set `IDS_KEY` (below); otherwise the timestamp line is meaningless garbage.
253
+
254
+ ```bash
255
+ IDS_KEY=<hex-or-base64url-key> npx @smonn/ids inspect inv_… --opaque
256
+ ```
257
+
258
+ Prints the decrypted timestamp **assuming `IDS_KEY` matches the key used at generation** — a well-formed but wrong key yields a plausible but incorrect timestamp, not an error (see [CONTEXT.md](./CONTEXT.md)).
259
+
260
+ ### `generate` (`g`)
261
+
262
+ Mint one or more canonical IDs for a brand. Output is one ID per line (pipeable).
263
+
264
+ ```bash
242
265
  $ npx @smonn/ids generate usr --count 3
243
266
  usr_…
244
267
  usr_…
245
268
  usr_…
246
269
  ```
247
270
 
248
- `inspect` accepts non-canonical input (uppercase, Crockford aliases) and shows the canonical form. `generate` prints one ID per line so output is pipeable. Invalid input prints the parse error to stderr and exits non-zero.
271
+ Flags: `--count` / `-c N` (default 1). Uses the Timestamp codec unless `--opaque` is set.
272
+
273
+ ```bash
274
+ IDS_KEY=<hex-or-base64url-key> npx @smonn/ids generate inv --opaque --count 2
275
+ ```
276
+
277
+ ### `keygen` (`k`)
278
+
279
+ Emit a random Opaque key to stdout (a secret — do not log or commit). Default: 256-bit hex.
280
+
281
+ ```bash
282
+ $ npx @smonn/ids keygen
283
+ a1b2c3…
284
+
285
+ $ npx @smonn/ids keygen --bits 128 --key-format base64url
286
+ AbCdEf…
287
+ ```
288
+
289
+ Flags: `--bits 128|192|256` (default 256), `--key-format hex|base64url` (default `hex`). `IDS_KEY_FORMAT` does not affect `keygen` — only `--key-format` on the command line. Output round-trips through `decodeOpaqueKey` / `importOpaqueKey`.
290
+
291
+ ### Opaque mode (`--opaque`)
292
+
293
+ `generate --opaque` and `inspect --opaque` read the AES key from the `IDS_KEY` environment variable — not from argv (argv leaks via `ps` and shell history). Missing or malformed `IDS_KEY` prints a clear stderr message and exits non-zero.
294
+
295
+ Key format defaults to `hex`; override per-invocation with `--key-format` or set `IDS_KEY_FORMAT=hex|base64url` for a session default. `--key-format` on the command line wins over `IDS_KEY_FORMAT`.
296
+
297
+ Invalid input prints the parse error to stderr and exits non-zero.
249
298
 
250
299
  ## Design
251
300
 
package/dist/cli.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { t as createId } from "./id-B4GiFKYV.mjs";
2
+ import { t as createId } from "./id-CcoPE2EX.mjs";
3
+ import { i as encodeOpaqueKey, n as importOpaqueKey, r as decodeOpaqueKey, t as createOpaqueId } from "./opaque-B-ttBfHO.mjs";
3
4
  //#region src/cli.ts
4
- function run(opts) {
5
+ async function run(opts) {
5
6
  const [subcommand, ...rest] = opts.argv;
6
7
  if (subcommand === "generate" || subcommand === "g") return runGenerate(rest, opts);
7
8
  if (subcommand === "inspect" || subcommand === "i") return runInspect(rest, opts);
9
+ if (subcommand === "keygen" || subcommand === "k") return runKeygen(rest, opts);
8
10
  if (subcommand === void 0 || subcommand === "--help" || subcommand === "-h") {
9
11
  opts.stdout(usage());
10
12
  return 0;
@@ -17,21 +19,72 @@ function usage() {
17
19
  "Usage: ids <subcommand> [args]",
18
20
  "",
19
21
  "Subcommands:",
20
- " inspect, i <id> Decode an ID and print brand, timestamp, and canonical form.",
21
- " generate, g <brand> [--count, -c N] Mint one or more canonical IDs for the given brand.",
22
+ " inspect, i <id> [--opaque] [--key-format hex|base64url]",
23
+ " Decode an ID and print brand, timestamp, and canonical form.",
24
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
25
+ " generate, g <brand> [--count, -c N] [--opaque] [--key-format hex|base64url]",
26
+ " Mint one or more canonical IDs for the given brand.",
27
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
28
+ " keygen, k [--bits 128|192|256] [--key-format hex|base64url]",
29
+ " Emit a random AES key for importOpaqueKey (stdout only).",
22
30
  ""
23
31
  ].join("\n");
24
32
  }
25
33
  function runInspect(args, opts) {
26
- const [input] = args;
34
+ const { flags, values, positionals } = splitFlags(args);
35
+ const [input] = positionals;
27
36
  if (input === void 0) {
28
37
  opts.stderr(usage());
29
- return 1;
38
+ return Promise.resolve(1);
30
39
  }
40
+ const opaque = flags.has("--opaque");
31
41
  const brand = input.slice(0, 3).toLowerCase();
42
+ if (opaque) {
43
+ const format = parseOpaqueKeyFormat(values, opts);
44
+ if (isKeyFormatError(format)) {
45
+ opts.stderr(format + "\n");
46
+ return Promise.resolve(1);
47
+ }
48
+ return runOpaqueInspect(brand, input, format, opts);
49
+ }
32
50
  let codec;
33
51
  try {
34
52
  codec = createId(brand, codecOpts(opts));
53
+ } catch (err) {
54
+ opts.stderr(err.message + "\n");
55
+ return Promise.resolve(1);
56
+ }
57
+ const validation = codec["~standard"].validate(input);
58
+ if (validation.issues) {
59
+ opts.stderr(validation.issues[0].message + "\n");
60
+ return Promise.resolve(1);
61
+ }
62
+ const canonical = validation.value;
63
+ const timestamp = codec.extractTimestamp(canonical);
64
+ const nowMs = (opts.now ?? Date.now)();
65
+ const relative = formatRelative(timestamp.getTime(), nowMs);
66
+ const inputLine = describeInputForm(input, canonical);
67
+ opts.stdout([
68
+ `brand: ${brand}`,
69
+ `timestamp: ${timestamp.toISOString()} (${relative})`,
70
+ `canonical: ${canonical}`,
71
+ `input: ${inputLine}`,
72
+ ""
73
+ ].join("\n"));
74
+ return Promise.resolve(0);
75
+ }
76
+ async function runOpaqueInspect(brand, input, format, opts) {
77
+ const keyResult = await loadOpaqueKey(opts, format);
78
+ if (typeof keyResult === "string") {
79
+ opts.stderr(keyResult + "\n");
80
+ return 1;
81
+ }
82
+ let codec;
83
+ try {
84
+ codec = createOpaqueId(brand, {
85
+ key: keyResult,
86
+ ...codecOpts(opts)
87
+ });
35
88
  } catch (err) {
36
89
  opts.stderr(err.message + "\n");
37
90
  return 1;
@@ -42,10 +95,11 @@ function runInspect(args, opts) {
42
95
  return 1;
43
96
  }
44
97
  const canonical = validation.value;
45
- const timestamp = codec.extractTimestamp(canonical);
98
+ const timestamp = await codec.extractTimestamp(canonical);
46
99
  const nowMs = (opts.now ?? Date.now)();
47
100
  const relative = formatRelative(timestamp.getTime(), nowMs);
48
101
  const inputLine = describeInputForm(input, canonical);
102
+ opts.stderr("note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp\n");
49
103
  opts.stdout([
50
104
  `brand: ${brand}`,
51
105
  `timestamp: ${timestamp.toISOString()} (${relative})`,
@@ -89,30 +143,171 @@ function unit(n, name) {
89
143
  return `${n} ${n === 1 ? name : `${name}s`}`;
90
144
  }
91
145
  function runGenerate(args, opts) {
92
- const [brand, ...flags] = args;
93
- const count = parseCount(flags);
146
+ const { flags, values, positionals } = splitFlags(args);
147
+ const [brand] = positionals;
148
+ const count = parseCount(values);
94
149
  if (typeof count === "string") {
95
150
  opts.stderr(count + "\n");
96
- return 1;
151
+ return Promise.resolve(1);
152
+ }
153
+ if (flags.has("--opaque")) {
154
+ const format = parseOpaqueKeyFormat(values, opts);
155
+ if (isKeyFormatError(format)) {
156
+ opts.stderr(format + "\n");
157
+ return Promise.resolve(1);
158
+ }
159
+ return runOpaqueGenerate(brand ?? "", count, format, opts);
97
160
  }
98
161
  let codec;
99
162
  try {
100
163
  codec = createId(brand ?? "", codecOpts(opts));
101
164
  } catch (err) {
102
165
  opts.stderr(err.message + "\n");
103
- return 1;
166
+ return Promise.resolve(1);
104
167
  }
105
168
  for (let i = 0; i < count; i++) opts.stdout(codec.generate() + "\n");
169
+ return Promise.resolve(0);
170
+ }
171
+ async function runOpaqueGenerate(brand, count, format, opts) {
172
+ const keyResult = await loadOpaqueKey(opts, format);
173
+ if (typeof keyResult === "string") {
174
+ opts.stderr(keyResult + "\n");
175
+ return 1;
176
+ }
177
+ let codec;
178
+ try {
179
+ codec = createOpaqueId(brand, {
180
+ key: keyResult,
181
+ ...codecOpts(opts)
182
+ });
183
+ } catch (err) {
184
+ opts.stderr(err.message + "\n");
185
+ return 1;
186
+ }
187
+ for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
106
188
  return 0;
107
189
  }
108
- function parseCount(flags) {
109
- const idx = flags.findIndex((f) => f === "--count" || f === "-c");
110
- if (idx === -1) return 1;
111
- const raw = flags[idx + 1];
112
- if (raw === void 0) return "--count requires a value";
190
+ function runKeygen(args, opts) {
191
+ const { values } = splitFlags(args);
192
+ const bits = parseBits(values);
193
+ if (typeof bits === "string") {
194
+ opts.stderr(bits + "\n");
195
+ return Promise.resolve(1);
196
+ }
197
+ const format = parseKeygenFormat(values);
198
+ if (isKeyFormatError(format)) {
199
+ opts.stderr(format + "\n");
200
+ return Promise.resolve(1);
201
+ }
202
+ const bytes = new Uint8Array(bits / 8);
203
+ crypto.getRandomValues(bytes);
204
+ opts.stdout(encodeOpaqueKey(bytes, format) + "\n");
205
+ return Promise.resolve(0);
206
+ }
207
+ async function loadOpaqueKey(opts, format) {
208
+ const raw = (opts.env ?? process.env).IDS_KEY;
209
+ if (raw === void 0 || raw === "") return "missing IDS_KEY environment variable";
210
+ try {
211
+ return importOpaqueKey(decodeOpaqueKey(raw, format));
212
+ } catch (err) {
213
+ return err.message;
214
+ }
215
+ }
216
+ function parseCount(values) {
217
+ const raw = values.get("--count") ?? values.get("-c");
218
+ if (raw === void 0) return 1;
219
+ if (raw === "") return "--count requires a value";
113
220
  if (!/^[1-9][0-9]*$/.test(raw)) return `--count must be a positive integer, got '${raw}'`;
114
221
  return Number(raw);
115
222
  }
223
+ function parseBits(values) {
224
+ const raw = values.get("--bits");
225
+ if (raw === void 0) return 256;
226
+ if (raw === "") return "--bits requires a value";
227
+ if (raw === "128") return 128;
228
+ if (raw === "192") return 192;
229
+ if (raw === "256") return 256;
230
+ return `--bits must be 128, 192, or 256, got '${raw}'`;
231
+ }
232
+ function isKeyFormatError(result) {
233
+ return result !== "hex" && result !== "base64url";
234
+ }
235
+ function parseKeyFormatFlag(values) {
236
+ const fromFlag = values.get("--key-format");
237
+ if (fromFlag === void 0) return void 0;
238
+ if (fromFlag === "") return "--key-format requires a value";
239
+ if (fromFlag === "hex" || fromFlag === "base64url") return fromFlag;
240
+ return `--key-format must be hex or base64url, got '${fromFlag}'`;
241
+ }
242
+ function parseKeygenFormat(values) {
243
+ const fromFlag = parseKeyFormatFlag(values);
244
+ if (fromFlag === void 0) return "hex";
245
+ return fromFlag;
246
+ }
247
+ function parseOpaqueKeyFormat(values, opts) {
248
+ const fromFlag = parseKeyFormatFlag(values);
249
+ if (fromFlag !== void 0) return fromFlag;
250
+ const fromEnv = (opts.env ?? process.env).IDS_KEY_FORMAT;
251
+ if (fromEnv === void 0 || fromEnv === "") return "hex";
252
+ if (fromEnv === "hex" || fromEnv === "base64url") return fromEnv;
253
+ return `IDS_KEY_FORMAT must be hex or base64url, got '${fromEnv}'`;
254
+ }
255
+ function splitFlagToken(arg) {
256
+ const eq = arg.indexOf("=");
257
+ if (eq <= 0) return {
258
+ flag: arg,
259
+ inlineValue: void 0
260
+ };
261
+ return {
262
+ flag: arg.slice(0, eq),
263
+ inlineValue: arg.slice(eq + 1)
264
+ };
265
+ }
266
+ function splitFlags(args) {
267
+ const flags = /* @__PURE__ */ new Set();
268
+ const values = /* @__PURE__ */ new Map();
269
+ const positionals = [];
270
+ const valueFlags = new Set([
271
+ "--count",
272
+ "-c",
273
+ "--bits",
274
+ "--key-format"
275
+ ]);
276
+ for (let i = 0; i < args.length; i++) {
277
+ const raw = args[i];
278
+ const { flag, inlineValue } = splitFlagToken(raw);
279
+ if (flag === "--opaque") {
280
+ flags.add(flag);
281
+ continue;
282
+ }
283
+ if (valueFlags.has(flag)) {
284
+ if (inlineValue !== void 0) {
285
+ flags.add(flag);
286
+ values.set(flag, inlineValue);
287
+ continue;
288
+ }
289
+ const value = args[i + 1];
290
+ if (value === void 0 || value.startsWith("-")) {
291
+ values.set(flag, "");
292
+ continue;
293
+ }
294
+ flags.add(flag);
295
+ values.set(flag, value);
296
+ i++;
297
+ continue;
298
+ }
299
+ if (flag.startsWith("-")) {
300
+ flags.add(flag);
301
+ continue;
302
+ }
303
+ positionals.push(raw);
304
+ }
305
+ return {
306
+ flags,
307
+ values,
308
+ positionals
309
+ };
310
+ }
116
311
  function codecOpts(opts) {
117
312
  const o = { allowDuplicateBrand: true };
118
313
  if (opts.now !== void 0) o.now = opts.now;
@@ -121,7 +316,7 @@ function codecOpts(opts) {
121
316
  }
122
317
  //#endregion
123
318
  //#region bin/cli.ts
124
- process.exitCode = run({
319
+ process.exitCode = await run({
125
320
  argv: process.argv.slice(2),
126
321
  stdout: (s) => process.stdout.write(s),
127
322
  stderr: (s) => process.stderr.write(s)
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 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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"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts","../bin/cli.ts"],"sourcesContent":["import { createId, type Options } from \"./id.js\";\nimport {\n createOpaqueId,\n decodeOpaqueKey,\n encodeOpaqueKey,\n importOpaqueKey,\n type OpaqueKeyFormat,\n} from \"./opaque.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 /** Defaults to `process.env`. Injected in tests for `IDS_KEY`. */\n env?: Readonly<Record<string, string | undefined>>;\n};\n\nexport async function run(opts: RunOpts): Promise<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 === \"keygen\" || subcommand === \"k\") return runKeygen(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> [--opaque] [--key-format hex|base64url]\",\n \" Decode an ID and print brand, timestamp, and canonical form.\",\n \" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).\",\n \" generate, g <brand> [--count, -c N] [--opaque] [--key-format hex|base64url]\",\n \" Mint one or more canonical IDs for the given brand.\",\n \" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).\",\n \" keygen, k [--bits 128|192|256] [--key-format hex|base64url]\",\n \" Emit a random AES key for importOpaqueKey (stdout only).\",\n \"\",\n ].join(\"\\n\");\n}\n\nfunction runInspect(args: ReadonlyArray<string>, opts: RunOpts): Promise<number> {\n const { flags, values, positionals } = splitFlags(args);\n const [input] = positionals;\n if (input === undefined) {\n opts.stderr(usage());\n return Promise.resolve(1);\n }\n const opaque = flags.has(\"--opaque\");\n const brand = input.slice(0, 3).toLowerCase();\n if (opaque) {\n const format = parseOpaqueKeyFormat(values, opts);\n if (isKeyFormatError(format)) {\n opts.stderr(format + \"\\n\");\n return Promise.resolve(1);\n }\n return runOpaqueInspect(brand, input, format, opts);\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 Promise.resolve(1);\n }\n const validation = codec[\"~standard\"].validate(input);\n if (validation.issues) {\n opts.stderr(validation.issues[0]!.message + \"\\n\");\n return Promise.resolve(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 Promise.resolve(0);\n}\n\nasync function runOpaqueInspect(\n brand: string,\n input: string,\n format: OpaqueKeyFormat,\n opts: RunOpts,\n): Promise<number> {\n const keyResult = await loadOpaqueKey(opts, format);\n if (typeof keyResult === \"string\") {\n opts.stderr(keyResult + \"\\n\");\n return 1;\n }\n let codec;\n try {\n codec = createOpaqueId(brand, { key: keyResult, ...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 = await 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.stderr(\n \"note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp\\n\",\n );\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): Promise<number> {\n const { flags, values, positionals } = splitFlags(args);\n const [brand] = positionals;\n const count = parseCount(values);\n if (typeof count === \"string\") {\n opts.stderr(count + \"\\n\");\n return Promise.resolve(1);\n }\n const opaque = flags.has(\"--opaque\");\n if (opaque) {\n const format = parseOpaqueKeyFormat(values, opts);\n if (isKeyFormatError(format)) {\n opts.stderr(format + \"\\n\");\n return Promise.resolve(1);\n }\n return runOpaqueGenerate(brand ?? \"\", count, format, opts);\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 Promise.resolve(1);\n }\n for (let i = 0; i < count; i++) opts.stdout(codec.generate() + \"\\n\");\n return Promise.resolve(0);\n}\n\nasync function runOpaqueGenerate(\n brand: string,\n count: number,\n format: OpaqueKeyFormat,\n opts: RunOpts,\n): Promise<number> {\n const keyResult = await loadOpaqueKey(opts, format);\n if (typeof keyResult === \"string\") {\n opts.stderr(keyResult + \"\\n\");\n return 1;\n }\n let codec;\n try {\n codec = createOpaqueId(brand, { key: keyResult, ...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((await codec.generate()) + \"\\n\");\n return 0;\n}\n\nfunction runKeygen(args: ReadonlyArray<string>, opts: RunOpts): Promise<number> {\n const { values } = splitFlags(args);\n const bits = parseBits(values);\n if (typeof bits === \"string\") {\n opts.stderr(bits + \"\\n\");\n return Promise.resolve(1);\n }\n const format = parseKeygenFormat(values);\n if (isKeyFormatError(format)) {\n opts.stderr(format + \"\\n\");\n return Promise.resolve(1);\n }\n const bytes = new Uint8Array(bits / 8);\n crypto.getRandomValues(bytes);\n opts.stdout(encodeOpaqueKey(bytes, format) + \"\\n\");\n return Promise.resolve(0);\n}\n\nasync function loadOpaqueKey(opts: RunOpts, format: OpaqueKeyFormat): Promise<CryptoKey | string> {\n const env = opts.env ?? process.env;\n const raw = env.IDS_KEY;\n if (raw === undefined || raw === \"\") return \"missing IDS_KEY environment variable\";\n try {\n return importOpaqueKey(decodeOpaqueKey(raw, format));\n } catch (err) {\n return (err as Error).message;\n }\n}\n\nfunction parseCount(values: Map<string, string>): number | string {\n const raw = values.get(\"--count\") ?? values.get(\"-c\");\n if (raw === undefined) return 1;\n if (raw === \"\") 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 parseBits(values: Map<string, string>): number | string {\n const raw = values.get(\"--bits\");\n if (raw === undefined) return 256;\n if (raw === \"\") return \"--bits requires a value\";\n if (raw === \"128\") return 128;\n if (raw === \"192\") return 192;\n if (raw === \"256\") return 256;\n return `--bits must be 128, 192, or 256, got '${raw}'`;\n}\n\nfunction isKeyFormatError(result: OpaqueKeyFormat | string): result is string {\n return result !== \"hex\" && result !== \"base64url\";\n}\n\nfunction parseKeyFormatFlag(values: Map<string, string>): OpaqueKeyFormat | string | undefined {\n const fromFlag = values.get(\"--key-format\");\n if (fromFlag === undefined) return undefined;\n if (fromFlag === \"\") return \"--key-format requires a value\";\n if (fromFlag === \"hex\" || fromFlag === \"base64url\") return fromFlag;\n return `--key-format must be hex or base64url, got '${fromFlag}'`;\n}\n\nfunction parseKeygenFormat(values: Map<string, string>): OpaqueKeyFormat | string {\n const fromFlag = parseKeyFormatFlag(values);\n if (fromFlag === undefined) return \"hex\";\n return fromFlag;\n}\n\nfunction parseOpaqueKeyFormat(\n values: Map<string, string>,\n opts: RunOpts,\n): OpaqueKeyFormat | string {\n const fromFlag = parseKeyFormatFlag(values);\n if (fromFlag !== undefined) return fromFlag;\n const env = opts.env ?? process.env;\n const fromEnv = env.IDS_KEY_FORMAT;\n if (fromEnv === undefined || fromEnv === \"\") return \"hex\";\n if (fromEnv === \"hex\" || fromEnv === \"base64url\") return fromEnv;\n return `IDS_KEY_FORMAT must be hex or base64url, got '${fromEnv}'`;\n}\n\ntype ParsedFlags = {\n flags: Set<string>;\n values: Map<string, string>;\n positionals: string[];\n};\n\nfunction splitFlagToken(arg: string): { flag: string; inlineValue: string | undefined } {\n const eq = arg.indexOf(\"=\");\n if (eq <= 0) return { flag: arg, inlineValue: undefined };\n return { flag: arg.slice(0, eq), inlineValue: arg.slice(eq + 1) };\n}\n\nfunction splitFlags(args: ReadonlyArray<string>): ParsedFlags {\n const flags = new Set<string>();\n const values = new Map<string, string>();\n const positionals: string[] = [];\n const valueFlags = new Set([\"--count\", \"-c\", \"--bits\", \"--key-format\"]);\n for (let i = 0; i < args.length; i++) {\n const raw = args[i]!;\n const { flag, inlineValue } = splitFlagToken(raw);\n if (flag === \"--opaque\") {\n flags.add(flag);\n continue;\n }\n if (valueFlags.has(flag)) {\n if (inlineValue !== undefined) {\n flags.add(flag);\n values.set(flag, inlineValue);\n continue;\n }\n const value = args[i + 1];\n if (value === undefined || value.startsWith(\"-\")) {\n values.set(flag, \"\");\n continue;\n }\n flags.add(flag);\n values.set(flag, value);\n i++;\n continue;\n }\n if (flag.startsWith(\"-\")) {\n flags.add(flag);\n continue;\n }\n positionals.push(raw);\n }\n return { flags, values, positionals };\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 = await run({\n argv: process.argv.slice(2),\n stdout: (s) => process.stdout.write(s),\n stderr: (s) => process.stderr.write(s),\n});\n"],"mappings":";;;;AAoBA,eAAsB,IAAI,MAAgC;CACxD,MAAM,CAAC,YAAY,GAAG,QAAQ,KAAK;CACnC,IAAI,eAAe,cAAc,eAAe,KAAK,OAAO,YAAY,MAAM,IAAI;CAClF,IAAI,eAAe,aAAa,eAAe,KAAK,OAAO,WAAW,MAAM,IAAI;CAChF,IAAI,eAAe,YAAY,eAAe,KAAK,OAAO,UAAU,MAAM,IAAI;CAC9E,IAAI,eAAe,KAAA,KAAa,eAAe,YAAY,eAAe,MAAM;EAC9E,KAAK,OAAO,MAAM,CAAC;EACnB,OAAO;CACT;CACA,KAAK,OAAO,MAAM,CAAC;CACnB,OAAO;AACT;AAEA,SAAS,QAAgB;CACvB,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,EAAE,KAAK,IAAI;AACb;AAEA,SAAS,WAAW,MAA6B,MAAgC;CAC/E,MAAM,EAAE,OAAO,QAAQ,gBAAgB,WAAW,IAAI;CACtD,MAAM,CAAC,SAAS;CAChB,IAAI,UAAU,KAAA,GAAW;EACvB,KAAK,OAAO,MAAM,CAAC;EACnB,OAAO,QAAQ,QAAQ,CAAC;CAC1B;CACA,MAAM,SAAS,MAAM,IAAI,UAAU;CACnC,MAAM,QAAQ,MAAM,MAAM,GAAG,CAAC,EAAE,YAAY;CAC5C,IAAI,QAAQ;EACV,MAAM,SAAS,qBAAqB,QAAQ,IAAI;EAChD,IAAI,iBAAiB,MAAM,GAAG;GAC5B,KAAK,OAAO,SAAS,IAAI;GACzB,OAAO,QAAQ,QAAQ,CAAC;EAC1B;EACA,OAAO,iBAAiB,OAAO,OAAO,QAAQ,IAAI;CACpD;CACA,IAAI;CACJ,IAAI;EACF,QAAQ,SAAS,OAAO,UAAU,IAAI,CAAC;CACzC,SAAS,KAAK;EACZ,KAAK,OAAQ,IAAc,UAAU,IAAI;EACzC,OAAO,QAAQ,QAAQ,CAAC;CAC1B;CACA,MAAM,aAAa,MAAM,aAAa,SAAS,KAAK;CACpD,IAAI,WAAW,QAAQ;EACrB,KAAK,OAAO,WAAW,OAAO,GAAI,UAAU,IAAI;EAChD,OAAO,QAAQ,QAAQ,CAAC;CAC1B;CACA,MAAM,YAAY,WAAW;CAC7B,MAAM,YAAY,MAAM,iBAAiB,SAAS;CAClD,MAAM,SAAS,KAAK,OAAO,KAAK,KAAK;CACrC,MAAM,WAAW,eAAe,UAAU,QAAQ,GAAG,KAAK;CAC1D,MAAM,YAAY,kBAAkB,OAAO,SAAS;CACpD,KAAK,OACH;EACE,cAAc;EACd,cAAc,UAAU,YAAY,EAAE,IAAI,SAAS;EACnD,cAAc;EACd,cAAc;EACd;CACF,EAAE,KAAK,IAAI,CACb;CACA,OAAO,QAAQ,QAAQ,CAAC;AAC1B;AAEA,eAAe,iBACb,OACA,OACA,QACA,MACiB;CACjB,MAAM,YAAY,MAAM,cAAc,MAAM,MAAM;CAClD,IAAI,OAAO,cAAc,UAAU;EACjC,KAAK,OAAO,YAAY,IAAI;EAC5B,OAAO;CACT;CACA,IAAI;CACJ,IAAI;EACF,QAAQ,eAAe,OAAO;GAAE,KAAK;GAAW,GAAG,UAAU,IAAI;EAAE,CAAC;CACtE,SAAS,KAAK;EACZ,KAAK,OAAQ,IAAc,UAAU,IAAI;EACzC,OAAO;CACT;CACA,MAAM,aAAa,MAAM,aAAa,SAAS,KAAK;CACpD,IAAI,WAAW,QAAQ;EACrB,KAAK,OAAO,WAAW,OAAO,GAAI,UAAU,IAAI;EAChD,OAAO;CACT;CACA,MAAM,YAAY,WAAW;CAC7B,MAAM,YAAY,MAAM,MAAM,iBAAiB,SAAS;CACxD,MAAM,SAAS,KAAK,OAAO,KAAK,KAAK;CACrC,MAAM,WAAW,eAAe,UAAU,QAAQ,GAAG,KAAK;CAC1D,MAAM,YAAY,kBAAkB,OAAO,SAAS;CACpD,KAAK,OACH,8HACF;CACA,KAAK,OACH;EACE,cAAc;EACd,cAAc,UAAU,YAAY,EAAE,IAAI,SAAS;EACnD,cAAc;EACd,cAAc;EACd;CACF,EAAE,KAAK,IAAI,CACb;CACA,OAAO;AACT;AAEA,SAAS,kBAAkB,OAAe,WAA+B;CACvE,IAAI,UAAU,WAAW,OAAO;CAChC,MAAM,QAAkB,CAAC;CACzB,IAAI,UAAU,MAAM,YAAY,GAAG,MAAM,KAAK,eAAe;CAC7D,IAAI,SAAS,KAAK,MAAM,MAAM,CAAC,CAAC,GAAG,MAAM,KAAK,wBAAwB;CACtE,OAAO,kBAAkB,MAAM,KAAK,KAAK,EAAE;AAC7C;AAGA,MAAM,cAAc,KAAK;AACzB,MAAM,YAAY,KAAK;AACvB,MAAM,WAAW,KAAK;AACtB,MAAM,eAAe;AACrB,MAAM,gBAAgB;AAEtB,SAAS,eAAe,QAAgB,OAAuB;CAC7D,MAAM,OAAO,QAAQ;CACrB,MAAM,MAAM,KAAK,IAAI,IAAI;CACzB,MAAM,SAAS,OAAO,IAAI,aAAa;CAEvC,MAAM,OAAO,UAAU,GAAG;CAC1B,OAAO,SAAS,KAAK,aAAa,GAAG,KAAK,GAAG;AAC/C;AAEA,SAAS,UAAU,KAAqB;CACtC,IAAI,MAAM,aAAa,OAAO;CAC9B,IAAI,MAAM,WAAW,OAAO,KAAK,KAAK,MAAM,MAAM,WAAW,GAAG,QAAQ;CACxE,IAAI,MAAM,UAAU,OAAO,KAAK,KAAK,MAAM,MAAM,SAAS,GAAG,MAAM;CACnE,IAAI,MAAM,WAAW,cAAc,OAAO,KAAK,KAAK,MAAM,MAAM,QAAQ,GAAG,KAAK;CAEhF,MAAM,cAAc,KAAK,MAAM,OAAO,WAAW,aAAa;CAC9D,IAAI,cAAc,eAAe,OAAO,KAAK,aAAa,OAAO;CAEjE,MAAM,QAAQ,KAAK,MAAM,cAAc,aAAa;CACpD,MAAM,SAAS,cAAc;CAC7B,OAAO,WAAW,IAAI,KAAK,OAAO,MAAM,IAAI,GAAG,KAAK,OAAO,MAAM,EAAE,GAAG,KAAK,QAAQ,OAAO;AAC5F;AAEA,SAAS,KAAK,GAAW,MAAsB;CAC7C,OAAO,GAAG,EAAE,GAAG,MAAM,IAAI,OAAO,GAAG,KAAK;AAC1C;AAEA,SAAS,YAAY,MAA6B,MAAgC;CAChF,MAAM,EAAE,OAAO,QAAQ,gBAAgB,WAAW,IAAI;CACtD,MAAM,CAAC,SAAS;CAChB,MAAM,QAAQ,WAAW,MAAM;CAC/B,IAAI,OAAO,UAAU,UAAU;EAC7B,KAAK,OAAO,QAAQ,IAAI;EACxB,OAAO,QAAQ,QAAQ,CAAC;CAC1B;CAEA,IADe,MAAM,IAAI,UAChB,GAAG;EACV,MAAM,SAAS,qBAAqB,QAAQ,IAAI;EAChD,IAAI,iBAAiB,MAAM,GAAG;GAC5B,KAAK,OAAO,SAAS,IAAI;GACzB,OAAO,QAAQ,QAAQ,CAAC;EAC1B;EACA,OAAO,kBAAkB,SAAS,IAAI,OAAO,QAAQ,IAAI;CAC3D;CACA,IAAI;CACJ,IAAI;EACF,QAAQ,SAAS,SAAS,IAAI,UAAU,IAAI,CAAC;CAC/C,SAAS,KAAK;EACZ,KAAK,OAAQ,IAAc,UAAU,IAAI;EACzC,OAAO,QAAQ,QAAQ,CAAC;CAC1B;CACA,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,KAAK,OAAO,MAAM,SAAS,IAAI,IAAI;CACnE,OAAO,QAAQ,QAAQ,CAAC;AAC1B;AAEA,eAAe,kBACb,OACA,OACA,QACA,MACiB;CACjB,MAAM,YAAY,MAAM,cAAc,MAAM,MAAM;CAClD,IAAI,OAAO,cAAc,UAAU;EACjC,KAAK,OAAO,YAAY,IAAI;EAC5B,OAAO;CACT;CACA,IAAI;CACJ,IAAI;EACF,QAAQ,eAAe,OAAO;GAAE,KAAK;GAAW,GAAG,UAAU,IAAI;EAAE,CAAC;CACtE,SAAS,KAAK;EACZ,KAAK,OAAQ,IAAc,UAAU,IAAI;EACzC,OAAO;CACT;CACA,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,KAAK,OAAQ,MAAM,MAAM,SAAS,IAAK,IAAI;CAC3E,OAAO;AACT;AAEA,SAAS,UAAU,MAA6B,MAAgC;CAC9E,MAAM,EAAE,WAAW,WAAW,IAAI;CAClC,MAAM,OAAO,UAAU,MAAM;CAC7B,IAAI,OAAO,SAAS,UAAU;EAC5B,KAAK,OAAO,OAAO,IAAI;EACvB,OAAO,QAAQ,QAAQ,CAAC;CAC1B;CACA,MAAM,SAAS,kBAAkB,MAAM;CACvC,IAAI,iBAAiB,MAAM,GAAG;EAC5B,KAAK,OAAO,SAAS,IAAI;EACzB,OAAO,QAAQ,QAAQ,CAAC;CAC1B;CACA,MAAM,QAAQ,IAAI,WAAW,OAAO,CAAC;CACrC,OAAO,gBAAgB,KAAK;CAC5B,KAAK,OAAO,gBAAgB,OAAO,MAAM,IAAI,IAAI;CACjD,OAAO,QAAQ,QAAQ,CAAC;AAC1B;AAEA,eAAe,cAAc,MAAe,QAAsD;CAEhG,MAAM,OADM,KAAK,OAAO,QAAQ,KAChB;CAChB,IAAI,QAAQ,KAAA,KAAa,QAAQ,IAAI,OAAO;CAC5C,IAAI;EACF,OAAO,gBAAgB,gBAAgB,KAAK,MAAM,CAAC;CACrD,SAAS,KAAK;EACZ,OAAQ,IAAc;CACxB;AACF;AAEA,SAAS,WAAW,QAA8C;CAChE,MAAM,MAAM,OAAO,IAAI,SAAS,KAAK,OAAO,IAAI,IAAI;CACpD,IAAI,QAAQ,KAAA,GAAW,OAAO;CAC9B,IAAI,QAAQ,IAAI,OAAO;CACvB,IAAI,CAAC,gBAAgB,KAAK,GAAG,GAAG,OAAO,4CAA4C,IAAI;CACvF,OAAO,OAAO,GAAG;AACnB;AAEA,SAAS,UAAU,QAA8C;CAC/D,MAAM,MAAM,OAAO,IAAI,QAAQ;CAC/B,IAAI,QAAQ,KAAA,GAAW,OAAO;CAC9B,IAAI,QAAQ,IAAI,OAAO;CACvB,IAAI,QAAQ,OAAO,OAAO;CAC1B,IAAI,QAAQ,OAAO,OAAO;CAC1B,IAAI,QAAQ,OAAO,OAAO;CAC1B,OAAO,yCAAyC,IAAI;AACtD;AAEA,SAAS,iBAAiB,QAAoD;CAC5E,OAAO,WAAW,SAAS,WAAW;AACxC;AAEA,SAAS,mBAAmB,QAAmE;CAC7F,MAAM,WAAW,OAAO,IAAI,cAAc;CAC1C,IAAI,aAAa,KAAA,GAAW,OAAO,KAAA;CACnC,IAAI,aAAa,IAAI,OAAO;CAC5B,IAAI,aAAa,SAAS,aAAa,aAAa,OAAO;CAC3D,OAAO,+CAA+C,SAAS;AACjE;AAEA,SAAS,kBAAkB,QAAuD;CAChF,MAAM,WAAW,mBAAmB,MAAM;CAC1C,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,OAAO;AACT;AAEA,SAAS,qBACP,QACA,MAC0B;CAC1B,MAAM,WAAW,mBAAmB,MAAM;CAC1C,IAAI,aAAa,KAAA,GAAW,OAAO;CAEnC,MAAM,WADM,KAAK,OAAO,QAAQ,KACZ;CACpB,IAAI,YAAY,KAAA,KAAa,YAAY,IAAI,OAAO;CACpD,IAAI,YAAY,SAAS,YAAY,aAAa,OAAO;CACzD,OAAO,iDAAiD,QAAQ;AAClE;AAQA,SAAS,eAAe,KAAgE;CACtF,MAAM,KAAK,IAAI,QAAQ,GAAG;CAC1B,IAAI,MAAM,GAAG,OAAO;EAAE,MAAM;EAAK,aAAa,KAAA;CAAU;CACxD,OAAO;EAAE,MAAM,IAAI,MAAM,GAAG,EAAE;EAAG,aAAa,IAAI,MAAM,KAAK,CAAC;CAAE;AAClE;AAEA,SAAS,WAAW,MAA0C;CAC5D,MAAM,wBAAQ,IAAI,IAAY;CAC9B,MAAM,yBAAS,IAAI,IAAoB;CACvC,MAAM,cAAwB,CAAC;CAC/B,MAAM,aAAa,IAAI,IAAI;EAAC;EAAW;EAAM;EAAU;CAAc,CAAC;CACtE,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,MAAM,EAAE,MAAM,gBAAgB,eAAe,GAAG;EAChD,IAAI,SAAS,YAAY;GACvB,MAAM,IAAI,IAAI;GACd;EACF;EACA,IAAI,WAAW,IAAI,IAAI,GAAG;GACxB,IAAI,gBAAgB,KAAA,GAAW;IAC7B,MAAM,IAAI,IAAI;IACd,OAAO,IAAI,MAAM,WAAW;IAC5B;GACF;GACA,MAAM,QAAQ,KAAK,IAAI;GACvB,IAAI,UAAU,KAAA,KAAa,MAAM,WAAW,GAAG,GAAG;IAChD,OAAO,IAAI,MAAM,EAAE;IACnB;GACF;GACA,MAAM,IAAI,IAAI;GACd,OAAO,IAAI,MAAM,KAAK;GACtB;GACA;EACF;EACA,IAAI,KAAK,WAAW,GAAG,GAAG;GACxB,MAAM,IAAI,IAAI;GACd;EACF;EACA,YAAY,KAAK,GAAG;CACtB;CACA,OAAO;EAAE;EAAO;EAAQ;CAAY;AACtC;AAEA,SAAS,UAAU,MAAiC;CAIlD,MAAM,IAAsB,EAAE,qBAAqB,KAAK;CACxD,IAAI,KAAK,QAAQ,KAAA,GAAW,EAAE,MAAM,KAAK;CACzC,IAAI,KAAK,QAAQ,KAAA,GAAW,EAAE,MAAM,KAAK;CACzC,OAAO;AACT;;;AC3WA,QAAQ,WAAW,MAAM,IAAI;CAC3B,MAAM,QAAQ,KAAK,MAAM,CAAC;CAC1B,SAAS,MAAM,QAAQ,OAAO,MAAM,CAAC;CACrC,SAAS,MAAM,QAAQ,OAAO,MAAM,CAAC;AACvC,CAAC"}
@@ -1,3 +1,10 @@
1
+ //#region src/brand.ts
2
+ const brandPattern = /^[a-z]{3}$/;
3
+ /** Validates a three-character lowercase brand. Throws on invalid input. */
4
+ function validateBrand(brand) {
5
+ if (!brandPattern.test(brand)) throw new Error("invalid brand, expected three lowercase a-z characters");
6
+ }
7
+ //#endregion
1
8
  //#region src/base32.ts
2
9
  const alphabet = "0123456789abcdefghjkmnpqrstvwxyz";
3
10
  const valueToCharCode = new Uint8Array(32);
@@ -37,13 +44,50 @@ function decodeBase32(str) {
37
44
  return result;
38
45
  }
39
46
  //#endregion
47
+ //#region src/wire/envelope.ts
48
+ /** Encodes a 16-byte payload as lowercase Crockford base32 (26 chars). */
49
+ function encodePayload(bytes) {
50
+ return encodeBase32(bytes);
51
+ }
52
+ /** Decodes a 26-char base32 payload suffix to 16 bytes. Trust-the-type. */
53
+ function decodePayload(base32) {
54
+ return decodeBase32(base32);
55
+ }
56
+ /** Composes a canonical wire ID from a prefix and 16-byte payload. */
57
+ function toWireId(prefix, payload) {
58
+ return prefix + encodePayload(payload);
59
+ }
60
+ /** Decodes the full 16-byte payload from a trusted wire ID. */
61
+ function payloadBytesFromId(prefix, id) {
62
+ return decodePayload(id.slice(prefix.length));
63
+ }
64
+ const payloadBase32Length = Math.ceil(128 / 5);
65
+ const base32CharClass = "[0-9a-hjkmnp-tv-z]";
66
+ const timestampBase32Length = Math.ceil(48 / 5);
67
+ /** Write the timestamp in big-endian; encoded via mod-256 to avoid 32-bit bitwise coercion. */
68
+ function writeTimestamp(ms, buffer) {
69
+ if (Number.isNaN(ms)) throw new Error("timestamp is not a number");
70
+ if (ms < 0) throw new Error("timestamp is negative");
71
+ if (ms >= 2 ** 48) throw new Error("timestamp exceeds 48-bit range");
72
+ for (let i = 5; i >= 0; i--) {
73
+ buffer[i] = ms % 256;
74
+ ms = Math.floor(ms / 256);
75
+ }
76
+ }
77
+ /** Decode the first `timestampByteLength` bytes of a buffer as a big-endian unsigned millisecond timestamp. */
78
+ function readTimestampMs(buffer) {
79
+ let ms = 0;
80
+ for (let i = 0; i < 6; i++) ms = ms * 256 + buffer[i];
81
+ return ms;
82
+ }
83
+ /** Decodes ms from the first 10 base32 chars of a payload suffix (partial decode). */
84
+ function readTimestampMsFromBase32Suffix(base32Suffix) {
85
+ return readTimestampMs(decodeBase32(base32Suffix.slice(0, timestampBase32Length)));
86
+ }
87
+ //#endregion
40
88
  //#region src/registry.ts
41
- const brandPattern = /^[a-z]{3}$/;
42
89
  const registeredBrands = /* @__PURE__ */ new Set();
43
90
  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
91
  function registerBrand(brand, allowDuplicateBrand) {
48
92
  if (typeof process === "undefined" || process.env.NODE_ENV === "production" || allowDuplicateBrand) return;
49
93
  if (registeredBrands.has(brand)) {
@@ -53,8 +97,8 @@ function registerBrand(brand, allowDuplicateBrand) {
53
97
  }
54
98
  } else registeredBrands.add(brand);
55
99
  }
56
- const payloadBase32Length = Math.ceil(128 / 5);
57
- const base32CharClass = "[0-9a-hjkmnp-tv-z]";
100
+ //#endregion
101
+ //#region src/wire/parse.ts
58
102
  const replacePattern = /[ilo]/g;
59
103
  const aliasTestPattern = /[ilo]/;
60
104
  const replacer = (match) => match === "o" ? "0" : "1";
@@ -97,26 +141,33 @@ function errorMessage(prefix, error) {
97
141
  case "invalid_base32": return "invalid base32 payload";
98
142
  }
99
143
  }
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
144
  function standardValidate(prefix, value) {
115
145
  const result = safeParse(prefix, value);
116
146
  if (result.ok) return { value: result.id };
117
147
  return { issues: [{ message: errorMessage(prefix, result.error) }] };
118
148
  }
119
149
  //#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 };
150
+ //#region src/wire/codec-shell.ts
151
+ /** Wire-only methods shared by every codec variant for a fixed prefix. */
152
+ function wireMethods(prefix) {
153
+ return {
154
+ is: (value) => is(prefix, value),
155
+ parse: (value) => parse(prefix, value),
156
+ safeParse: (value) => safeParse(prefix, value),
157
+ toJsonSchema: (brand, example) => ({
158
+ type: "string",
159
+ pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,
160
+ description: `Branded ID for '${brand}'`,
161
+ example
162
+ }),
163
+ "~standard": {
164
+ version: 1,
165
+ vendor: "@smonn/ids",
166
+ validate: (value) => standardValidate(prefix, value)
167
+ }
168
+ };
169
+ }
170
+ //#endregion
171
+ export { writeTimestamp as a, toWireId as c, readTimestampMsFromBase32Suffix as i, validateBrand as l, registerBrand as n, payloadBase32Length as o, readTimestampMs as r, payloadBytesFromId as s, wireMethods as t };
121
172
 
122
- //# sourceMappingURL=shared-CmbAeUdM.mjs.map
173
+ //# sourceMappingURL=codec-shell-C0arqqX3.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec-shell-C0arqqX3.mjs","names":[],"sources":["../src/brand.ts","../src/base32.ts","../src/wire/envelope.ts","../src/wire/invariants.ts","../src/wire/timestamp-bytes.ts","../src/registry.ts","../src/wire/parse.ts","../src/wire/codec-shell.ts"],"sourcesContent":["const brandPattern = /^[a-z]{3}$/;\n\n/** Validates a three-character lowercase brand. Throws on invalid input. */\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","/*\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 { decodeBase32, encodeBase32 } from \"../base32.js\";\nimport type { Id, Prefix } from \"../types.js\";\n\n/** Encodes a 16-byte payload as lowercase Crockford base32 (26 chars). */\nfunction encodePayload(bytes: Uint8Array): string {\n return encodeBase32(bytes);\n}\n\n/** Decodes a 26-char base32 payload suffix to 16 bytes. Trust-the-type. */\nfunction decodePayload(base32: string): Uint8Array {\n return decodeBase32(base32);\n}\n\n/** Composes a canonical wire ID from a prefix and 16-byte payload. */\nexport function toWireId<Brand extends string>(\n prefix: Prefix<Brand>,\n payload: Uint8Array,\n): Id<Brand> {\n return (prefix + encodePayload(payload)) as Id<Brand>;\n}\n\n/** Decodes the full 16-byte payload from a trusted wire ID. */\nexport function payloadBytesFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n id: Id<Brand>,\n): Uint8Array {\n return decodePayload(id.slice(prefix.length));\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","import { decodeBase32 } from \"../base32.js\";\n\n// Timestamp byte layout: first N bytes of the plaintext payload encode a\n// big-endian Unix-ms timestamp. Shared by Timestamp and Opaque layouts.\nexport const timestampByteLength: number = 6;\n\nconst timestampBase32Length: number = Math.ceil((timestampByteLength * 8) / 5);\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 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\n/** Decodes ms from the first 10 base32 chars of a payload suffix (partial decode). */\nexport function readTimestampMsFromBase32Suffix(base32Suffix: string): number {\n return readTimestampMs(decodeBase32(base32Suffix.slice(0, timestampBase32Length)));\n}\n","const registeredBrands = new Set<string>();\nconst warnedBrands = new Set<string>();\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\";\nimport { payloadBase32Length } from \"./invariants.js\";\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\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","import type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"../types.js\";\nimport { base32CharClass, payloadBase32Length } from \"./invariants.js\";\nimport { is, parse, safeParse, standardValidate } from \"./parse.js\";\n\ntype WireMethods<Brand extends string> = {\n is: (value: unknown) => value is Id<Brand>;\n parse: (value: unknown) => Id<Brand>;\n safeParse: (value: unknown) => ParseResult<Brand>;\n toJsonSchema: (brand: Brand, example: string) => JsonSchema;\n \"~standard\": StandardSchemaProps<Brand>;\n};\n\n/** Wire-only methods shared by every codec variant for a fixed prefix. */\nexport function wireMethods<Brand extends string>(prefix: Prefix<Brand>): WireMethods<Brand> {\n const standard: StandardSchemaProps<Brand> = {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n };\n return {\n is: (value: unknown): value is Id<Brand> => is(prefix, value),\n parse: (value: unknown): Id<Brand> => parse(prefix, value),\n safeParse: (value: unknown): ParseResult<Brand> => safeParse(prefix, value),\n toJsonSchema: (brand: Brand, example: string): JsonSchema => ({\n type: \"string\",\n pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,\n description: `Branded ID for '${brand}'`,\n example,\n }),\n \"~standard\": standard,\n };\n}\n"],"mappings":"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"}