@smonn/ids 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -5,6 +5,40 @@ import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as
5
5
  import { t as createReverseTimestampId } from "./reverse-d5uEoIET.mjs";
6
6
  import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-BnRSC03a.mjs";
7
7
  import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-BI9UXnAm.mjs";
8
+ //#region src/cli/key-io.ts
9
+ function isKeyFormatError(result) {
10
+ return result !== "hex" && result !== "base64url";
11
+ }
12
+ function parseKeyFormatFlag(values) {
13
+ const fromFlag = values.get("--key-format");
14
+ if (fromFlag === void 0) return void 0;
15
+ if (fromFlag === "") return "--key-format requires a value";
16
+ if (fromFlag === "hex" || fromFlag === "base64url") return fromFlag;
17
+ return `--key-format must be hex or base64url, got '${fromFlag}'`;
18
+ }
19
+ function parseKeyFormatFromFlag(values) {
20
+ const fromFlag = parseKeyFormatFlag(values);
21
+ if (fromFlag === void 0) return "hex";
22
+ return fromFlag;
23
+ }
24
+ function parseKeyFormat(values, opts, facet) {
25
+ const fromFlag = parseKeyFormatFlag(values);
26
+ if (fromFlag !== void 0) return fromFlag;
27
+ const fromEnv = (opts.env ?? process.env)[facet.formatEnvVar];
28
+ if (fromEnv === void 0 || fromEnv === "") return "hex";
29
+ if (fromEnv === "hex" || fromEnv === "base64url") return fromEnv;
30
+ return `${facet.formatEnvVar} must be hex or base64url, got '${fromEnv}'`;
31
+ }
32
+ async function loadKey(opts, format, facet) {
33
+ const raw = (opts.env ?? process.env)[facet.envVar];
34
+ if (raw === void 0 || raw === "") return `missing ${facet.envVar} environment variable`;
35
+ try {
36
+ return await facet.import(facet.decode(raw, format));
37
+ } catch (err) {
38
+ return err.message;
39
+ }
40
+ }
41
+ //#endregion
8
42
  //#region src/cli/codec-options.ts
9
43
  function codecOpts(opts) {
10
44
  const o = { allowDuplicateBrand: true };
@@ -13,73 +47,6 @@ function codecOpts(opts) {
13
47
  return o;
14
48
  }
15
49
  //#endregion
16
- //#region src/cli/format.ts
17
- function formatCliError(err) {
18
- return isIdsError(err) ? `${err.code}: ${err.message}` : err instanceof Error ? err.message : String(err);
19
- }
20
- function formatWrappedInspectOutput(result) {
21
- const inputLine = describeInputForm(result.input, result.canonical);
22
- return [
23
- `brand: ${result.brand}`,
24
- `lookup-key: ${result.lookupKey.toString()}`,
25
- `canonical: ${result.canonical}`,
26
- `input: ${inputLine}`,
27
- ""
28
- ].join("\n");
29
- }
30
- function formatSignedInspectOutput(result) {
31
- const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
32
- const inputLine = describeInputForm(result.input, result.canonical);
33
- const lines = [`brand: ${result.brand}`, `timestamp: ${result.timestamp.toISOString()} (${relative})`];
34
- lines.push(`verification: ${result.verification}`);
35
- lines.push(`canonical: ${result.canonical}`, `input: ${inputLine}`, "");
36
- return lines.join("\n");
37
- }
38
- function formatInspectOutput(result) {
39
- const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
40
- const inputLine = describeInputForm(result.input, result.canonical);
41
- return [
42
- `brand: ${result.brand}`,
43
- `timestamp: ${result.timestamp.toISOString()} (${relative})`,
44
- `canonical: ${result.canonical}`,
45
- `input: ${inputLine}`,
46
- ""
47
- ].join("\n");
48
- }
49
- function describeInputForm(input, canonical) {
50
- if (input === canonical) return "canonical";
51
- const notes = [];
52
- if (input !== input.toLowerCase()) notes.push("was uppercase");
53
- if (/[ilo]/i.test(input.slice(4))) notes.push("used Crockford aliases");
54
- return `not canonical (${notes.join(" + ")})`;
55
- }
56
- const msPerMinute = 60 * 1e3;
57
- const msPerHour = 60 * msPerMinute;
58
- const msPerDay = 24 * msPerHour;
59
- const daysPerMonth = 30.44;
60
- const monthsPerYear = 12;
61
- function formatRelative(thenMs, nowMs) {
62
- const diff = nowMs - thenMs;
63
- const abs = Math.abs(diff);
64
- const suffix = diff < 0 ? "from now" : "ago";
65
- const head = headUnits(abs);
66
- return head === "" ? "just now" : `${head} ${suffix}`;
67
- }
68
- function headUnits(abs) {
69
- if (abs < msPerMinute) return "";
70
- if (abs < msPerHour) return unit(Math.round(abs / msPerMinute), "minute");
71
- if (abs < msPerDay) return unit(Math.round(abs / msPerHour), "hour");
72
- if (abs < msPerDay * daysPerMonth) return unit(Math.round(abs / msPerDay), "day");
73
- const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));
74
- if (totalMonths < monthsPerYear) return unit(totalMonths, "month");
75
- const years = Math.floor(totalMonths / monthsPerYear);
76
- const months = totalMonths % monthsPerYear;
77
- return months === 0 ? unit(years, "year") : `${unit(years, "year")} ${unit(months, "month")}`;
78
- }
79
- function unit(n, name) {
80
- return `${n} ${n === 1 ? name : `${name}s`}`;
81
- }
82
- //#endregion
83
50
  //#region src/cli/constants.ts
