@smonn/ids 0.3.2 → 0.3.4
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/dist/cli.mjs +92 -11
- package/dist/cli.mjs.map +1 -1
- package/dist/{shared-CmbAeUdM.mjs → codec-shell-C0arqqX3.mjs} +73 -22
- package/dist/codec-shell-C0arqqX3.mjs.map +1 -0
- package/dist/id-CcoPE2EX.mjs +95 -0
- package/dist/id-CcoPE2EX.mjs.map +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{opaque-ChnxvPm5.mjs → opaque-B-ttBfHO.mjs} +64 -50
- package/dist/opaque-B-ttBfHO.mjs.map +1 -0
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +1 -1
- package/package.json +3 -1
- package/dist/id-BlQPohZp.mjs +0 -82
- package/dist/id-BlQPohZp.mjs.map +0 -1
- package/dist/opaque-ChnxvPm5.mjs.map +0 -1
- package/dist/shared-CmbAeUdM.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as createId } from "./id-
|
|
3
|
-
import { i as encodeOpaqueKey, n as importOpaqueKey, r as decodeOpaqueKey, t as createOpaqueId } from "./opaque-
|
|
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";
|
|
4
4
|
//#region src/cli.ts
|
|
5
5
|
async function run(opts) {
|
|
6
6
|
const [subcommand, ...rest] = opts.argv;
|
|
@@ -31,13 +31,31 @@ function usage() {
|
|
|
31
31
|
].join("\n");
|
|
32
32
|
}
|
|
33
33
|
function runInspect(args, opts) {
|
|
34
|
-
const { flags, values, positionals } = splitFlags(args);
|
|
34
|
+
const { flags, values, positionals, errors } = splitFlags(args);
|
|
35
|
+
const unsupported = unsupportedFlagForCommand("inspect", flags, new Set(["--opaque", "--key-format"]));
|
|
36
|
+
if (unsupported !== void 0) {
|
|
37
|
+
opts.stderr(unsupported + "\n");
|
|
38
|
+
return Promise.resolve(1);
|
|
39
|
+
}
|
|
40
|
+
if (errors[0] !== void 0) {
|
|
41
|
+
opts.stderr(errors[0] + "\n");
|
|
42
|
+
return Promise.resolve(1);
|
|
43
|
+
}
|
|
35
44
|
const [input] = positionals;
|
|
36
45
|
if (input === void 0) {
|
|
37
46
|
opts.stderr(usage());
|
|
38
47
|
return Promise.resolve(1);
|
|
39
48
|
}
|
|
49
|
+
const extra = positionals[1];
|
|
50
|
+
if (extra !== void 0) {
|
|
51
|
+
opts.stderr(`unexpected argument: ${extra}\n`);
|
|
52
|
+
return Promise.resolve(1);
|
|
53
|
+
}
|
|
40
54
|
const opaque = flags.has("--opaque");
|
|
55
|
+
if (!opaque && flags.has("--key-format")) {
|
|
56
|
+
opts.stderr("--key-format requires --opaque\n");
|
|
57
|
+
return Promise.resolve(1);
|
|
58
|
+
}
|
|
41
59
|
const brand = input.slice(0, 3).toLowerCase();
|
|
42
60
|
if (opaque) {
|
|
43
61
|
const format = parseOpaqueKeyFormat(values, opts);
|
|
@@ -143,14 +161,38 @@ function unit(n, name) {
|
|
|
143
161
|
return `${n} ${n === 1 ? name : `${name}s`}`;
|
|
144
162
|
}
|
|
145
163
|
function runGenerate(args, opts) {
|
|
146
|
-
const { flags, values, positionals } = splitFlags(args);
|
|
164
|
+
const { flags, values, positionals, errors } = splitFlags(args);
|
|
165
|
+
const unsupported = unsupportedFlagForCommand("generate", flags, new Set([
|
|
166
|
+
"--count",
|
|
167
|
+
"-c",
|
|
168
|
+
"--opaque",
|
|
169
|
+
"--key-format"
|
|
170
|
+
]));
|
|
171
|
+
if (unsupported !== void 0) {
|
|
172
|
+
opts.stderr(unsupported + "\n");
|
|
173
|
+
return Promise.resolve(1);
|
|
174
|
+
}
|
|
175
|
+
if (errors[0] !== void 0) {
|
|
176
|
+
opts.stderr(errors[0] + "\n");
|
|
177
|
+
return Promise.resolve(1);
|
|
178
|
+
}
|
|
179
|
+
const extra = positionals[1];
|
|
180
|
+
if (extra !== void 0) {
|
|
181
|
+
opts.stderr(`unexpected argument: ${extra}\n`);
|
|
182
|
+
return Promise.resolve(1);
|
|
183
|
+
}
|
|
147
184
|
const [brand] = positionals;
|
|
148
185
|
const count = parseCount(values);
|
|
149
186
|
if (typeof count === "string") {
|
|
150
187
|
opts.stderr(count + "\n");
|
|
151
188
|
return Promise.resolve(1);
|
|
152
189
|
}
|
|
153
|
-
|
|
190
|
+
const opaque = flags.has("--opaque");
|
|
191
|
+
if (!opaque && flags.has("--key-format")) {
|
|
192
|
+
opts.stderr("--key-format requires --opaque\n");
|
|
193
|
+
return Promise.resolve(1);
|
|
194
|
+
}
|
|
195
|
+
if (opaque) {
|
|
154
196
|
const format = parseOpaqueKeyFormat(values, opts);
|
|
155
197
|
if (isKeyFormatError(format)) {
|
|
156
198
|
opts.stderr(format + "\n");
|
|
@@ -188,7 +230,21 @@ async function runOpaqueGenerate(brand, count, format, opts) {
|
|
|
188
230
|
return 0;
|
|
189
231
|
}
|
|
190
232
|
function runKeygen(args, opts) {
|
|
191
|
-
const { values } = splitFlags(args);
|
|
233
|
+
const { flags, values, positionals, errors } = splitFlags(args);
|
|
234
|
+
const unsupported = unsupportedFlagForCommand("keygen", flags, new Set(["--bits", "--key-format"]));
|
|
235
|
+
if (unsupported !== void 0) {
|
|
236
|
+
opts.stderr(unsupported + "\n");
|
|
237
|
+
return Promise.resolve(1);
|
|
238
|
+
}
|
|
239
|
+
if (errors[0] !== void 0) {
|
|
240
|
+
opts.stderr(errors[0] + "\n");
|
|
241
|
+
return Promise.resolve(1);
|
|
242
|
+
}
|
|
243
|
+
const extra = positionals[0];
|
|
244
|
+
if (extra !== void 0) {
|
|
245
|
+
opts.stderr(`unexpected argument: ${extra}\n`);
|
|
246
|
+
return Promise.resolve(1);
|
|
247
|
+
}
|
|
192
248
|
const bits = parseBits(values);
|
|
193
249
|
if (typeof bits === "string") {
|
|
194
250
|
opts.stderr(bits + "\n");
|
|
@@ -267,37 +323,47 @@ function splitFlags(args) {
|
|
|
267
323
|
const flags = /* @__PURE__ */ new Set();
|
|
268
324
|
const values = /* @__PURE__ */ new Map();
|
|
269
325
|
const positionals = [];
|
|
326
|
+
const errors = [];
|
|
327
|
+
const seenFlags = /* @__PURE__ */ new Set();
|
|
270
328
|
const valueFlags = new Set([
|
|
271
329
|
"--count",
|
|
272
330
|
"-c",
|
|
273
331
|
"--bits",
|
|
274
332
|
"--key-format"
|
|
275
333
|
]);
|
|
334
|
+
const addFlag = (flag) => {
|
|
335
|
+
const canonical = canonicalFlag(flag);
|
|
336
|
+
if (seenFlags.has(canonical)) errors.push(`duplicate flag: ${canonical}`);
|
|
337
|
+
seenFlags.add(canonical);
|
|
338
|
+
flags.add(flag);
|
|
339
|
+
};
|
|
276
340
|
for (let i = 0; i < args.length; i++) {
|
|
277
341
|
const raw = args[i];
|
|
278
342
|
const { flag, inlineValue } = splitFlagToken(raw);
|
|
279
343
|
if (flag === "--opaque") {
|
|
280
|
-
|
|
344
|
+
addFlag(flag);
|
|
345
|
+
if (inlineValue !== void 0) errors.push("flag does not take a value: --opaque");
|
|
281
346
|
continue;
|
|
282
347
|
}
|
|
283
348
|
if (valueFlags.has(flag)) {
|
|
284
349
|
if (inlineValue !== void 0) {
|
|
285
|
-
|
|
350
|
+
addFlag(flag);
|
|
286
351
|
values.set(flag, inlineValue);
|
|
287
352
|
continue;
|
|
288
353
|
}
|
|
289
354
|
const value = args[i + 1];
|
|
290
355
|
if (value === void 0 || value.startsWith("-")) {
|
|
356
|
+
addFlag(flag);
|
|
291
357
|
values.set(flag, "");
|
|
292
358
|
continue;
|
|
293
359
|
}
|
|
294
|
-
|
|
360
|
+
addFlag(flag);
|
|
295
361
|
values.set(flag, value);
|
|
296
362
|
i++;
|
|
297
363
|
continue;
|
|
298
364
|
}
|
|
299
365
|
if (flag.startsWith("-")) {
|
|
300
|
-
|
|
366
|
+
addFlag(flag);
|
|
301
367
|
continue;
|
|
302
368
|
}
|
|
303
369
|
positionals.push(raw);
|
|
@@ -305,9 +371,24 @@ function splitFlags(args) {
|
|
|
305
371
|
return {
|
|
306
372
|
flags,
|
|
307
373
|
values,
|
|
308
|
-
positionals
|
|
374
|
+
positionals,
|
|
375
|
+
errors
|
|
309
376
|
};
|
|
310
377
|
}
|
|
378
|
+
function canonicalFlag(flag) {
|
|
379
|
+
if (flag === "-c") return "--count";
|
|
380
|
+
return flag;
|
|
381
|
+
}
|
|
382
|
+
const knownFlags = new Set([
|
|
383
|
+
"--opaque",
|
|
384
|
+
"--key-format",
|
|
385
|
+
"--count",
|
|
386
|
+
"-c",
|
|
387
|
+
"--bits"
|
|
388
|
+
]);
|
|
389
|
+
function unsupportedFlagForCommand(command, flags, allowed) {
|
|
390
|
+
for (const flag of flags) if (!allowed.has(flag)) return knownFlags.has(flag) ? `unsupported flag for ${command}: ${flag}` : `unsupported flag: ${flag}`;
|
|
391
|
+
}
|
|
311
392
|
function codecOpts(opts) {
|
|
312
393
|
const o = { allowDuplicateBrand: true };
|
|
313
394
|
if (opts.now !== void 0) o.now = opts.now;
|
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 {\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":"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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, errors } = splitFlags(args);\n const unsupported = unsupportedFlagForCommand(\n \"inspect\",\n flags,\n new Set([\"--opaque\", \"--key-format\"]),\n );\n if (unsupported !== undefined) {\n opts.stderr(unsupported + \"\\n\");\n return Promise.resolve(1);\n }\n if (errors[0] !== undefined) {\n opts.stderr(errors[0] + \"\\n\");\n return Promise.resolve(1);\n }\n const [input] = positionals;\n if (input === undefined) {\n opts.stderr(usage());\n return Promise.resolve(1);\n }\n const extra = positionals[1];\n if (extra !== undefined) {\n opts.stderr(`unexpected argument: ${extra}\\n`);\n return Promise.resolve(1);\n }\n const opaque = flags.has(\"--opaque\");\n if (!opaque && flags.has(\"--key-format\")) {\n opts.stderr(\"--key-format requires --opaque\\n\");\n return Promise.resolve(1);\n }\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, errors } = splitFlags(args);\n const unsupported = unsupportedFlagForCommand(\n \"generate\",\n flags,\n new Set([\"--count\", \"-c\", \"--opaque\", \"--key-format\"]),\n );\n if (unsupported !== undefined) {\n opts.stderr(unsupported + \"\\n\");\n return Promise.resolve(1);\n }\n if (errors[0] !== undefined) {\n opts.stderr(errors[0] + \"\\n\");\n return Promise.resolve(1);\n }\n const extra = positionals[1];\n if (extra !== undefined) {\n opts.stderr(`unexpected argument: ${extra}\\n`);\n return Promise.resolve(1);\n }\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 && flags.has(\"--key-format\")) {\n opts.stderr(\"--key-format requires --opaque\\n\");\n return Promise.resolve(1);\n }\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 { flags, values, positionals, errors } = splitFlags(args);\n const unsupported = unsupportedFlagForCommand(\n \"keygen\",\n flags,\n new Set([\"--bits\", \"--key-format\"]),\n );\n if (unsupported !== undefined) {\n opts.stderr(unsupported + \"\\n\");\n return Promise.resolve(1);\n }\n if (errors[0] !== undefined) {\n opts.stderr(errors[0] + \"\\n\");\n return Promise.resolve(1);\n }\n const extra = positionals[0];\n if (extra !== undefined) {\n opts.stderr(`unexpected argument: ${extra}\\n`);\n return Promise.resolve(1);\n }\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 errors: 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 errors: string[] = [];\n const seenFlags = new Set<string>();\n const valueFlags = new Set([\"--count\", \"-c\", \"--bits\", \"--key-format\"]);\n const addFlag = (flag: string) => {\n const canonical = canonicalFlag(flag);\n if (seenFlags.has(canonical)) errors.push(`duplicate flag: ${canonical}`);\n seenFlags.add(canonical);\n flags.add(flag);\n };\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 addFlag(flag);\n if (inlineValue !== undefined) errors.push(\"flag does not take a value: --opaque\");\n continue;\n }\n if (valueFlags.has(flag)) {\n if (inlineValue !== undefined) {\n addFlag(flag);\n values.set(flag, inlineValue);\n continue;\n }\n const value = args[i + 1];\n if (value === undefined || value.startsWith(\"-\")) {\n addFlag(flag);\n values.set(flag, \"\");\n continue;\n }\n addFlag(flag);\n values.set(flag, value);\n i++;\n continue;\n }\n if (flag.startsWith(\"-\")) {\n addFlag(flag);\n continue;\n }\n positionals.push(raw);\n }\n return { flags, values, positionals, errors };\n}\n\nfunction canonicalFlag(flag: string): string {\n if (flag === \"-c\") return \"--count\";\n return flag;\n}\n\nconst knownFlags = new Set([\"--opaque\", \"--key-format\", \"--count\", \"-c\", \"--bits\"]);\n\nfunction unsupportedFlagForCommand(\n command: string,\n flags: Set<string>,\n allowed: Set<string>,\n): string | undefined {\n for (const flag of flags) {\n if (!allowed.has(flag)) {\n return knownFlags.has(flag)\n ? `unsupported flag for ${command}: ${flag}`\n : `unsupported flag: ${flag}`;\n }\n }\n return undefined;\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":"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"}
|
|
@@ -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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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=
|
|
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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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { a as writeTimestamp, c as toWireId, i as readTimestampMsFromBase32Suffix, l as validateBrand, n as registerBrand, t as wireMethods } from "./codec-shell-C0arqqX3.mjs";
|
|
2
|
+
//#region src/layouts/timestamp.ts
|
|
3
|
+
const randomByteLength = 10;
|
|
4
|
+
/** Writes a 16-byte timestamp-layout payload into codec-owned scratch. */
|
|
5
|
+
function buildPayload(ms, rng, buffer, randomView) {
|
|
6
|
+
writeTimestamp(ms, buffer);
|
|
7
|
+
rng(randomView);
|
|
8
|
+
}
|
|
9
|
+
/** Writes sentinel min/max random bytes into codec-owned scratch. */
|
|
10
|
+
function buildSentinelPayload(ms, fill, buffer, randomView) {
|
|
11
|
+
writeTimestamp(ms, buffer);
|
|
12
|
+
randomView.fill(fill);
|
|
13
|
+
}
|
|
14
|
+
/** Decodes the creation timestamp from a trusted wire ID. */
|
|
15
|
+
function extractTimestampFromId(prefix, id) {
|
|
16
|
+
return new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length)));
|
|
17
|
+
}
|
|
18
|
+
/** Layout ops binder for the Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */
|
|
19
|
+
function createTimestampLayoutOps(prefix, rng) {
|
|
20
|
+
const buffer = new Uint8Array(16);
|
|
21
|
+
const randomView = new Uint8Array(buffer.buffer, 6, randomByteLength);
|
|
22
|
+
return {
|
|
23
|
+
generateAt: (ms) => {
|
|
24
|
+
buildPayload(ms, rng, buffer, randomView);
|
|
25
|
+
return toWireId(prefix, buffer);
|
|
26
|
+
},
|
|
27
|
+
extractTimestamp: (id) => extractTimestampFromId(prefix, id),
|
|
28
|
+
minIdForTime: (ms) => {
|
|
29
|
+
buildSentinelPayload(ms, 0, buffer, randomView);
|
|
30
|
+
return toWireId(prefix, buffer);
|
|
31
|
+
},
|
|
32
|
+
maxIdForTime: (ms) => {
|
|
33
|
+
buildSentinelPayload(ms, 255, buffer, randomView);
|
|
34
|
+
return toWireId(prefix, buffer);
|
|
35
|
+
},
|
|
36
|
+
exampleWireId: (ms) => {
|
|
37
|
+
buildPayload(ms, rng, buffer, randomView);
|
|
38
|
+
return toWireId(prefix, buffer);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/id.ts
|
|
44
|
+
const hexCharCodeToNibble = new Uint8Array(128);
|
|
45
|
+
for (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;
|
|
46
|
+
for (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;
|
|
47
|
+
const defaultOptions = {
|
|
48
|
+
now: Date.now,
|
|
49
|
+
rng: (target) => {
|
|
50
|
+
const s = crypto.randomUUID();
|
|
51
|
+
target[0] = hexCharCodeToNibble[s.charCodeAt(0)] << 4 | hexCharCodeToNibble[s.charCodeAt(1)];
|
|
52
|
+
target[1] = hexCharCodeToNibble[s.charCodeAt(2)] << 4 | hexCharCodeToNibble[s.charCodeAt(3)];
|
|
53
|
+
target[2] = hexCharCodeToNibble[s.charCodeAt(4)] << 4 | hexCharCodeToNibble[s.charCodeAt(5)];
|
|
54
|
+
target[3] = hexCharCodeToNibble[s.charCodeAt(6)] << 4 | hexCharCodeToNibble[s.charCodeAt(7)];
|
|
55
|
+
target[4] = hexCharCodeToNibble[s.charCodeAt(9)] << 4 | hexCharCodeToNibble[s.charCodeAt(10)];
|
|
56
|
+
target[5] = hexCharCodeToNibble[s.charCodeAt(11)] << 4 | hexCharCodeToNibble[s.charCodeAt(12)];
|
|
57
|
+
target[6] = hexCharCodeToNibble[s.charCodeAt(24)] << 4 | hexCharCodeToNibble[s.charCodeAt(25)];
|
|
58
|
+
target[7] = hexCharCodeToNibble[s.charCodeAt(26)] << 4 | hexCharCodeToNibble[s.charCodeAt(27)];
|
|
59
|
+
target[8] = hexCharCodeToNibble[s.charCodeAt(28)] << 4 | hexCharCodeToNibble[s.charCodeAt(29)];
|
|
60
|
+
target[9] = hexCharCodeToNibble[s.charCodeAt(30)] << 4 | hexCharCodeToNibble[s.charCodeAt(31)];
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Creates a codec for `brand` (three lowercase a–z characters).
|
|
65
|
+
*
|
|
66
|
+
* @param brand - Entity type brand validated once at construction.
|
|
67
|
+
* @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.
|
|
68
|
+
*/
|
|
69
|
+
function createId(brand, opts = {}) {
|
|
70
|
+
validateBrand(brand);
|
|
71
|
+
registerBrand(brand, opts.allowDuplicateBrand);
|
|
72
|
+
const options = {
|
|
73
|
+
...defaultOptions,
|
|
74
|
+
...opts
|
|
75
|
+
};
|
|
76
|
+
const prefix = `${brand}_`;
|
|
77
|
+
const wire = wireMethods(prefix);
|
|
78
|
+
const layout = createTimestampLayoutOps(prefix, options.rng);
|
|
79
|
+
return {
|
|
80
|
+
generate: () => layout.generateAt(options.now()),
|
|
81
|
+
generateAt: (date) => layout.generateAt(date.getTime()),
|
|
82
|
+
is: wire.is,
|
|
83
|
+
parse: wire.parse,
|
|
84
|
+
safeParse: wire.safeParse,
|
|
85
|
+
extractTimestamp: layout.extractTimestamp,
|
|
86
|
+
minIdForTime: (date) => layout.minIdForTime(date.getTime()),
|
|
87
|
+
maxIdForTime: (date) => layout.maxIdForTime(date.getTime()),
|
|
88
|
+
toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(options.now())),
|
|
89
|
+
"~standard": wire["~standard"]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { createId as t };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=id-CcoPE2EX.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id-CcoPE2EX.mjs","names":[],"sources":["../src/layouts/timestamp.ts","../src/id.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { toWireId } from \"../wire/envelope.js\";\nimport { payloadByteLength } from \"../wire/invariants.js\";\nimport {\n readTimestampMsFromBase32Suffix,\n timestampByteLength,\n writeTimestamp,\n} from \"../wire/timestamp-bytes.js\";\n\nconst randomByteLength: number = payloadByteLength - timestampByteLength;\n\n/** Writes a 16-byte timestamp-layout payload into codec-owned scratch. */\nfunction buildPayload(\n ms: number,\n rng: (target: Uint8Array) => void,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n rng(randomView);\n}\n\n/** Writes sentinel min/max random bytes into codec-owned scratch. */\nfunction buildSentinelPayload(\n ms: number,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n randomView.fill(fill);\n}\n\n/** Decodes the creation timestamp from a trusted wire ID. */\nfunction extractTimestampFromId<Brand extends string>(prefix: Prefix<Brand>, id: Id<Brand>): Date {\n return new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length)));\n}\n\n/** Layout ops binder for the Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */\nexport function createTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n rng: (target: Uint8Array) => void,\n) {\n // Per-codec scratch buffer. Shared across generateAt(), minIdForTime(),\n // maxIdForTime(), and exampleWireId() — all are synchronous and overwrite both\n // the timestamp and random slices before encoding, so successive callers see\n // their own freshly-written bytes. toWireId reads the buffer and returns an\n // 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 generateAt: (ms: number): Id<Brand> => {\n buildPayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n extractTimestamp: (id: Id<Brand>): Date => extractTimestampFromId(prefix, id),\n minIdForTime: (ms: number): Id<Brand> => {\n buildSentinelPayload(ms, 0x00, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n maxIdForTime: (ms: number): Id<Brand> => {\n buildSentinelPayload(ms, 0xff, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n exampleWireId: (ms: number): Id<Brand> => {\n buildPayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n };\n}\n","import { validateBrand } from \"./brand.js\";\nimport { createTimestampLayoutOps } from \"./layouts/timestamp.js\";\nimport { registerBrand } from \"./registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\n\n/**\n * Configuration options for a codec instance.\n */\nexport type Options = {\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to a `crypto.randomUUID` fast path. */\n rng: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * A brand-scoped codec for generating and validating public-facing IDs.\n *\n * Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a\n * 16-byte payload (6-byte ms timestamp + 10 random bytes). IDs sort by creation\n * time in ascending order.\n *\n * For encrypted IDs, use `createOpaqueId` from `@smonn/ids/opaque`.\n */\nexport type Codec<Brand extends string> = {\n /** Produces a new canonical ID using the codec's `now` and `rng`. */\n generate(): Id<Brand>;\n /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */\n generateAt(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /** Tight lower bound for any ID generated at `date` (random portion `0x00`). Throws on invalid dates. */\n minIdForTime(date: Date): Id<Brand>;\n /** Tight upper bound for any ID generated at `date` (random portion `0xff`). Throws on invalid dates. */\n maxIdForTime(date: Date): Id<Brand>;\n /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\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\n/**\n * Creates a codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.\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 const wire = wireMethods(prefix);\n const layout = createTimestampLayoutOps(prefix, options.rng);\n\n return {\n generate: () => layout.generateAt(options.now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n minIdForTime: (date: Date) => layout.minIdForTime(date.getTime()),\n maxIdForTime: (date: Date) => layout.maxIdForTime(date.getTime()),\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(options.now())),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":"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"}
|
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":";;;;;AASA;KAAY,OAAA;+EAEV,GAAA;EAEA,GAAA,GAAM,MAAA,EAAQ,UAAA;EAEd,mBAAA;AAAA;;AAAA;AAYF;;;;;;;KAAY,KAAA;uEAEV,QAAA,IAAY,EAAA,CAAG,KAAA;EAEf,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;EAK3B,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;;;EAIhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;EAI1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;EAIvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;EAEjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;iBAiD5B,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-CcoPE2EX.mjs";
|
|
2
2
|
export { createId };
|
|
@@ -1,4 +1,56 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as writeTimestamp, c as toWireId, l as validateBrand, n as registerBrand, o as payloadBase32Length, r as readTimestampMs, s as payloadBytesFromId, t as wireMethods } from "./codec-shell-C0arqqX3.mjs";
|
|
2
|
+
//#region src/layouts/opaque.ts
|
|
3
|
+
const zeroIv = new Uint8Array(16);
|
|
4
|
+
const pkcsPad = 16;
|
|
5
|
+
function buildPlaintext(ms, rng) {
|
|
6
|
+
const plaintext = new Uint8Array(16);
|
|
7
|
+
writeTimestamp(ms, plaintext);
|
|
8
|
+
rng(plaintext.subarray(6, 16));
|
|
9
|
+
return plaintext;
|
|
10
|
+
}
|
|
11
|
+
async function encryptPayload(key, plaintext) {
|
|
12
|
+
return new Uint8Array(await crypto.subtle.encrypt({
|
|
13
|
+
name: "AES-CBC",
|
|
14
|
+
iv: zeroIv
|
|
15
|
+
}, key, plaintext)).subarray(0, 16);
|
|
16
|
+
}
|
|
17
|
+
async function decryptPayload(key, c1) {
|
|
18
|
+
const c2Input = new Uint8Array(16);
|
|
19
|
+
for (let i = 0; i < 16; i++) c2Input[i] = pkcsPad ^ c1[i];
|
|
20
|
+
const c2Encrypted = new Uint8Array(await crypto.subtle.encrypt({
|
|
21
|
+
name: "AES-CBC",
|
|
22
|
+
iv: zeroIv
|
|
23
|
+
}, key, c2Input));
|
|
24
|
+
const ciphertext = new Uint8Array(32);
|
|
25
|
+
ciphertext.set(c1, 0);
|
|
26
|
+
ciphertext.set(c2Encrypted.subarray(0, 16), 16);
|
|
27
|
+
return new Uint8Array(await crypto.subtle.decrypt({
|
|
28
|
+
name: "AES-CBC",
|
|
29
|
+
iv: zeroIv
|
|
30
|
+
}, key, ciphertext));
|
|
31
|
+
}
|
|
32
|
+
async function extractTimestampFromId(prefix, key, id) {
|
|
33
|
+
const plaintext = await decryptPayload(key, payloadBytesFromId(prefix, id));
|
|
34
|
+
return new Date(readTimestampMs(plaintext));
|
|
35
|
+
}
|
|
36
|
+
/** Produces a canonical encrypted wire ID. Per-call plaintext/ciphertext buffers —
|
|
37
|
+
* subtle dominates this path; reuse would be safe but not worth pinning to spec detail. */
|
|
38
|
+
async function generateWireId(prefix, key, rng, ms) {
|
|
39
|
+
return toWireId(prefix, await encryptPayload(key, buildPlaintext(ms, rng)));
|
|
40
|
+
}
|
|
41
|
+
/** Structural placeholder for JSON Schema (encrypt is async). */
|
|
42
|
+
function schemaExample(prefix) {
|
|
43
|
+
return prefix + "0".repeat(payloadBase32Length);
|
|
44
|
+
}
|
|
45
|
+
/** Layout ops binder for the Opaque variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */
|
|
46
|
+
function createOpaqueLayoutOps(prefix, key, rng) {
|
|
47
|
+
return {
|
|
48
|
+
generateAt: (ms) => generateWireId(prefix, key, rng, ms),
|
|
49
|
+
extractTimestamp: (id) => extractTimestampFromId(prefix, key, id),
|
|
50
|
+
exampleWireId: () => schemaExample(prefix)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
2
54
|
//#region src/bytes.ts
|
|
3
55
|
const hexDigits = "0123456789abcdef";
|
|
4
56
|
const invalidNibble = 255;
|
|
@@ -91,8 +143,6 @@ function assertValidAesKeyByteLength(byteLength) {
|
|
|
91
143
|
}
|
|
92
144
|
//#endregion
|
|
93
145
|
//#region src/opaque.ts
|
|
94
|
-
const zeroIv = new Uint8Array(16);
|
|
95
|
-
const pkcsPad = 16;
|
|
96
146
|
function defaultRng(target) {
|
|
97
147
|
crypto.getRandomValues(target);
|
|
98
148
|
}
|
|
@@ -117,56 +167,20 @@ function createOpaqueId(brand, opts) {
|
|
|
117
167
|
const now = opts.now ?? Date.now;
|
|
118
168
|
const rng = opts.rng ?? defaultRng;
|
|
119
169
|
const prefix = `${brand}_`;
|
|
170
|
+
const wire = wireMethods(prefix);
|
|
171
|
+
const layout = createOpaqueLayoutOps(prefix, key, rng);
|
|
120
172
|
return {
|
|
121
|
-
generate: () =>
|
|
122
|
-
generateAt: (date) =>
|
|
123
|
-
is:
|
|
124
|
-
parse:
|
|
125
|
-
safeParse:
|
|
126
|
-
extractTimestamp:
|
|
127
|
-
toJsonSchema: () => toJsonSchema(brand,
|
|
128
|
-
"~standard":
|
|
129
|
-
version: 1,
|
|
130
|
-
vendor: "@smonn/ids",
|
|
131
|
-
validate: (value) => standardValidate(prefix, value)
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
async function generate(prefix, key, rng, ms) {
|
|
136
|
-
const plaintext = new Uint8Array(16);
|
|
137
|
-
writeTimestamp(ms, plaintext);
|
|
138
|
-
rng(plaintext.subarray(6, 16));
|
|
139
|
-
return prefix + encodeBase32(new Uint8Array(await crypto.subtle.encrypt({
|
|
140
|
-
name: "AES-CBC",
|
|
141
|
-
iv: zeroIv
|
|
142
|
-
}, key, plaintext)).subarray(0, 16));
|
|
143
|
-
}
|
|
144
|
-
async function extractTimestamp(prefix, key, id) {
|
|
145
|
-
const c1 = decodeBase32(id.slice(prefix.length));
|
|
146
|
-
const c2Input = new Uint8Array(16);
|
|
147
|
-
for (let i = 0; i < 16; i++) c2Input[i] = pkcsPad ^ c1[i];
|
|
148
|
-
const c2Encrypted = new Uint8Array(await crypto.subtle.encrypt({
|
|
149
|
-
name: "AES-CBC",
|
|
150
|
-
iv: zeroIv
|
|
151
|
-
}, key, c2Input));
|
|
152
|
-
const ciphertext = new Uint8Array(32);
|
|
153
|
-
ciphertext.set(c1, 0);
|
|
154
|
-
ciphertext.set(c2Encrypted.subarray(0, 16), 16);
|
|
155
|
-
const plaintext = new Uint8Array(await crypto.subtle.decrypt({
|
|
156
|
-
name: "AES-CBC",
|
|
157
|
-
iv: zeroIv
|
|
158
|
-
}, key, ciphertext));
|
|
159
|
-
return new Date(readTimestampMs(plaintext));
|
|
160
|
-
}
|
|
161
|
-
function toJsonSchema(brand, prefix) {
|
|
162
|
-
return {
|
|
163
|
-
type: "string",
|
|
164
|
-
pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,
|
|
165
|
-
description: `Branded ID for '${brand}'`,
|
|
166
|
-
example: prefix + "0".repeat(payloadBase32Length)
|
|
173
|
+
generate: () => layout.generateAt(now()),
|
|
174
|
+
generateAt: (date) => layout.generateAt(date.getTime()),
|
|
175
|
+
is: wire.is,
|
|
176
|
+
parse: wire.parse,
|
|
177
|
+
safeParse: wire.safeParse,
|
|
178
|
+
extractTimestamp: layout.extractTimestamp,
|
|
179
|
+
toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),
|
|
180
|
+
"~standard": wire["~standard"]
|
|
167
181
|
};
|
|
168
182
|
}
|
|
169
183
|
//#endregion
|
|
170
184
|
export { encodeOpaqueKey as i, importOpaqueKey as n, decodeOpaqueKey as r, createOpaqueId as t };
|
|
171
185
|
|
|
172
|
-
//# sourceMappingURL=opaque-
|
|
186
|
+
//# sourceMappingURL=opaque-B-ttBfHO.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opaque-B-ttBfHO.mjs","names":[],"sources":["../src/layouts/opaque.ts","../src/bytes.ts","../src/opaque-key.ts","../src/opaque.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { payloadBytesFromId, toWireId } from \"../wire/envelope.js\";\nimport { payloadBase32Length, payloadByteLength } from \"../wire/invariants.js\";\nimport { readTimestampMs, timestampByteLength, writeTimestamp } from \"../wire/timestamp-bytes.js\";\n\nconst zeroIv = new Uint8Array(payloadByteLength);\nconst pkcsPad = 0x10;\n\nfunction buildPlaintext(ms: number, rng: (target: Uint8Array) => void): Uint8Array {\n const plaintext = new Uint8Array(payloadByteLength);\n writeTimestamp(ms, plaintext);\n rng(plaintext.subarray(timestampByteLength, payloadByteLength));\n return plaintext;\n}\n\nasync function encryptPayload(key: CryptoKey, plaintext: Uint8Array): Promise<Uint8Array> {\n const encrypted = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n plaintext as Uint8Array<ArrayBuffer>,\n ),\n );\n return encrypted.subarray(0, payloadByteLength);\n}\n\n// AES-CBC strip-and-reconstruct decrypt (ADR-0004). The wire carries only C1\n// (16 bytes); C2 = AES_K(P2 XOR C1) where P2 is the PKCS#7 pad block (0x10×16).\n// Recompute C2 via CBC encrypt of (P2 XOR C1) with IV=0, then decrypt C1‖C2.\nasync function decryptPayload(key: CryptoKey, c1: Uint8Array): Promise<Uint8Array> {\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(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n c2Input as Uint8Array<ArrayBuffer>,\n ),\n );\n const ciphertext = new Uint8Array(payloadByteLength * 2);\n ciphertext.set(c1, 0);\n ciphertext.set(c2Encrypted.subarray(0, payloadByteLength), payloadByteLength);\n return new Uint8Array(\n await crypto.subtle.decrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n ciphertext as Uint8Array<ArrayBuffer>,\n ),\n );\n}\n\nasync function extractTimestampFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n id: Id<Brand>,\n): Promise<Date> {\n const plaintext = await decryptPayload(key, payloadBytesFromId(prefix, id));\n return new Date(readTimestampMs(plaintext));\n}\n\n/** Produces a canonical encrypted wire ID. Per-call plaintext/ciphertext buffers —\n * subtle dominates this path; reuse would be safe but not worth pinning to spec detail. */\nasync function generateWireId<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 = buildPlaintext(ms, rng);\n const encrypted = await encryptPayload(key, plaintext);\n return toWireId(prefix, encrypted);\n}\n\n/** Structural placeholder for JSON Schema (encrypt is async). */\nfunction schemaExample<Brand extends string>(prefix: Prefix<Brand>): string {\n return prefix + \"0\".repeat(payloadBase32Length);\n}\n\n/** Layout ops binder for the Opaque variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */\nexport function createOpaqueLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n rng: (target: Uint8Array) => void,\n) {\n return {\n generateAt: (ms: number): Promise<Id<Brand>> => generateWireId(prefix, key, rng, ms),\n extractTimestamp: (id: Id<Brand>): Promise<Date> => extractTimestampFromId(prefix, key, id),\n exampleWireId: (): Id<Brand> => schemaExample(prefix) as Id<Brand>,\n };\n}\n","const hexDigits = \"0123456789abcdef\";\n\nconst invalidNibble = 0xff;\nconst hexCharCodeToNibble = new Uint8Array(128).fill(invalidNibble);\nfor (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;\nfor (let i = 0; i < 6; i++) {\n hexCharCodeToNibble[97 + i] = 10 + i;\n hexCharCodeToNibble[65 + i] = 10 + i;\n}\n\n/** Lowercase hex encoding of raw bytes. */\nexport function encodeHex(bytes: Uint8Array): string {\n // oxlint-disable-next-line no-new-array\n const codes = new Array<number>(bytes.length * 2);\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i]!;\n codes[i * 2] = hexDigits.charCodeAt(b >>> 4);\n codes[i * 2 + 1] = hexDigits.charCodeAt(b & 0x0f);\n }\n return String.fromCharCode(...codes);\n}\n\n/** Decodes a hex string to raw bytes. Throws on non-hex input. */\nexport function decodeHex(encoded: string): Uint8Array {\n if (encoded.length % 2 !== 0) throw new Error(\"invalid hex\");\n const out = new Uint8Array(encoded.length / 2);\n for (let i = 0; i < out.length; i++) {\n const hiCode = encoded.charCodeAt(i * 2);\n const loCode = encoded.charCodeAt(i * 2 + 1);\n if (hiCode >= hexCharCodeToNibble.length || loCode >= hexCharCodeToNibble.length) {\n throw new Error(\"invalid hex\");\n }\n const hi = hexCharCodeToNibble[hiCode]!;\n const lo = hexCharCodeToNibble[loCode]!;\n if (hi === invalidNibble || lo === invalidNibble) {\n throw new Error(\"invalid hex\");\n }\n out[i] = (hi << 4) | lo;\n }\n return out;\n}\n\n/** Base64url encoding without padding. */\nexport function encodeBase64Url(bytes: Uint8Array): string {\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** Decodes a base64url string to raw bytes. Throws on invalid input. */\nexport function decodeBase64Url(encoded: string): Uint8Array {\n const base64 = encoded.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const pad = (4 - (base64.length % 4)) % 4;\n const binary = atob(base64 + \"=\".repeat(pad));\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n}\n","import { decodeBase64Url, decodeHex, encodeBase64Url, encodeHex } from \"./bytes.js\";\n\n/** Wire encoding for opaque AES key material (not Crockford base32). */\nexport type OpaqueKeyFormat = \"hex\" | \"base64url\";\n\nconst validAesKeyByteLengths = new Set([16, 24, 32]);\n\n/**\n * Encodes raw AES key bytes for storage in env vars or secret managers.\n *\n * @param bytes - 16, 24, or 32 raw key bytes (AES-128/192/256).\n * @param format - `hex` (lowercase) or `base64url`.\n */\nexport function encodeOpaqueKey(bytes: Uint8Array, format: OpaqueKeyFormat): string {\n assertValidAesKeyByteLength(bytes.length);\n if (format === \"hex\") return encodeHex(bytes);\n return encodeBase64Url(bytes);\n}\n\n/**\n * Decodes key material emitted by `encodeOpaqueKey` (or `ids keygen`) back to raw bytes.\n *\n * @param encoded - Hex or base64url string.\n * @param format - Must match how the string was encoded.\n */\nexport function decodeOpaqueKey(encoded: string, format: OpaqueKeyFormat): Uint8Array {\n let bytes: Uint8Array;\n if (format === \"hex\") {\n if (encoded.length === 0 || encoded.length % 2 !== 0) {\n throw new Error(\"invalid hex key: length must be a positive even number of characters\");\n }\n if (!/^[0-9a-fA-F]+$/.test(encoded)) {\n throw new Error(\"invalid hex key: expected [0-9a-fA-F] only\");\n }\n bytes = decodeHex(encoded);\n } else {\n try {\n bytes = decodeBase64Url(encoded);\n } catch {\n throw new Error(\"invalid base64url key\");\n }\n }\n assertValidAesKeyByteLength(bytes.length);\n return bytes;\n}\n\nfunction assertValidAesKeyByteLength(byteLength: number): void {\n if (!validAesKeyByteLengths.has(byteLength)) {\n throw new Error(`invalid AES key length: expected 16, 24, or 32 bytes, got ${byteLength}`);\n }\n}\n","import { validateBrand } from \"./brand.js\";\nimport { createOpaqueLayoutOps } from \"./layouts/opaque.js\";\nimport { registerBrand } from \"./registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\n\nexport { decodeOpaqueKey, encodeOpaqueKey, type OpaqueKeyFormat } from \"./opaque-key.js\";\n\n/**\n * Configuration options for an Opaque codec instance.\n */\nexport type OpaqueOptions = {\n /** AES-CBC key used for encryption and decryption. */\n key: CryptoKey;\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */\n rng: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * A brand-scoped codec for generating and validating encrypted (opaque) IDs.\n *\n * Same wire shape as the Timestamp codec (`{brand}_` + 26 base32 chars) but the\n * payload is AES-CBC encrypted. `generate`, `generateAt`, and `extractTimestamp`\n * are async; parsing methods are sync. No `minIdForTime` / `maxIdForTime` —\n * encrypted payloads do not sort by creation time.\n */\nexport type OpaqueCodec<Brand extends string> = {\n /** Produces a new canonical encrypted ID using the codec's `now` and `rng`. */\n generate(): Promise<Id<Brand>>;\n /** Produces a new canonical encrypted ID with timestamp bytes from `date`. Throws on invalid dates. */\n generateAt(date: Date): Promise<Id<Brand>>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decrypts and decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n */\n extractTimestamp(id: Id<Brand>): Promise<Date>;\n /** JSON Schema for the canonical wire form (`example` is a structural placeholder). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\nfunction defaultRng(target: Uint8Array): void {\n crypto.getRandomValues(target as Uint8Array<ArrayBuffer>);\n}\n\n/**\n * Imports a raw AES key for use with the Opaque codec.\n *\n * @param bytes - Raw key bytes (16, 24, or 32 bytes for AES-128/192/256).\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\n/**\n * Creates an Opaque codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Required `key` plus optional `now`, `rng`, and `allowDuplicateBrand` overrides.\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 const wire = wireMethods(prefix);\n const layout = createOpaqueLayoutOps(prefix, key, rng);\n\n return {\n generate: () => layout.generateAt(now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":"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"}
|
package/dist/opaque.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/opaque-key.ts","../src/opaque.ts"],"mappings":";;;;KAGY,eAAA;;AAAZ;;;;AAAY;iBAUI,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;;;;iBAY3C,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;AAtB3E;;
|
|
1
|
+
{"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/opaque-key.ts","../src/opaque.ts"],"mappings":";;;;KAGY,eAAA;;AAAZ;;;;AAAY;iBAUI,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;;;;iBAY3C,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;AAtB3E;;KCQY,aAAA;EDRA,sDCUV,GAAA,EAAK,SAAA,EDAP;ECEE,GAAA;EAEA,GAAA,GAAM,MAAA,EAAQ,UAAA;EAEd,mBAAA;AAAA;;;ADNyD;AAY3D;;;;;KCKY,WAAA;iFAEV,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA,IDPkD;ECSzE,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;;;;AAvBrC;EA4BE,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;;;EAIhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;EAI1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;AA5BvC;EAgCA,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA,CAAQ,IAAA,GArB/B;EAuBV,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;iBAY5B,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;;;;;;;iBAa5C,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
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as encodeOpaqueKey, n as importOpaqueKey, r as decodeOpaqueKey, t as createOpaqueId } from "./opaque-
|
|
1
|
+
import { i as encodeOpaqueKey, n as importOpaqueKey, r as decodeOpaqueKey, t as createOpaqueId } from "./opaque-B-ttBfHO.mjs";
|
|
2
2
|
export { createOpaqueId, decodeOpaqueKey, encodeOpaqueKey, importOpaqueKey };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smonn/ids",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Simon Ingeson (https://github.com/smonn)",
|
|
6
6
|
"repository": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"@changesets/cli": "2.31.0",
|
|
24
24
|
"@types/node": "25.9.1",
|
|
25
25
|
"@vitest/coverage-v8": "4.1.8",
|
|
26
|
+
"dependency-cruiser": "16.10.4",
|
|
26
27
|
"knip": "6.15.0",
|
|
27
28
|
"mitata": "1.0.34",
|
|
28
29
|
"oxfmt": "0.53.0",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"test": "vitest run",
|
|
45
46
|
"test:watch": "vitest",
|
|
46
47
|
"test:coverage": "vitest run --coverage",
|
|
48
|
+
"depcruise": "depcruise --config .dependency-cruiser.cjs \"src/**/*.ts\" \"bin/**/*.ts\"",
|
|
47
49
|
"knip": "knip",
|
|
48
50
|
"bench:build": "tsdown -c tsdown.bench.config.ts",
|
|
49
51
|
"bench": "pnpm -s bench:build 1>&2 && node bench/dist/index.mjs",
|
package/dist/id-BlQPohZp.mjs
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
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
|
-
/**
|
|
25
|
-
* Creates a codec for `brand` (three lowercase a–z characters).
|
|
26
|
-
*
|
|
27
|
-
* @param brand - Entity type brand validated once at construction.
|
|
28
|
-
* @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.
|
|
29
|
-
*/
|
|
30
|
-
function createId(brand, opts = {}) {
|
|
31
|
-
validateBrand(brand);
|
|
32
|
-
registerBrand(brand, opts.allowDuplicateBrand);
|
|
33
|
-
const options = {
|
|
34
|
-
...defaultOptions,
|
|
35
|
-
...opts
|
|
36
|
-
};
|
|
37
|
-
const prefix = `${brand}_`;
|
|
38
|
-
const buffer = new Uint8Array(16);
|
|
39
|
-
const randomView = new Uint8Array(buffer.buffer, 6, randomByteLength);
|
|
40
|
-
return {
|
|
41
|
-
generate: () => generate(prefix, options, buffer, randomView),
|
|
42
|
-
generateAt: (date) => generate(prefix, options, buffer, randomView, date.getTime()),
|
|
43
|
-
is: (value) => is(prefix, value),
|
|
44
|
-
parse: (value) => parse(prefix, value),
|
|
45
|
-
safeParse: (value) => safeParse(prefix, value),
|
|
46
|
-
extractTimestamp: (id) => extractTimestamp(prefix, id),
|
|
47
|
-
minIdForTime: (date) => sentinelIdForTime(prefix, date, 0, buffer, randomView),
|
|
48
|
-
maxIdForTime: (date) => sentinelIdForTime(prefix, date, 255, buffer, randomView),
|
|
49
|
-
toJsonSchema: () => toJsonSchema(brand, prefix, options, buffer, randomView),
|
|
50
|
-
"~standard": {
|
|
51
|
-
version: 1,
|
|
52
|
-
vendor: "@smonn/ids",
|
|
53
|
-
validate: (value) => standardValidate(prefix, value)
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function toJsonSchema(brand, prefix, options, buffer, randomView) {
|
|
58
|
-
return {
|
|
59
|
-
type: "string",
|
|
60
|
-
pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,
|
|
61
|
-
description: `Branded ID for '${brand}'`,
|
|
62
|
-
example: generate(prefix, options, buffer, randomView)
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
function generate(prefix, options, buffer, randomView, ms = options.now()) {
|
|
66
|
-
writeTimestamp(ms, buffer);
|
|
67
|
-
options.rng(randomView);
|
|
68
|
-
return prefix + encodeBase32(buffer);
|
|
69
|
-
}
|
|
70
|
-
function sentinelIdForTime(prefix, date, fill, buffer, randomView) {
|
|
71
|
-
writeTimestamp(date.getTime(), buffer);
|
|
72
|
-
randomView.fill(fill);
|
|
73
|
-
return prefix + encodeBase32(buffer);
|
|
74
|
-
}
|
|
75
|
-
function extractTimestamp(prefix, id) {
|
|
76
|
-
const base32 = id.slice(prefix.length, prefix.length + timestampBase32Length);
|
|
77
|
-
return new Date(readTimestampMs(decodeBase32(base32)));
|
|
78
|
-
}
|
|
79
|
-
//#endregion
|
|
80
|
-
export { createId as t };
|
|
81
|
-
|
|
82
|
-
//# sourceMappingURL=id-BlQPohZp.mjs.map
|
package/dist/id-BlQPohZp.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"id-BlQPohZp.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\n/**\n * Configuration options for a codec instance.\n */\nexport type Options = {\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to a `crypto.randomUUID` fast path. */\n rng: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * A brand-scoped codec for generating and validating public-facing IDs.\n *\n * Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a\n * 16-byte payload (6-byte ms timestamp + 10 random bytes). IDs sort by creation\n * time in ascending order.\n *\n * For encrypted IDs, use `createOpaqueId` from `@smonn/ids/opaque`.\n */\nexport type Codec<Brand extends string> = {\n /** Produces a new canonical ID using the codec's `now` and `rng`. */\n generate(): Id<Brand>;\n /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */\n generateAt(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /** Tight lower bound for any ID generated at `date` (random portion `0x00`). Throws on invalid dates. */\n minIdForTime(date: Date): Id<Brand>;\n /** Tight upper bound for any ID generated at `date` (random portion `0xff`). Throws on invalid dates. */\n maxIdForTime(date: Date): Id<Brand>;\n /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\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\n/**\n * Creates a codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.\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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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"opaque-ChnxvPm5.mjs","names":[],"sources":["../src/bytes.ts","../src/opaque-key.ts","../src/opaque.ts"],"sourcesContent":["const hexDigits = \"0123456789abcdef\";\n\nconst invalidNibble = 0xff;\nconst hexCharCodeToNibble = new Uint8Array(128).fill(invalidNibble);\nfor (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;\nfor (let i = 0; i < 6; i++) {\n hexCharCodeToNibble[97 + i] = 10 + i;\n hexCharCodeToNibble[65 + i] = 10 + i;\n}\n\n/** Lowercase hex encoding of raw bytes. */\nexport function encodeHex(bytes: Uint8Array): string {\n // oxlint-disable-next-line no-new-array\n const codes = new Array<number>(bytes.length * 2);\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i]!;\n codes[i * 2] = hexDigits.charCodeAt(b >>> 4);\n codes[i * 2 + 1] = hexDigits.charCodeAt(b & 0x0f);\n }\n return String.fromCharCode(...codes);\n}\n\n/** Decodes a hex string to raw bytes. Throws on non-hex input. */\nexport function decodeHex(encoded: string): Uint8Array {\n if (encoded.length % 2 !== 0) throw new Error(\"invalid hex\");\n const out = new Uint8Array(encoded.length / 2);\n for (let i = 0; i < out.length; i++) {\n const hiCode = encoded.charCodeAt(i * 2);\n const loCode = encoded.charCodeAt(i * 2 + 1);\n if (hiCode >= hexCharCodeToNibble.length || loCode >= hexCharCodeToNibble.length) {\n throw new Error(\"invalid hex\");\n }\n const hi = hexCharCodeToNibble[hiCode]!;\n const lo = hexCharCodeToNibble[loCode]!;\n if (hi === invalidNibble || lo === invalidNibble) {\n throw new Error(\"invalid hex\");\n }\n out[i] = (hi << 4) | lo;\n }\n return out;\n}\n\n/** Base64url encoding without padding. */\nexport function encodeBase64Url(bytes: Uint8Array): string {\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** Decodes a base64url string to raw bytes. Throws on invalid input. */\nexport function decodeBase64Url(encoded: string): Uint8Array {\n const base64 = encoded.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const pad = (4 - (base64.length % 4)) % 4;\n const binary = atob(base64 + \"=\".repeat(pad));\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n}\n","import { decodeBase64Url, decodeHex, encodeBase64Url, encodeHex } from \"./bytes.js\";\n\n/** Wire encoding for opaque AES key material (not Crockford base32). */\nexport type OpaqueKeyFormat = \"hex\" | \"base64url\";\n\nconst validAesKeyByteLengths = new Set([16, 24, 32]);\n\n/**\n * Encodes raw AES key bytes for storage in env vars or secret managers.\n *\n * @param bytes - 16, 24, or 32 raw key bytes (AES-128/192/256).\n * @param format - `hex` (lowercase) or `base64url`.\n */\nexport function encodeOpaqueKey(bytes: Uint8Array, format: OpaqueKeyFormat): string {\n assertValidAesKeyByteLength(bytes.length);\n if (format === \"hex\") return encodeHex(bytes);\n return encodeBase64Url(bytes);\n}\n\n/**\n * Decodes key material emitted by `encodeOpaqueKey` (or `ids keygen`) back to raw bytes.\n *\n * @param encoded - Hex or base64url string.\n * @param format - Must match how the string was encoded.\n */\nexport function decodeOpaqueKey(encoded: string, format: OpaqueKeyFormat): Uint8Array {\n let bytes: Uint8Array;\n if (format === \"hex\") {\n if (encoded.length === 0 || encoded.length % 2 !== 0) {\n throw new Error(\"invalid hex key: length must be a positive even number of characters\");\n }\n if (!/^[0-9a-fA-F]+$/.test(encoded)) {\n throw new Error(\"invalid hex key: expected [0-9a-fA-F] only\");\n }\n bytes = decodeHex(encoded);\n } else {\n try {\n bytes = decodeBase64Url(encoded);\n } catch {\n throw new Error(\"invalid base64url key\");\n }\n }\n assertValidAesKeyByteLength(bytes.length);\n return bytes;\n}\n\nfunction assertValidAesKeyByteLength(byteLength: number): void {\n if (!validAesKeyByteLengths.has(byteLength)) {\n throw new Error(`invalid AES key length: expected 16, 24, or 32 bytes, got ${byteLength}`);\n }\n}\n","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 { decodeOpaqueKey, encodeOpaqueKey, type OpaqueKeyFormat } from \"./opaque-key.js\";\n\n/**\n * Configuration options for an Opaque codec instance.\n */\nexport type OpaqueOptions = {\n /** AES-CBC key used for encryption and decryption. */\n key: CryptoKey;\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */\n rng: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * A brand-scoped codec for generating and validating encrypted (opaque) IDs.\n *\n * Same wire shape as the Timestamp codec (`{brand}_` + 26 base32 chars) but the\n * payload is AES-CBC encrypted. `generate`, `generateAt`, and `extractTimestamp`\n * are async; parsing methods are sync. No `minIdForTime` / `maxIdForTime` —\n * encrypted payloads do not sort by creation time.\n */\nexport type OpaqueCodec<Brand extends string> = {\n /** Produces a new canonical encrypted ID using the codec's `now` and `rng`. */\n generate(): Promise<Id<Brand>>;\n /** Produces a new canonical encrypted ID with timestamp bytes from `date`. Throws on invalid dates. */\n generateAt(date: Date): Promise<Id<Brand>>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decrypts and decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n */\n extractTimestamp(id: Id<Brand>): Promise<Date>;\n /** JSON Schema for the canonical wire form (`example` is a structural placeholder). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\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\n/**\n * Imports a raw AES key for use with the Opaque codec.\n *\n * @param bytes - Raw key bytes (16, 24, or 32 bytes for AES-128/192/256).\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\n/**\n * Creates an Opaque codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Required `key` plus optional `now`, `rng`, and `allowDuplicateBrand` overrides.\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":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|