84
51
  const maxGenerateCount = 1e4;
85
52
  //#endregion
@@ -188,527 +155,84 @@ function isKindError(result) {
188
155
  return result !== "u32" && result !== "i32" && result !== "u64" && result !== "i64";
189
156
  }
190
157
  //#endregion
191
- //#region src/cli/key-io.ts
192
- function isKeyFormatError(result) {
193
- return result !== "hex" && result !== "base64url";
194
- }
195
- function parseKeyFormatFlag(values) {
196
- const fromFlag = values.get("--key-format");
197
- if (fromFlag === void 0) return void 0;
198
- if (fromFlag === "") return "--key-format requires a value";
199
- if (fromFlag === "hex" || fromFlag === "base64url") return fromFlag;
200
- return `--key-format must be hex or base64url, got '${fromFlag}'`;
158
+ //#region src/cli/format.ts
159
+ function formatCliError(err) {
160
+ return isIdsError(err) ? `${err.code}: ${err.message}` : err instanceof Error ? err.message : String(err);
201
161
  }
202
- function parseKeyFormatFromFlag(values) {
203
- const fromFlag = parseKeyFormatFlag(values);
204
- if (fromFlag === void 0) return "hex";
205
- return fromFlag;
162
+ function formatWrappedInspectOutput(result) {
163
+ const inputLine = describeInputForm(result.input, result.canonical);
164
+ return [
165
+ `brand: ${result.brand}`,
166
+ `lookup-key: ${result.lookupKey.toString()}`,
167
+ `canonical: ${result.canonical}`,
168
+ `input: ${inputLine}`,
169
+ ""
170
+ ].join("\n");
206
171
  }
207
- function parseKeyFormat(values, opts, facet) {
208
- const fromFlag = parseKeyFormatFlag(values);
209
- if (fromFlag !== void 0) return fromFlag;
210
- const fromEnv = (opts.env ?? process.env)[facet.formatEnvVar];
211
- if (fromEnv === void 0 || fromEnv === "") return "hex";
212
- if (fromEnv === "hex" || fromEnv === "base64url") return fromEnv;
213
- return `${facet.formatEnvVar} must be hex or base64url, got '${fromEnv}'`;
172
+ function formatSignedInspectOutput(result) {
173
+ const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
174
+ const inputLine = describeInputForm(result.input, result.canonical);
175
+ const lines = [`brand: ${result.brand}`, `timestamp: ${result.timestamp.toISOString()} (${relative})`];
176
+ lines.push(`verification: ${result.verification}`);
177
+ lines.push(`canonical: ${result.canonical}`, `input: ${inputLine}`, "");
178
+ return lines.join("\n");
214
179
  }
215
- async function loadKey(opts, format, facet) {
216
- const raw = (opts.env ?? process.env)[facet.envVar];
217
- if (raw === void 0 || raw === "") return `missing ${facet.envVar} environment variable`;
218
- try {
219
- return await facet.import(facet.decode(raw, format));
220
- } catch (err) {
221
- return err.message;
222
- }
180
+ function formatInspectOutput(result) {
181
+ const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
182
+ const inputLine = describeInputForm(result.input, result.canonical);
183
+ return [
184
+ `brand: ${result.brand}`,
185
+ `timestamp: ${result.timestamp.toISOString()} (${relative})`,
186
+ `canonical: ${result.canonical}`,
187
+ `input: ${inputLine}`,
188
+ ""
189
+ ].join("\n");
223
190
  }
224
- //#endregion
225
- //#region src/cli/opaque-key.ts
226
- const opaqueFacet = {
227
- envVar: "IDS_KEY",
228
- formatEnvVar: "IDS_KEY_FORMAT",
229
- decode: decodeOpaqueKey,
230
- import: importOpaqueKey
231
- };
232
- function parseOpaqueKeyFormat(values, opts) {
233
- return parseKeyFormat(values, opts, opaqueFacet);
191
+ function describeInputForm(input, canonical) {
192
+ if (input === canonical) return "canonical";
193
+ const notes = [];
194
+ if (input !== input.toLowerCase()) notes.push("was uppercase");
195
+ if (/[ilo]/i.test(input.slice(4))) notes.push("used Crockford aliases");
196
+ return `not canonical (${notes.join(" + ")})`;
234
197
  }
235
- async function loadOpaqueKey(opts, format) {
236
- return loadKey(opts, format, opaqueFacet);
198
+ const msPerMinute = 60 * 1e3;
199
+ const msPerHour = 60 * msPerMinute;
200
+ const msPerDay = 24 * msPerHour;
201
+ const daysPerMonth = 30.44;
202
+ const monthsPerYear = 12;
203
+ function formatRelative(thenMs, nowMs) {
204
+ const diff = nowMs - thenMs;
205
+ const abs = Math.abs(diff);
206
+ const suffix = diff < 0 ? "from now" : "ago";
207
+ const head = headUnits(abs);
208
+ return head === "" ? "just now" : `${head} ${suffix}`;
237
209
  }
238
- //#endregion
239
- //#region src/cli/signing-key.ts
240
- const signingFacet = {
241
- envVar: "IDS_SIGNING_KEY",
242
- formatEnvVar: "IDS_SIGNING_KEY_FORMAT",
243
- decode: decodeSigningKey,
244
- import: importSigningKey
245
- };
246
- function parseSigningKeyFormat(values, opts) {
247
- return parseKeyFormat(values, opts, signingFacet);
210
+ function headUnits(abs) {
211
+ if (abs < msPerMinute) return "";
212
+ if (abs < msPerHour) return unit(Math.round(abs / msPerMinute), "minute");
213
+ if (abs < msPerDay) return unit(Math.round(abs / msPerHour), "hour");
214
+ if (abs < msPerDay * daysPerMonth) return unit(Math.round(abs / msPerDay), "day");
215
+ const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));
216
+ if (totalMonths < monthsPerYear) return unit(totalMonths, "month");
217
+ const years = Math.floor(totalMonths / monthsPerYear);
218
+ const months = totalMonths % monthsPerYear;
219
+ return months === 0 ? unit(years, "year") : `${unit(years, "year")} ${unit(months, "month")}`;
248
220
  }
249
- async function loadSigningKey(opts, format) {
250
- return loadKey(opts, format, signingFacet);
221
+ function unit(n, name) {
222
+ return `${n} ${n === 1 ? name : `${name}s`}`;
251
223
  }
252
224
  //#endregion
253
- //#region src/cli/commands/generate.ts
254
- function runGenerate(args, opts) {
255
- const { flags, values, positionals, errors } = splitFlags(args, /* @__PURE__ */ new Set([
256
- "--count",
257
- "-c",
258
- "--bits",
259
- "--key-format",
260
- "--kind"
261
- ]));
262
- const unsupported = unsupportedFlagForCommand("generate", flags, /* @__PURE__ */ new Set([
263
- "--count",
264
- "-c",
265
- "--opaque",
266
- "--reverse",
267
- "--signed",
268
- "--key-format"
269
- ]));
270
- if (unsupported !== void 0) {
271
- opts.stderr(unsupported + "\n");
272
- return Promise.resolve(1);
273
- }
274
- if (errors[0] !== void 0) {
275
- opts.stderr(errors[0] + "\n");
276
- return Promise.resolve(1);
277
- }
278
- const extra = positionals[1];
279
- if (extra !== void 0) {
280
- opts.stderr(`unexpected argument: ${extra}\n`);
281
- return Promise.resolve(1);
282
- }
283
- const [brand] = positionals;
284
- const count = parseCount(values);
285
- if (typeof count === "string") {
286
- opts.stderr(count + "\n");
287
- return Promise.resolve(1);
288
- }
289
- const opaque = flags.has("--opaque");
290
- const reverse = flags.has("--reverse");
291
- const signed = flags.has("--signed");
292
- if (reverse && opaque) {
293
- opts.stderr("cannot use --reverse and --opaque together\n");
294
- return Promise.resolve(1);
295
- }
296
- if (signed && opaque) {
297
- opts.stderr("cannot use --signed and --opaque together\n");
298
- return Promise.resolve(1);
299
- }
300
- if (signed && reverse) {
301
- opts.stderr("cannot use --signed and --reverse together\n");
302
- return Promise.resolve(1);
303
- }
304
- if (!opaque && !signed && flags.has("--key-format")) {
305
- opts.stderr("--key-format requires --opaque or --signed\n");
306
- return Promise.resolve(1);
307
- }
308
- if (opaque) {
309
- const format = parseOpaqueKeyFormat(values, opts);
310
- if (isKeyFormatError(format)) {
311
- opts.stderr(format + "\n");
312
- return Promise.resolve(1);
313
- }
314
- return runOpaqueGenerate(brand ?? "", count, format, opts);
315
- }
316
- if (signed) {
317
- const format = parseSigningKeyFormat(values, opts);
318
- if (isKeyFormatError(format)) {
319
- opts.stderr(format + "\n");
320
- return Promise.resolve(1);
321
- }
322
- return runSignedGenerate(brand ?? "", count, format, opts);
323
- }
324
- if (reverse) {
325
- let codec;
225
+ //#region src/cli/variants.ts
226
+ const timestampVariant = {
227
+ inspectMode: "readable",
228
+ construct(brand, opts) {
326
229
  try {
327
- codec = createReverseTimestampId(brand ?? "", codecOpts(opts));
230
+ return createTimestampId(brand, codecOpts(opts));
328
231
  } catch (err) {
329
- opts.stderr(formatCliError(err) + "\n");
330
- return Promise.resolve(1);
232
+ return formatCliError(err);
331
233
  }
332
- for (let i = 0; i < count; i++) opts.stdout(codec.generate() + "\n");
333
- return Promise.resolve(0);
334
- }
335
- let codec;
336
- try {
337
- codec = createTimestampId(brand ?? "", codecOpts(opts));
338
- } catch (err) {
339
- opts.stderr(formatCliError(err) + "\n");
340
- return Promise.resolve(1);
341
- }
342
- for (let i = 0; i < count; i++) opts.stdout(codec.generate() + "\n");
343
- return Promise.resolve(0);
344
- }
345
- async function runOpaqueGenerate(brand, count, format, opts) {
346
- const keyResult = await loadOpaqueKey(opts, format);
347
- if (typeof keyResult === "string") {
348
- opts.stderr(keyResult + "\n");
349
- return 1;
350
234
  }
351
- let codec;
352
- try {
353
- codec = createOpaqueTimestampId(brand, {
354
- key: keyResult,
355
- ...codecOpts(opts)
356
- });
357
- } catch (err) {
358
- opts.stderr(formatCliError(err) + "\n");
359
- return 1;
360
- }
361
- for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
362
- return 0;
363
- }
364
- async function runSignedGenerate(brand, count, format, opts) {
365
- const keyResult = await loadSigningKey(opts, format);
366
- if (typeof keyResult === "string") {
367
- opts.stderr(keyResult + "\n");
368
- return 1;
369
- }
370
- let codec;
371
- try {
372
- codec = createSignedTimestampId(brand, {
373
- keys: [keyResult],
374
- allowDuplicateBrand: true,
375
- ...codecOpts(opts)
376
- });
377
- } catch (err) {
378
- opts.stderr(formatCliError(err) + "\n");
379
- return 1;
380
- }
381
- for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
382
- return 0;
383
- }
384
- //#endregion
385
- //#region src/cli/wrapping-key.ts
386
- const wrappingFacet = {
387
- envVar: "IDS_WRAPPING_KEY",
388
- formatEnvVar: "IDS_WRAPPING_KEY_FORMAT",
389
- decode: decodeWrappingKey,
390
- import: importWrappingKey
391
235
  };
392
- function parseWrappingKeyFormat(values, opts) {
393
- return parseKeyFormat(values, opts, wrappingFacet);
394
- }
395
- async function loadWrappingKey(opts, format) {
396
- return loadKey(opts, format, wrappingFacet);
397
- }
398
- //#endregion
399
- //#region src/cli/usage.ts
400
- function usage() {
401
- return [
402
- "Usage: ids <subcommand> [args]",
403
- "",
404
- "Subcommands:",
405
- " inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
406
- " Decode an ID and print brand, timestamp (or lookup key), and canonical form.",
407
- " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
408
- " --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
409
- " --kind is required with --wrapped: u32, i32, u64, or i64.",
410
- " --reverse decodes a Reverse Timestamp ID (newest-first sort order).",
411
- " --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
412
- " Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
413
- " generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--key-format hex|base64url]",
414
- ` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
415
- " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
416
- " --reverse mints Reverse Timestamp IDs (newest-first sort order).",
417
- " --signed mints Signed Timestamp IDs; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
418
- " keygen, k [--wrapped] [--signed] [--bits 128|192|256] [--key-format hex|base64url]",
419
- " Emit a random key for importOpaqueKey, importWrappingKey, or importSigningKey (stdout only).",
420
- " --wrapped emits a wrapping key for importWrappingKey instead (IDS_WRAPPING_KEY).",
421
- " --signed emits a signing key for importSigningKey instead (IDS_SIGNING_KEY; hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
422
- ""
423
- ].join("\n");
424
- }
425
- //#endregion
426
- //#region src/cli/commands/inspect.ts
427
- function runInspect(args, opts) {
428
- const { flags, values, positionals, errors } = splitFlags(args, /* @__PURE__ */ new Set([
429
- "--count",
430
- "-c",
431
- "--bits",
432
- "--key-format",
433
- "--kind"
434
- ]));
435
- const unsupported = unsupportedFlagForCommand("inspect", flags, /* @__PURE__ */ new Set([
436
- "--opaque",
437
- "--wrapped",
438
- "--reverse",
439
- "--signed",
440
- "--kind",
441
- "--key-format"
442
- ]));
443
- if (unsupported !== void 0) {
444
- opts.stderr(unsupported + "\n");
445
- return Promise.resolve(1);
446
- }
447
- if (errors[0] !== void 0) {
448
- opts.stderr(errors[0] + "\n");
449
- return Promise.resolve(1);
450
- }
451
- const [input] = positionals;
452
- if (input === void 0) {
453
- opts.stderr(usage());
454
- return Promise.resolve(1);
455
- }
456
- const extra = positionals[1];
457
- if (extra !== void 0) {
458
- opts.stderr(`unexpected argument: ${extra}\n`);
459
- return Promise.resolve(1);
460
- }
461
- const opaque = flags.has("--opaque");
462
- const wrapped = flags.has("--wrapped");
463
- const reverse = flags.has("--reverse");
464
- const signed = flags.has("--signed");
465
- if (opaque && wrapped) {
466
- opts.stderr("cannot use --wrapped and --opaque together\n");
467
- return Promise.resolve(1);
468
- }
469
- if (reverse && opaque) {
470
- opts.stderr("cannot use --reverse and --opaque together\n");
471
- return Promise.resolve(1);
472
- }
473
- if (reverse && wrapped) {
474
- opts.stderr("cannot use --reverse and --wrapped together\n");
475
- return Promise.resolve(1);
476
- }
477
- if (signed && opaque) {
478
- opts.stderr("cannot use --signed and --opaque together\n");
479
- return Promise.resolve(1);
480
- }
481
- if (signed && wrapped) {
482
- opts.stderr("cannot use --signed and --wrapped together\n");
483
- return Promise.resolve(1);
484
- }
485
- if (signed && reverse) {
486
- opts.stderr("cannot use --signed and --reverse together\n");
487
- return Promise.resolve(1);
488
- }
489
- if (!opaque && !wrapped && !signed && flags.has("--key-format")) {
490
- opts.stderr("--key-format requires --opaque, --wrapped, or --signed\n");
491
- return Promise.resolve(1);
492
- }
493
- const brand = input.slice(0, 3).toLowerCase();
494
- if (wrapped) {
495
- const kind = parseKind(values);
496
- if (kind === void 0) {
497
- opts.stderr("--kind is required with --wrapped\n");
498
- return Promise.resolve(1);
499
- }
500
- if (isKindError(kind)) {
501
- opts.stderr(kind + "\n");
502
- return Promise.resolve(1);
503
- }
504
- const format = parseWrappingKeyFormat(values, opts);
505
- if (isKeyFormatError(format)) {
506
- opts.stderr(format + "\n");
507
- return Promise.resolve(1);
508
- }
509
- return runWrappedInspect(brand, input, kind, format, opts);
510
- }
511
- if (opaque) {
512
- const format = parseOpaqueKeyFormat(values, opts);
513
- if (isKeyFormatError(format)) {
514
- opts.stderr(format + "\n");
515
- return Promise.resolve(1);
516
- }
517
- return runOpaqueInspect(brand, input, format, opts);
518
- }
519
- if (signed) {
520
- const format = parseSigningKeyFormat(values, opts);
521
- if (isKeyFormatError(format)) {
522
- opts.stderr(format + "\n");
523
- return Promise.resolve(1);
524
- }
525
- return runSignedInspect(brand, input, format, opts);
526
- }
527
- if (reverse) {
528
- let reverseCodec;
529
- try {
530
- reverseCodec = createReverseTimestampId(brand, codecOpts(opts));
531
- } catch (err) {
532
- opts.stderr(formatCliError(err) + "\n");
533
- return Promise.resolve(1);
534
- }
535
- const reverseValidation = reverseCodec["~standard"].validate(input);
536
- if (reverseValidation.issues) {
537
- opts.stderr(reverseValidation.issues[0].message + "\n");
538
- return Promise.resolve(1);
539
- }
540
- const reverseCanonical = reverseValidation.value;
541
- const reverseTimestamp = reverseCodec.extractTimestamp(reverseCanonical);
542
- const reverseNowMs = (opts.now ?? Date.now)();
543
- opts.stdout(formatInspectOutput({
544
- brand,
545
- timestamp: reverseTimestamp,
546
- canonical: reverseCanonical,
547
- input,
548
- nowMs: reverseNowMs
549
- }));
550
- return Promise.resolve(0);
551
- }
552
- let codec;
553
- try {
554
- codec = createTimestampId(brand, codecOpts(opts));
555
- } catch (err) {
556
- opts.stderr(formatCliError(err) + "\n");
557
- return Promise.resolve(1);
558
- }
559
- const validation = codec["~standard"].validate(input);
560
- if (validation.issues) {
561
- opts.stderr(validation.issues[0].message + "\n");
562
- return Promise.resolve(1);
563
- }
564
- const canonical = validation.value;
565
- const timestamp = codec.extractTimestamp(canonical);
566
- const nowMs = (opts.now ?? Date.now)();
567
- opts.stdout(formatInspectOutput({
568
- brand,
569
- timestamp,
570
- canonical,
571
- input,
572
- nowMs
573
- }));
574
- return Promise.resolve(0);
575
- }
576
- async function runWrappedInspect(brand, input, kind, format, opts) {
577
- const keyResult = await loadWrappingKey(opts, format);
578
- if (typeof keyResult === "string") {
579
- opts.stderr(keyResult + "\n");
580
- return 1;
581
- }
582
- let codec;
583
- try {
584
- codec = createWrappedKeyId(brand, {
585
- kind,
586
- keys: [keyResult],
587
- allowDuplicateBrand: true
588
- });
589
- } catch (err) {
590
- opts.stderr(formatCliError(err) + "\n");
591
- return 1;
592
- }
593
- const validation = codec["~standard"].validate(input);
594
- if (validation.issues) {
595
- opts.stderr(validation.issues[0].message + "\n");
596
- return 1;
597
- }
598
- const canonical = validation.value;
599
- let lookupKey;
600
- try {
601
- lookupKey = await codec.unwrap(canonical);
602
- } catch (err) {
603
- opts.stderr(formatCliError(err) + "\n");
604
- return 1;
605
- }
606
- opts.stdout(formatWrappedInspectOutput({
607
- brand,
608
- lookupKey,
609
- canonical,
610
- input
611
- }));
612
- return 0;
613
- }
614
- async function runOpaqueInspect(brand, input, format, opts) {
615
- const keyResult = await loadOpaqueKey(opts, format);
616
- if (typeof keyResult === "string") {
617
- opts.stderr(keyResult + "\n");
618
- return 1;
619
- }
620
- let codec;
621
- try {
622
- codec = createOpaqueTimestampId(brand, {
623
- key: keyResult,
624
- ...codecOpts(opts)
625
- });
626
- } catch (err) {
627
- opts.stderr(formatCliError(err) + "\n");
628
- return 1;
629
- }
630
- const validation = codec["~standard"].validate(input);
631
- if (validation.issues) {
632
- opts.stderr(validation.issues[0].message + "\n");
633
- return 1;
634
- }
635
- const canonical = validation.value;
636
- const timestamp = await codec.extractTimestamp(canonical);
637
- const nowMs = (opts.now ?? Date.now)();
638
- opts.stderr("note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp\n");
639
- opts.stdout(formatInspectOutput({
640
- brand,
641
- timestamp,
642
- canonical,
643
- input,
644
- nowMs
645
- }));
646
- return 0;
647
- }
648
- async function runSignedInspect(brand, input, format, opts) {
649
- let structCodec;
650
- try {
651
- structCodec = createTimestampId(brand, codecOpts(opts));
652
- } catch (err) {
653
- opts.stderr(formatCliError(err) + "\n");
654
- return 1;
655
- }
656
- const validation = structCodec["~standard"].validate(input);
657
- if (validation.issues) {
658
- opts.stderr(validation.issues[0].message + "\n");
659
- return 1;
660
- }
661
- const canonical = validation.value;
662
- const timestamp = structCodec.extractTimestamp(canonical);
663
- const nowMs = (opts.now ?? Date.now)();
664
- const keyResult = await loadSigningKey(opts, format);
665
- if (typeof keyResult === "string") {
666
- opts.stdout(formatSignedInspectOutput({
667
- brand,
668
- timestamp,
669
- canonical,
670
- input,
671
- nowMs,
672
- verification: "unavailable"
673
- }));
674
- opts.stderr(keyResult + "\n");
675
- return 1;
676
- }
677
- const verifyResult = await createSignedTimestampId(brand, {
678
- keys: [keyResult],
679
- allowDuplicateBrand: true,
680
- ...codecOpts(opts)
681
- }).safeVerify(input);
682
- if (!verifyResult.ok) {
683
- /* v8 ignore next 4 -- defensive: both codecs share the same wire parse so ParseError
684
- is unreachable after the createTimestampId pre-validation above passes */
685
- if (verifyResult.error !== "verification_failed") {
686
- opts.stderr(verifyResult.error + "\n");
687
- return 1;
688
- }
689
- opts.stdout(formatSignedInspectOutput({
690
- brand,
691
- timestamp,
692
- canonical,
693
- input,
694
- nowMs,
695
- verification: "failed"
696
- }));
697
- opts.stderr("verification_failed: verification failed\n");
698
- return 1;
699
- }
700
- opts.stdout(formatSignedInspectOutput({
701
- brand,
702
- timestamp,
703
- canonical: verifyResult.id,
704
- input,
705
- nowMs,
706
- verification: "ok"
707
- }));
708
- return 0;
709
- }
710
- //#endregion
711
- //#region src/cli/variants.ts
712
236
  const opaqueVariant = {
713
237
  flag: "--opaque",
714
238
  key: {
@@ -794,6 +318,25 @@ const conflictPriorityOrder = [
794
318
  wrappedVariant,
795
319
  opaqueVariant
796
320
  ];
321
+ const generatePolicy = {
322
+ default: timestampVariant,
323
+ selectable: [
324
+ opaqueVariant,
325
+ reverseVariant,
326
+ signedVariant
327
+ ],
328
+ intrinsicFlags: ["--count", "-c"]
329
+ };
330
+ const inspectPolicy = {
331
+ default: timestampVariant,
332
+ selectable: [
333
+ reverseVariant,
334
+ wrappedVariant,
335
+ opaqueVariant,
336
+ signedVariant
337
+ ],
338
+ intrinsicFlags: []
339
+ };
797
340
  const keygenPolicy = {
798
341
  default: opaqueVariant,
799
342
  selectable: [wrappedVariant, signedVariant],
@@ -813,11 +356,248 @@ function deriveAllowedFlags(policy) {
813
356
  return flags;
814
357
  }
815
358
  function resolveVariant(policy, flags) {
816
- const selected = conflictPriorityOrder.filter((v) => policy.selectable.includes(v) && v.flag !== void 0 && flags.has(v.flag));
359
+ const selected = conflictPriorityOrder.filter((v) => policy.selectable.some((d) => d === v) && v.flag !== void 0 && flags.has(v.flag));
817
360
  if (selected.length === 0) return policy.default;
818
361
  if (selected.length === 1) return selected[0];
819
362
  return `cannot use ${selected[0].flag} and ${selected[1].flag} together`;
820
363
  }
364
+ async function buildCodec(variant, brand, values, opts) {
365
+ let key;
366
+ if (variant.key !== void 0) {
367
+ const format = parseKeyFormat(values, opts, variant.key);
368
+ if (isKeyFormatError(format)) return format;
369
+ const keyResult = await loadKey(opts, format, variant.key);
370
+ if (typeof keyResult === "string") return keyResult;
371
+ key = keyResult;
372
+ }
373
+ return variant.construct(brand, opts, key, values);
374
+ }
375
+ //#endregion
376
+ //#region src/cli/commands/generate.ts
377
+ async function runGenerate(args, opts) {
378
+ const allowedFlags = deriveAllowedFlags(generatePolicy);
379
+ const selectorFlags = new Set(generatePolicy.selectable.map((v) => v.flag).filter((f) => f !== void 0));
380
+ const { flags, values, positionals, errors } = splitFlags(args, new Set([...allowedFlags].filter((f) => !selectorFlags.has(f))));
381
+ const unsupported = unsupportedFlagForCommand("generate", flags, allowedFlags);
382
+ if (unsupported !== void 0) {
383
+ opts.stderr(unsupported + "\n");
384
+ return 1;
385
+ }
386
+ if (errors[0] !== void 0) {
387
+ opts.stderr(errors[0] + "\n");
388
+ return 1;
389
+ }
390
+ const extra = positionals[1];
391
+ if (extra !== void 0) {
392
+ opts.stderr(`unexpected argument: ${extra}\n`);
393
+ return 1;
394
+ }
395
+ const [brand] = positionals;
396
+ const count = parseCount(values);
397
+ if (typeof count === "string") {
398
+ opts.stderr(count + "\n");
399
+ return 1;
400
+ }
401
+ const variant = resolveVariant(generatePolicy, flags);
402
+ if (typeof variant === "string") {
403
+ opts.stderr(variant + "\n");
404
+ return 1;
405
+ }
406
+ if (variant.key === void 0 && flags.has("--key-format")) {
407
+ opts.stderr("--key-format requires --opaque or --signed\n");
408
+ return 1;
409
+ }
410
+ const codec = await buildCodec(variant, brand ?? "", values, opts);
411
+ if (typeof codec === "string") {
412
+ opts.stderr(codec + "\n");
413
+ return 1;
414
+ }
415
+ for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
416
+ return 0;
417
+ }
418
+ //#endregion
419
+ //#region src/cli/usage.ts
420
+ function usage() {
421
+ return [
422
+ "Usage: ids <subcommand> [args]",
423
+ "",
424
+ "Subcommands:",
425
+ " inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
426
+ " Decode an ID and print brand, timestamp (or lookup key), and canonical form.",
427
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
428
+ " --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
429
+ " --kind is required with --wrapped: u32, i32, u64, or i64.",
430
+ " --reverse decodes a Reverse Timestamp ID (newest-first sort order).",
431
+ " --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
432
+ " Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
433
+ " generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--key-format hex|base64url]",
434
+ ` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
435
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
436
+ " --reverse mints Reverse Timestamp IDs (newest-first sort order).",
437
+ " --signed mints Signed Timestamp IDs; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
438
+ " keygen, k [--wrapped] [--signed] [--bits 128|192|256] [--key-format hex|base64url]",
439
+ " Emit a random key for importOpaqueKey, importWrappingKey, or importSigningKey (stdout only).",
440
+ " --wrapped emits a wrapping key for importWrappingKey instead (IDS_WRAPPING_KEY).",
441
+ " --signed emits a signing key for importSigningKey instead (IDS_SIGNING_KEY; hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
442
+ ""
443
+ ].join("\n");
444
+ }
445
+ //#endregion
446
+ //#region src/cli/commands/inspect.ts
447
+ async function runInspect(args, opts) {
448
+ const allowedFlags = deriveAllowedFlags(inspectPolicy);
449
+ const selectorFlags = new Set(inspectPolicy.selectable.map((v) => v.flag).filter((f) => f !== void 0));
450
+ const { flags, values, positionals, errors } = splitFlags(args, new Set([...allowedFlags].filter((f) => !selectorFlags.has(f))));
451
+ const unsupported = unsupportedFlagForCommand("inspect", flags, allowedFlags);
452
+ if (unsupported !== void 0) {
453
+ opts.stderr(unsupported + "\n");
454
+ return 1;
455
+ }
456
+ if (errors[0] !== void 0) {
457
+ opts.stderr(errors[0] + "\n");
458
+ return 1;
459
+ }
460
+ const [input] = positionals;
461
+ if (input === void 0) {
462
+ opts.stderr(usage());
463
+ return 1;
464
+ }
465
+ const extra = positionals[1];
466
+ if (extra !== void 0) {
467
+ opts.stderr(`unexpected argument: ${extra}\n`);
468
+ return 1;
469
+ }
470
+ const variant = resolveVariant(inspectPolicy, flags);
471
+ if (typeof variant === "string") {
472
+ opts.stderr(variant + "\n");
473
+ return 1;
474
+ }
475
+ if (variant.key === void 0 && flags.has("--key-format")) {
476
+ opts.stderr("--key-format requires --opaque, --wrapped, or --signed\n");
477
+ return 1;
478
+ }
479
+ const brand = input.slice(0, 3).toLowerCase();
480
+ let verifyTimestamp;
481
+ let verifyCanonical;
482
+ let verifyNowMs;
483
+ if (variant.inspectMode === "verify") {
484
+ const fmtCheck = parseKeyFormat(values, opts, variant.key);
485
+ if (isKeyFormatError(fmtCheck)) {
486
+ opts.stderr(fmtCheck + "\n");
487
+ return 1;
488
+ }
489
+ let tsCodec;
490
+ try {
491
+ tsCodec = createTimestampId(brand, codecOpts(opts));
492
+ } catch (err) {
493
+ opts.stderr(formatCliError(err) + "\n");
494
+ return 1;
495
+ }
496
+ const structValidation = tsCodec["~standard"].validate(input);
497
+ if (structValidation.issues) {
498
+ opts.stderr(structValidation.issues[0].message + "\n");
499
+ return 1;
500
+ }
501
+ verifyCanonical = structValidation.value;
502
+ verifyTimestamp = tsCodec.extractTimestamp(verifyCanonical);
503
+ verifyNowMs = (opts.now ?? Date.now)();
504
+ }
505
+ const codecOrError = await buildCodec(variant, brand, values, opts);
506
+ if (typeof codecOrError === "string") {
507
+ if (variant.inspectMode === "verify") opts.stdout(formatSignedInspectOutput({
508
+ brand,
509
+ timestamp: verifyTimestamp,
510
+ canonical: verifyCanonical,
511
+ input,
512
+ nowMs: verifyNowMs,
513
+ verification: "unavailable"
514
+ }));
515
+ opts.stderr(codecOrError + "\n");
516
+ return 1;
517
+ }
518
+ let canonical;
519
+ if (variant.inspectMode !== "verify") {
520
+ const validation = codecOrError["~standard"].validate(input);
521
+ if (validation.issues) {
522
+ opts.stderr(validation.issues[0].message + "\n");
523
+ return 1;
524
+ }
525
+ canonical = validation.value;
526
+ }
527
+ switch (variant.inspectMode) {
528
+ case "readable": {
529
+ const timestamp = codecOrError.extractTimestamp(canonical);
530
+ const nowMs = (opts.now ?? Date.now)();
531
+ opts.stdout(formatInspectOutput({
532
+ brand,
533
+ timestamp,
534
+ canonical,
535
+ input,
536
+ nowMs
537
+ }));
538
+ return 0;
539
+ }
540
+ case "keyed-readable": {
541
+ const timestamp = await codecOrError.extractTimestamp(canonical);
542
+ const nowMs = (opts.now ?? Date.now)();
543
+ opts.stderr("note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp\n");
544
+ opts.stdout(formatInspectOutput({
545
+ brand,
546
+ timestamp,
547
+ canonical,
548
+ input,
549
+ nowMs
550
+ }));
551
+ return 0;
552
+ }
553
+ case "unwrap": {
554
+ let lookupKey;
555
+ try {
556
+ lookupKey = await codecOrError.unwrap(canonical);
557
+ } catch (err) {
558
+ opts.stderr(formatCliError(err) + "\n");
559
+ return 1;
560
+ }
561
+ opts.stdout(formatWrappedInspectOutput({
562
+ brand,
563
+ lookupKey,
564
+ canonical,
565
+ input
566
+ }));
567
+ return 0;
568
+ }
569
+ case "verify": {
570
+ const verifyResult = await codecOrError.safeVerify(input);
571
+ if (!verifyResult.ok) {
572
+ /* v8 ignore next 4 -- defensive: both codecs share the same wire parse so ParseError
573
+ is unreachable after the createTimestampId pre-validation above passes */
574
+ if (verifyResult.error !== "verification_failed") {
575
+ opts.stderr(verifyResult.error + "\n");
576
+ return 1;
577
+ }
578
+ opts.stdout(formatSignedInspectOutput({
579
+ brand,
580
+ timestamp: verifyTimestamp,
581
+ canonical: verifyCanonical,
582
+ input,
583
+ nowMs: verifyNowMs,
584
+ verification: "failed"
585
+ }));
586
+ opts.stderr("verification_failed: verification failed\n");
587
+ return 1;
588
+ }
589
+ opts.stdout(formatSignedInspectOutput({
590
+ brand,
591
+ timestamp: verifyTimestamp,
592
+ canonical: verifyResult.id,
593
+ input,
594
+ nowMs: verifyNowMs,
595
+ verification: "ok"
596
+ }));
597
+ return 0;
598
+ }
599
+ }
600
+ }
821
601
  //#endregion
822
602
  //#region src/cli/commands/keygen.ts
823
603
  function runKeygen(args, opts) {