@smonn/ids 0.12.3 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +42 -32
  2. package/dist/{adapter-types-CdYJM6Sf.d.mts → adapter-types-CIc-4O-P.d.mts} +2 -2
  3. package/dist/{adapter-types-CdYJM6Sf.d.mts.map → adapter-types-CIc-4O-P.d.mts.map} +1 -1
  4. package/dist/cli.mjs +316 -178
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{codec-shell-DvrTDa65.mjs → codec-shell-C2NKQEx2.mjs} +5 -1
  7. package/dist/codec-shell-C2NKQEx2.mjs.map +1 -0
  8. package/dist/{digest-CknNw2wa.mjs → digest-DsGeXfk3.mjs} +9 -24
  9. package/dist/digest-DsGeXfk3.mjs.map +1 -0
  10. package/dist/digest.d.mts +3 -3
  11. package/dist/digest.d.mts.map +1 -1
  12. package/dist/digest.mjs +1 -1
  13. package/dist/drizzle.d.mts +3 -3
  14. package/dist/drizzle.d.mts.map +1 -1
  15. package/dist/drizzle.mjs.map +1 -1
  16. package/dist/error-Cp5qYZcv.mjs.map +1 -1
  17. package/dist/{error-JIPylU_E.d.mts → error-Dqyho9vp.d.mts} +7 -2
  18. package/dist/error-Dqyho9vp.d.mts.map +1 -0
  19. package/dist/express.d.mts +2 -2
  20. package/dist/fastify.d.mts +2 -2
  21. package/dist/graphql.d.mts +3 -3
  22. package/dist/graphql.mjs +2 -2
  23. package/dist/graphql.mjs.map +1 -1
  24. package/dist/hono.d.mts +2 -2
  25. package/dist/hono.mjs +1 -2
  26. package/dist/hono.mjs.map +1 -1
  27. package/dist/index.d.mts +21 -5
  28. package/dist/index.d.mts.map +1 -1
  29. package/dist/index.mjs +1 -1
  30. package/dist/{key-material-f29JIyrz.mjs → key-material-DvjACe89.mjs} +53 -2
  31. package/dist/key-material-DvjACe89.mjs.map +1 -0
  32. package/dist/kysely.d.mts +3 -3
  33. package/dist/kysely.d.mts.map +1 -1
  34. package/dist/kysely.mjs.map +1 -1
  35. package/dist/mikro-orm.d.mts +3 -3
  36. package/dist/mikro-orm.d.mts.map +1 -1
  37. package/dist/mikro-orm.mjs.map +1 -1
  38. package/dist/nestjs.d.mts +2 -2
  39. package/dist/nestjs.mjs +1 -1
  40. package/dist/nestjs.mjs.map +1 -1
  41. package/dist/{opaque-BQVNoIIh.mjs → opaque-BW3Uzeeb.mjs} +5 -28
  42. package/dist/opaque-BW3Uzeeb.mjs.map +1 -0
  43. package/dist/opaque.d.mts +22 -4
  44. package/dist/opaque.d.mts.map +1 -1
  45. package/dist/opaque.mjs +1 -1
  46. package/dist/prisma.d.mts +32 -27
  47. package/dist/prisma.d.mts.map +1 -1
  48. package/dist/prisma.mjs +11 -15
  49. package/dist/prisma.mjs.map +1 -1
  50. package/dist/{reverse-DsPd7Lco.mjs → reverse-BW8g_cln.mjs} +12 -5
  51. package/dist/reverse-BW8g_cln.mjs.map +1 -0
  52. package/dist/reverse.d.mts +20 -4
  53. package/dist/reverse.d.mts.map +1 -1
  54. package/dist/reverse.mjs +1 -1
  55. package/dist/{rng-Clos6uC0.mjs → rng-BHFxX1Fc.mjs} +2 -2
  56. package/dist/{rng-Clos6uC0.mjs.map → rng-BHFxX1Fc.mjs.map} +1 -1
  57. package/dist/{signed-4h2BnlWx.mjs → signed-BTz3ZFYE.mjs} +12 -33
  58. package/dist/signed-BTz3ZFYE.mjs.map +1 -0
  59. package/dist/signed.d.mts +13 -4
  60. package/dist/signed.d.mts.map +1 -1
  61. package/dist/signed.mjs +1 -1
  62. package/dist/{timestamp-Cg9nRfnK.mjs → timestamp-CleAIdZI.mjs} +12 -5
  63. package/dist/timestamp-CleAIdZI.mjs.map +1 -0
  64. package/dist/typeorm.d.mts +2 -2
  65. package/dist/typeorm.d.mts.map +1 -1
  66. package/dist/typeorm.mjs.map +1 -1
  67. package/dist/{types-g7CiQDyE.d.mts → types-wplmOgOK.d.mts} +20 -3
  68. package/dist/types-wplmOgOK.d.mts.map +1 -0
  69. package/dist/{wrapped-BQ-lNECo.mjs → wrapped-DPlsv1x-.mjs} +18 -76
  70. package/dist/wrapped-DPlsv1x-.mjs.map +1 -0
  71. package/dist/wrapped.d.mts +31 -5
  72. package/dist/wrapped.d.mts.map +1 -1
  73. package/dist/wrapped.mjs +1 -1
  74. package/package.json +80 -27
  75. package/dist/codec-shell-DvrTDa65.mjs.map +0 -1
  76. package/dist/digest-CknNw2wa.mjs.map +0 -1
  77. package/dist/error-JIPylU_E.d.mts.map +0 -1
  78. package/dist/key-material-f29JIyrz.mjs.map +0 -1
  79. package/dist/opaque-BQVNoIIh.mjs.map +0 -1
  80. package/dist/reverse-DsPd7Lco.mjs.map +0 -1
  81. package/dist/signed-4h2BnlWx.mjs.map +0 -1
  82. package/dist/timestamp-Cg9nRfnK.mjs.map +0 -1
  83. package/dist/types-g7CiQDyE.d.mts.map +0 -1
  84. package/dist/wrapped-BQ-lNECo.mjs.map +0 -1
package/dist/cli.mjs CHANGED
@@ -1,12 +1,85 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as isIdsError } from "./error-Cp5qYZcv.mjs";
3
- import { t as createTimestampId } from "./timestamp-Cg9nRfnK.mjs";
4
- import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-BQVNoIIh.mjs";
5
- import { t as createReverseTimestampId } from "./reverse-DsPd7Lco.mjs";
6
- import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-4h2BnlWx.mjs";
7
- import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-BQ-lNECo.mjs";
8
- import { i as importDigestKey, n as decodeDigestKey, r as encodeDigestKey, t as createDigestId } from "./digest-CknNw2wa.mjs";
3
+ import { t as createTimestampId } from "./timestamp-CleAIdZI.mjs";
4
+ import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-BW3Uzeeb.mjs";
5
+ import { t as createReverseTimestampId } from "./reverse-BW8g_cln.mjs";
6
+ import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-BTz3ZFYE.mjs";
7
+ import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-DPlsv1x-.mjs";
8
+ import { i as importDigestKey, n as decodeDigestKey, r as encodeDigestKey, t as createDigestId } from "./digest-DsGeXfk3.mjs";
9
+ //#region src/cli/format.ts
10
+ const invalidIdPrefix = "invalid_id: ";
11
+ function formatCliError(err) {
12
+ return isIdsError(err) ? `${err.code}: ${err.message}` : err instanceof Error ? err.message : String(err);
13
+ }
14
+ function formatWrappedInspectOutput(result) {
15
+ const inputLine = describeInputForm(result.input, result.canonical);
16
+ return [
17
+ `brand: ${result.brand}`,
18
+ `lookup-key: ${result.lookupKey.toString()}`,
19
+ `canonical: ${result.canonical}`,
20
+ `input: ${inputLine}`,
21
+ ""
22
+ ].join("\n");
23
+ }
24
+ function formatSignedInspectOutput(result) {
25
+ const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
26
+ const inputLine = describeInputForm(result.input, result.canonical);
27
+ const lines = [`brand: ${result.brand}`, `timestamp: ${result.timestamp.toISOString()} (${relative})`];
28
+ lines.push(`verification: ${result.verification}`);
29
+ lines.push(`canonical: ${result.canonical}`, `input: ${inputLine}`, "");
30
+ return lines.join("\n");
31
+ }
32
+ function formatInspectOutput(result) {
33
+ const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
34
+ const inputLine = describeInputForm(result.input, result.canonical);
35
+ return [
36
+ `brand: ${result.brand}`,
37
+ `timestamp: ${result.timestamp.toISOString()} (${relative})`,
38
+ `canonical: ${result.canonical}`,
39
+ `input: ${inputLine}`,
40
+ ""
41
+ ].join("\n");
42
+ }
43
+ function describeInputForm(input, canonical) {
44
+ if (input === canonical) return "canonical";
45
+ const notes = [];
46
+ if (input !== input.toLowerCase()) notes.push("was uppercase");
47
+ if (/[ilo]/i.test(input.slice(4))) notes.push("used Crockford aliases");
48
+ return `not canonical (${notes.join(" + ")})`;
49
+ }
50
+ const msPerMinute = 60 * 1e3;
51
+ const msPerHour = 60 * msPerMinute;
52
+ const msPerDay = 24 * msPerHour;
53
+ const daysPerMonth = 30.44;
54
+ const monthsPerYear = 12;
55
+ function formatRelative(thenMs, nowMs) {
56
+ const diff = nowMs - thenMs;
57
+ const abs = Math.abs(diff);
58
+ const suffix = diff < 0 ? "from now" : "ago";
59
+ const head = headUnits(abs);
60
+ return head === "" ? "just now" : `${head} ${suffix}`;
61
+ }
62
+ function headUnits(abs) {
63
+ if (abs < 6e4) return "";
64
+ if (abs < 36e5) return unit(Math.round(abs / msPerMinute), "minute");
65
+ if (abs < 864e5) return unit(Math.round(abs / msPerHour), "hour");
66
+ if (abs < 864e5 * daysPerMonth) return unit(Math.round(abs / msPerDay), "day");
67
+ const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));
68
+ if (totalMonths < monthsPerYear) return unit(totalMonths, "month");
69
+ const years = Math.floor(totalMonths / monthsPerYear);
70
+ const months = totalMonths % monthsPerYear;
71
+ return months === 0 ? unit(years, "year") : `${unit(years, "year")} ${unit(months, "month")}`;
72
+ }
73
+ function unit(n, name) {
74
+ return `${n} ${n === 1 ? name : `${name}s`}`;
75
+ }
76
+ //#endregion
9
77
  //#region src/cli/key-io.ts
78
+ function isLoadKeyError(value) {
79
+ if (typeof value !== "object" || value === null) return false;
80
+ const kind = value.kind;
81
+ return kind === "missing" || kind === "import-failure";
82
+ }
10
83
  function isKeyFormatError(result) {
11
84
  return result !== "hex" && result !== "base64url";
12
85
  }
@@ -32,11 +105,17 @@ function parseKeyFormat(values, opts, facet) {
32
105
  }
33
106
  async function loadKey(opts, format, facet) {
34
107
  const raw = (opts.env ?? process.env)[facet.envVar];
35
- if (raw === void 0 || raw === "") return `missing ${facet.envVar} environment variable`;
108
+ if (raw === void 0 || raw === "") return {
109
+ kind: "missing",
110
+ message: `missing ${facet.envVar} environment variable`
111
+ };
36
112
  try {
37
113
  return await facet.import(facet.decode(raw, format));
38
114
  } catch (err) {
39
- return err.message;
115
+ return {
116
+ kind: "import-failure",
117
+ message: formatCliError(err)
118
+ };
40
119
  }
41
120
  }
42
121
  //#endregion
@@ -167,76 +246,21 @@ function isNsError(result) {
167
246
  return result === "--ns requires a value";
168
247
  }
169
248
  //#endregion
170
- //#region src/cli/format.ts
171
- function formatCliError(err) {
172
- return isIdsError(err) ? `${err.code}: ${err.message}` : err instanceof Error ? err.message : String(err);
173
- }
174
- function formatWrappedInspectOutput(result) {
175
- const inputLine = describeInputForm(result.input, result.canonical);
176
- return [
177
- `brand: ${result.brand}`,
178
- `lookup-key: ${result.lookupKey.toString()}`,
179
- `canonical: ${result.canonical}`,
180
- `input: ${inputLine}`,
181
- ""
182
- ].join("\n");
183
- }
184
- function formatSignedInspectOutput(result) {
185
- const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
186
- const inputLine = describeInputForm(result.input, result.canonical);
187
- const lines = [`brand: ${result.brand}`, `timestamp: ${result.timestamp.toISOString()} (${relative})`];
188
- lines.push(`verification: ${result.verification}`);
189
- lines.push(`canonical: ${result.canonical}`, `input: ${inputLine}`, "");
190
- return lines.join("\n");
191
- }
192
- function formatInspectOutput(result) {
193
- const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
194
- const inputLine = describeInputForm(result.input, result.canonical);
195
- return [
196
- `brand: ${result.brand}`,
197
- `timestamp: ${result.timestamp.toISOString()} (${relative})`,
198
- `canonical: ${result.canonical}`,
199
- `input: ${inputLine}`,
200
- ""
201
- ].join("\n");
202
- }
203
- function describeInputForm(input, canonical) {
204
- if (input === canonical) return "canonical";
205
- const notes = [];
206
- if (input !== input.toLowerCase()) notes.push("was uppercase");
207
- if (/[ilo]/i.test(input.slice(4))) notes.push("used Crockford aliases");
208
- return `not canonical (${notes.join(" + ")})`;
209
- }
210
- const msPerMinute = 60 * 1e3;
211
- const msPerHour = 60 * msPerMinute;
212
- const msPerDay = 24 * msPerHour;
213
- const daysPerMonth = 30.44;
214
- const monthsPerYear = 12;
215
- function formatRelative(thenMs, nowMs) {
216
- const diff = nowMs - thenMs;
217
- const abs = Math.abs(diff);
218
- const suffix = diff < 0 ? "from now" : "ago";
219
- const head = headUnits(abs);
220
- return head === "" ? "just now" : `${head} ${suffix}`;
221
- }
222
- function headUnits(abs) {
223
- if (abs < 6e4) return "";
224
- if (abs < 36e5) return unit(Math.round(abs / msPerMinute), "minute");
225
- if (abs < 864e5) return unit(Math.round(abs / msPerHour), "hour");
226
- if (abs < 864e5 * daysPerMonth) return unit(Math.round(abs / msPerDay), "day");
227
- const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));
228
- if (totalMonths < monthsPerYear) return unit(totalMonths, "month");
229
- const years = Math.floor(totalMonths / monthsPerYear);
230
- const months = totalMonths % monthsPerYear;
231
- return months === 0 ? unit(years, "year") : `${unit(years, "year")} ${unit(months, "month")}`;
232
- }
233
- function unit(n, name) {
234
- return `${n} ${n === 1 ? name : `${name}s`}`;
235
- }
236
- //#endregion
237
249
  //#region src/cli/variants.ts
250
+ function standardValidate(codec, input) {
251
+ const result = codec["~standard"].validate(input);
252
+ if (result.issues) return { issue: invalidIdPrefix + result.issues[0].message };
253
+ return { value: result.value };
254
+ }
238
255
  const timestampVariant = {
239
- inspectMode: "readable",
256
+ inspect: {
257
+ mode: "readable",
258
+ note: "note: timestamp assumes a plaintext Timestamp ID; if this ID was Opaque-encoded, the timestamp is meaningless — re-run with --opaque and the correct IDS_KEY",
259
+ validate: standardValidate,
260
+ extractTimestamp(codec, id) {
261
+ return codec.extractTimestamp(id);
262
+ }
263
+ },
240
264
  construct(brand, opts) {
241
265
  try {
242
266
  return createTimestampId(brand, codecOpts(opts));
@@ -254,7 +278,14 @@ const opaqueVariant = {
254
278
  decode: decodeOpaqueKey,
255
279
  import: importOpaqueKey
256
280
  },
257
- inspectMode: "keyed-readable",
281
+ inspect: {
282
+ mode: "keyed-readable",
283
+ note: "note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp",
284
+ validate: standardValidate,
285
+ extractTimestamp(codec, id) {
286
+ return codec.extractTimestamp(id);
287
+ }
288
+ },
258
289
  construct(brand, opts, key) {
259
290
  try {
260
291
  return createOpaqueTimestampId(brand, {
@@ -268,7 +299,14 @@ const opaqueVariant = {
268
299
  };
269
300
  const reverseVariant = {
270
301
  flag: "--reverse",
271
- inspectMode: "readable",
302
+ inspect: {
303
+ mode: "readable",
304
+ note: "note: timestamp assumes a plaintext Timestamp ID; if this ID was Opaque-encoded, the timestamp is meaningless — re-run with --opaque and the correct IDS_KEY",
305
+ validate: standardValidate,
306
+ extractTimestamp(codec, id) {
307
+ return codec.extractTimestamp(id);
308
+ }
309
+ },
272
310
  construct(brand, opts) {
273
311
  try {
274
312
  return createReverseTimestampId(brand, codecOpts(opts));
@@ -286,7 +324,13 @@ const wrappedVariant = {
286
324
  decode: decodeWrappingKey,
287
325
  import: importWrappingKey
288
326
  },
289
- inspectMode: "unwrap",
327
+ inspect: {
328
+ mode: "unwrap",
329
+ validate: standardValidate,
330
+ unwrap(codec, id) {
331
+ return codec.unwrap(id);
332
+ }
333
+ },
290
334
  extraFlags: ["--kind"],
291
335
  construct(brand, _opts, key, values) {
292
336
  const kind = parseKind(values ?? /* @__PURE__ */ new Map());
@@ -312,7 +356,12 @@ const signedVariant = {
312
356
  decode: decodeSigningKey,
313
357
  import: importSigningKey
314
358
  },
315
- inspectMode: "verify",
359
+ inspect: {
360
+ mode: "verify",
361
+ safeVerify(codec, id) {
362
+ return codec.safeVerify(id);
363
+ }
364
+ },
316
365
  construct(brand, opts, key) {
317
366
  try {
318
367
  return createSignedTimestampId(brand, {
@@ -333,7 +382,7 @@ const digestVariant = {
333
382
  decode: decodeDigestKey,
334
383
  import: importDigestKey
335
384
  },
336
- inspectMode: "unsupported",
385
+ inspect: { mode: "unsupported" },
337
386
  extraFlags: ["--ns"],
338
387
  construct(brand, opts, key, values) {
339
388
  const ns = parseNs(values ?? /* @__PURE__ */ new Map());
@@ -347,8 +396,9 @@ const digestVariant = {
347
396
  });
348
397
  return {
349
398
  safeParse: (v) => codec.safeParse(v),
350
- generate() {
351
- return (opts.readStdin ?? (() => Promise.resolve("")))().then((material) => codec.digest(material));
399
+ async generate() {
400
+ const material = await (opts.readStdin ?? (() => Promise.resolve("")))();
401
+ return codec.digest(material);
352
402
  }
353
403
  };
354
404
  } catch (err) {
@@ -394,6 +444,11 @@ const keygenPolicy = {
394
444
  };
395
445
  //#endregion
396
446
  //#region src/cli/dispatch.ts
447
+ function isCodecError(v) {
448
+ if (typeof v !== "object" || v === null) return false;
449
+ const kind = v.kind;
450
+ return (kind === "usage" || kind === "runtime") && "message" in v;
451
+ }
397
452
  function deriveAllowedFlags(policy) {
398
453
  const flags = new Set(policy.intrinsicFlags);
399
454
  let hasKeyed = policy.default.key !== void 0;
@@ -415,12 +470,106 @@ async function buildCodec(variant, brand, values, opts) {
415
470
  let key;
416
471
  if (variant.key !== void 0) {
417
472
  const format = parseKeyFormat(values, opts, variant.key);
418
- if (isKeyFormatError(format)) return format;
473
+ if (isKeyFormatError(format)) return {
474
+ kind: "usage",
475
+ message: format
476
+ };
419
477
  const keyResult = await loadKey(opts, format, variant.key);
420
- if (typeof keyResult === "string") return keyResult;
478
+ if (isLoadKeyError(keyResult)) return {
479
+ kind: keyResult.kind === "missing" ? "usage" : "runtime",
480
+ message: keyResult.message
481
+ };
421
482
  key = keyResult;
422
483
  }
423
- return variant.construct(brand, opts, key, values);
484
+ const codecOrError = variant.construct(brand, opts, key, values);
485
+ if (typeof codecOrError === "string") return {
486
+ kind: codecOrError.startsWith("--") ? "usage" : "runtime",
487
+ message: codecOrError
488
+ };
489
+ return codecOrError;
490
+ }
491
+ //#endregion
492
+ //#region src/cli/usage.ts
493
+ function usageInspect() {
494
+ return [
495
+ "Usage: ids inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
496
+ "",
497
+ " Decode an ID and print brand, timestamp (or lookup key), and canonical form.",
498
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
499
+ " --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
500
+ " --kind is required with --wrapped: u32, i32, u64, or i64.",
501
+ " --reverse decodes a Reverse Timestamp ID (newest-first sort order).",
502
+ " --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
503
+ " Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
504
+ " Note: --digest is not supported for inspect (Digest IDs are one-way; there is no reverse path).",
505
+ ""
506
+ ].join("\n");
507
+ }
508
+ function usageGenerate() {
509
+ return [
510
+ `Usage: ids generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--key-format hex|base64url]`,
511
+ "",
512
+ ` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
513
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
514
+ " --reverse mints Reverse Timestamp IDs (newest-first sort order).",
515
+ " --signed mints Signed Timestamp IDs; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
516
+ " --digest mints a deterministic Digest ID from material read on stdin.",
517
+ " --ns <ns> is required: the namespace domain separator (non-secret, non-empty).",
518
+ " Reads the digest key from IDS_DIGEST_KEY (hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
519
+ " Same material + ns + key always produces the same ID. Digest IDs are one-way.",
520
+ " --count N > 1 is rejected: same material always produces the same ID.",
521
+ ""
522
+ ].join("\n");
523
+ }
524
+ function usageKeygen() {
525
+ return [
526
+ "Usage: ids keygen, k [--wrapped] [--signed] [--digest] [--bits 128|192|256] [--key-format hex|base64url]",
527
+ "",
528
+ " Emit a random key for importOpaqueKey, importWrappingKey, importSigningKey, or importDigestKey (stdout only).",
529
+ " --wrapped emits a wrapping key for importWrappingKey instead (IDS_WRAPPING_KEY).",
530
+ " --signed emits a signing key for importSigningKey instead (IDS_SIGNING_KEY; hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
531
+ " --digest emits a digest key for importDigestKey instead (IDS_DIGEST_KEY; hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
532
+ ""
533
+ ].join("\n");
534
+ }
535
+ function usage() {
536
+ return [
537
+ "Usage: ids <subcommand> [args]",
538
+ "",
539
+ "Subcommands:",
540
+ " inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
541
+ " Decode an ID and print brand, timestamp (or lookup key), and canonical form.",
542
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
543
+ " --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
544
+ " --kind is required with --wrapped: u32, i32, u64, or i64.",
545
+ " --reverse decodes a Reverse Timestamp ID (newest-first sort order).",
546
+ " --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
547
+ " Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
548
+ " Note: --digest is not supported for inspect (Digest IDs are one-way; there is no reverse path).",
549
+ " generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--key-format hex|base64url]",
550
+ ` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
551
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
552
+ " --reverse mints Reverse Timestamp IDs (newest-first sort order).",
553
+ " --signed mints Signed Timestamp IDs; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
554
+ " --digest mints a deterministic Digest ID from material read on stdin.",
555
+ " --ns <ns> is required: the namespace domain separator (non-secret, non-empty).",
556
+ " Reads the digest key from IDS_DIGEST_KEY (hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
557
+ " Same material + ns + key always produces the same ID. Digest IDs are one-way.",
558
+ " --count N > 1 is rejected: same material always produces the same ID.",
559
+ " keygen, k [--wrapped] [--signed] [--digest] [--bits 128|192|256] [--key-format hex|base64url]",
560
+ " Emit a random key for importOpaqueKey, importWrappingKey, importSigningKey, or importDigestKey (key on stdout; warning on stderr).",
561
+ " Safe handling: redirect stdout to a 0600 file (e.g. ids keygen > key.hex && chmod 0600 key.hex);",
562
+ " do not let the key appear in shell history or CI logs. A warning is printed to stderr on every run.",
563
+ " --wrapped emits a wrapping key for importWrappingKey instead (IDS_WRAPPING_KEY).",
564
+ " --signed emits a signing key for importSigningKey instead (IDS_SIGNING_KEY; hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
565
+ " --digest emits a digest key for importDigestKey instead (IDS_DIGEST_KEY; hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
566
+ "",
567
+ "Exit codes:",
568
+ " 0 Success",
569
+ " 1 Runtime/operational error (codec failure, bad key material, verification failure)",
570
+ " 2 Usage/argument error (unknown subcommand, unrecognised flag, bad flag value, missing required arg)",
571
+ ""
572
+ ].join("\n");
424
573
  }
425
574
  //#endregion
426
575
  //#region src/cli/commands/generate.ts
@@ -437,131 +586,106 @@ function readProcessStdin() {
437
586
  return stdinCache;
438
587
  }
439
588
  async function runGenerate(args, opts) {
589
+ if (args.includes("--help") || args.includes("-h")) {
590
+ opts.stdout(usageGenerate());
591
+ return 0;
592
+ }
440
593
  const allowedFlags = deriveAllowedFlags(generatePolicy);
441
594
  const selectorFlags = new Set(generatePolicy.selectable.map((v) => v.flag).filter((f) => f !== void 0));
442
595
  const { flags, values, positionals, errors } = splitFlags(args, new Set([...allowedFlags].filter((f) => !selectorFlags.has(f))));
443
596
  const unsupported = unsupportedFlagForCommand("generate", flags, allowedFlags);
444
597
  if (unsupported !== void 0) {
445
598
  opts.stderr(unsupported + "\n");
446
- return 1;
599
+ return 2;
447
600
  }
448
601
  if (errors[0] !== void 0) {
449
602
  opts.stderr(errors[0] + "\n");
450
- return 1;
603
+ return 2;
451
604
  }
452
605
  const extra = positionals[1];
453
606
  if (extra !== void 0) {
454
607
  opts.stderr(`unexpected argument: ${extra}\n`);
455
- return 1;
608
+ return 2;
456
609
  }
457
610
  const [brand] = positionals;
458
611
  const count = parseCount(values);
459
612
  if (typeof count === "string") {
460
613
  opts.stderr(count + "\n");
461
- return 1;
614
+ return 2;
462
615
  }
463
616
  const variant = resolveVariant(generatePolicy, flags);
464
617
  if (typeof variant === "string") {
465
618
  opts.stderr(variant + "\n");
466
- return 1;
619
+ return 2;
467
620
  }
468
621
  if (variant.key === void 0 && flags.has("--key-format")) {
469
622
  opts.stderr("--key-format requires --opaque, --signed, or --digest\n");
470
- return 1;
623
+ return 2;
471
624
  }
472
625
  if (flags.has("--digest") && count > 1) {
473
626
  opts.stderr("--count N > 1 is rejected with --digest: same material always produces the same ID\n");
474
- return 1;
627
+ return 2;
475
628
  }
476
629
  const optsWithStdin = {
477
630
  ...opts,
478
631
  readStdin: opts.readStdin ?? readProcessStdin
479
632
  };
480
633
  const codec = await buildCodec(variant, brand ?? "", values, optsWithStdin);
481
- if (typeof codec === "string") {
482
- opts.stderr(codec + "\n");
483
- return 1;
634
+ if (isCodecError(codec)) {
635
+ opts.stderr(codec.message + "\n");
636
+ return codec.kind === "usage" ? 2 : 1;
484
637
  }
485
638
  for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
486
639
  return 0;
487
640
  }
488
641
  //#endregion
489
- //#region src/cli/usage.ts
490
- function usage() {
491
- return [
492
- "Usage: ids <subcommand> [args]",
493
- "",
494
- "Subcommands:",
495
- " inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
496
- " Decode an ID and print brand, timestamp (or lookup key), and canonical form.",
497
- " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
498
- " --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
499
- " --kind is required with --wrapped: u32, i32, u64, or i64.",
500
- " --reverse decodes a Reverse Timestamp ID (newest-first sort order).",
501
- " --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
502
- " Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
503
- " Note: --digest is not supported for inspect (Digest IDs are one-way; there is no reverse path).",
504
- " generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--key-format hex|base64url]",
505
- ` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
506
- " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
507
- " --reverse mints Reverse Timestamp IDs (newest-first sort order).",
508
- " --signed mints Signed Timestamp IDs; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
509
- " --digest mints a deterministic Digest ID from material read on stdin.",
510
- " --ns <ns> is required: the namespace domain separator (non-secret, non-empty).",
511
- " Reads the digest key from IDS_DIGEST_KEY (hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
512
- " Same material + ns + key always produces the same ID. Digest IDs are one-way.",
513
- " --count N > 1 is rejected: same material always produces the same ID.",
514
- " keygen, k [--wrapped] [--signed] [--digest] [--bits 128|192|256] [--key-format hex|base64url]",
515
- " Emit a random key for importOpaqueKey, importWrappingKey, importSigningKey, or importDigestKey (stdout only).",
516
- " --wrapped emits a wrapping key for importWrappingKey instead (IDS_WRAPPING_KEY).",
517
- " --signed emits a signing key for importSigningKey instead (IDS_SIGNING_KEY; hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
518
- " --digest emits a digest key for importDigestKey instead (IDS_DIGEST_KEY; hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
519
- ""
520
- ].join("\n");
521
- }
522
- //#endregion
523
642
  //#region src/cli/commands/inspect.ts
524
643
  async function runInspect(args, opts) {
644
+ if (args.includes("--help") || args.includes("-h")) {
645
+ opts.stdout(usageInspect());
646
+ return 0;
647
+ }
525
648
  const allowedFlags = deriveAllowedFlags(inspectPolicy);
526
649
  const selectorFlags = new Set(inspectPolicy.selectable.map((v) => v.flag).filter((f) => f !== void 0));
527
650
  const { flags, values, positionals, errors } = splitFlags(args, new Set([...allowedFlags].filter((f) => !selectorFlags.has(f))));
528
651
  const unsupported = unsupportedFlagForCommand("inspect", flags, allowedFlags);
529
652
  if (unsupported !== void 0) {
530
653
  opts.stderr(unsupported + "\n");
531
- return 1;
654
+ return 2;
532
655
  }
533
656
  if (errors[0] !== void 0) {
534
657
  opts.stderr(errors[0] + "\n");
535
- return 1;
658
+ return 2;
536
659
  }
537
660
  const [input] = positionals;
538
661
  if (input === void 0) {
539
- opts.stderr(usage());
540
- return 1;
662
+ opts.stderr(usageInspect());
663
+ return 2;
541
664
  }
542
665
  const extra = positionals[1];
543
666
  if (extra !== void 0) {
544
667
  opts.stderr(`unexpected argument: ${extra}\n`);
545
- return 1;
668
+ return 2;
546
669
  }
547
670
  const variant = resolveVariant(inspectPolicy, flags);
548
671
  if (typeof variant === "string") {
549
672
  opts.stderr(variant + "\n");
550
- return 1;
673
+ return 2;
551
674
  }
552
675
  if (variant.key === void 0 && flags.has("--key-format")) {
553
676
  opts.stderr("--key-format requires --opaque, --wrapped, or --signed\n");
554
- return 1;
677
+ return 2;
555
678
  }
556
679
  const brand = input.slice(0, 3).toLowerCase();
680
+ const cap = variant.inspect;
557
681
  let verifyTimestamp;
558
682
  let verifyCanonical;
559
683
  let verifyNowMs;
560
- if (variant.inspectMode === "verify") {
684
+ if (cap.mode === "verify") {
561
685
  const fmtCheck = parseKeyFormat(values, opts, variant.key);
562
686
  if (isKeyFormatError(fmtCheck)) {
563
687
  opts.stderr(fmtCheck + "\n");
564
- return 1;
688
+ return 2;
565
689
  }
566
690
  let tsCodec;
567
691
  try {
@@ -572,7 +696,7 @@ async function runInspect(args, opts) {
572
696
  }
573
697
  const structValidation = tsCodec["~standard"].validate(input);
574
698
  if (structValidation.issues) {
575
- opts.stderr(structValidation.issues[0].message + "\n");
699
+ opts.stderr(invalidIdPrefix + structValidation.issues[0].message + "\n");
576
700
  return 1;
577
701
  }
578
702
  verifyCanonical = structValidation.value;
@@ -580,32 +704,36 @@ async function runInspect(args, opts) {
580
704
  verifyNowMs = (opts.now ?? Date.now)();
581
705
  }
582
706
  const codecOrError = await buildCodec(variant, brand, values, opts);
583
- if (typeof codecOrError === "string") {
584
- if (variant.inspectMode === "verify") opts.stdout(formatSignedInspectOutput({
585
- brand,
586
- timestamp: verifyTimestamp,
587
- canonical: verifyCanonical,
588
- input,
589
- nowMs: verifyNowMs,
590
- verification: "unavailable"
591
- }));
592
- opts.stderr(codecOrError + "\n");
593
- return 1;
707
+ if (isCodecError(codecOrError)) {
708
+ if (cap.mode === "verify") {
709
+ opts.stdout(formatSignedInspectOutput({
710
+ brand,
711
+ timestamp: verifyTimestamp,
712
+ canonical: verifyCanonical,
713
+ input,
714
+ nowMs: verifyNowMs,
715
+ verification: "unavailable"
716
+ }));
717
+ opts.stderr(codecOrError.message + "\n");
718
+ return 1;
719
+ }
720
+ opts.stderr(codecOrError.message + "\n");
721
+ return codecOrError.kind === "usage" ? 2 : 1;
594
722
  }
595
723
  let canonical;
596
- if (variant.inspectMode !== "verify") {
597
- const validation = codecOrError["~standard"].validate(input);
598
- if (validation.issues) {
599
- opts.stderr(validation.issues[0].message + "\n");
724
+ if (cap.mode !== "verify" && cap.mode !== "unsupported") {
725
+ const parsed = cap.validate(codecOrError, input);
726
+ if ("issue" in parsed) {
727
+ opts.stderr(parsed.issue + "\n");
600
728
  return 1;
601
729
  }
602
- canonical = validation.value;
730
+ canonical = parsed.value;
603
731
  }
604
- switch (variant.inspectMode) {
732
+ switch (cap.mode) {
605
733
  case "readable": {
606
- const timestamp = codecOrError.extractTimestamp(canonical);
734
+ const timestamp = cap.extractTimestamp(codecOrError, canonical);
607
735
  const nowMs = (opts.now ?? Date.now)();
608
- opts.stderr("note: timestamp assumes a plaintext Timestamp ID; if this ID was Opaque-encoded, the timestamp is meaningless — re-run with --opaque and the correct IDS_KEY\n");
736
+ opts.stderr(cap.note + "\n");
609
737
  opts.stdout(formatInspectOutput({
610
738
  brand,
611
739
  timestamp,
@@ -616,9 +744,9 @@ async function runInspect(args, opts) {
616
744
  return 0;
617
745
  }
618
746
  case "keyed-readable": {
619
- const timestamp = await codecOrError.extractTimestamp(canonical);
747
+ const timestamp = await cap.extractTimestamp(codecOrError, canonical);
620
748
  const nowMs = (opts.now ?? Date.now)();
621
- opts.stderr("note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp\n");
749
+ opts.stderr(cap.note + "\n");
622
750
  opts.stdout(formatInspectOutput({
623
751
  brand,
624
752
  timestamp,
@@ -631,7 +759,7 @@ async function runInspect(args, opts) {
631
759
  case "unwrap": {
632
760
  let lookupKey;
633
761
  try {
634
- lookupKey = await codecOrError.unwrap(canonical);
762
+ lookupKey = await cap.unwrap(codecOrError, canonical);
635
763
  } catch (err) {
636
764
  opts.stderr(formatCliError(err) + "\n");
637
765
  return 1;
@@ -645,7 +773,7 @@ async function runInspect(args, opts) {
645
773
  return 0;
646
774
  }
647
775
  case "verify": {
648
- const verifyResult = await codecOrError.safeVerify(input);
776
+ const verifyResult = await cap.safeVerify(codecOrError, input);
649
777
  if (!verifyResult.ok) {
650
778
  /* v8 ignore next 4 -- defensive: both codecs share the same wire parse so ParseError
651
779
  is unreachable after the createTimestampId pre-validation above passes */
@@ -684,38 +812,42 @@ async function runInspect(args, opts) {
684
812
  }
685
813
  //#endregion
686
814
  //#region src/cli/commands/keygen.ts
687
- function runKeygen(args, opts) {
815
+ async function runKeygen(args, opts) {
816
+ if (args.includes("--help") || args.includes("-h")) {
817
+ opts.stdout(usageKeygen());
818
+ return Promise.resolve(0);
819
+ }
688
820
  const allowedFlags = deriveAllowedFlags(keygenPolicy);
689
821
  const variantExtraFlags = new Set(keygenPolicy.selectable.flatMap((v) => v.extraFlags ?? []));
690
822
  const { flags, values, positionals, errors } = splitFlags(args, allowedFlags);
691
823
  const unsupported = unsupportedFlagForCommand("keygen", flags, new Set([...allowedFlags].filter((f) => !variantExtraFlags.has(f))));
692
824
  if (unsupported !== void 0) {
693
825
  opts.stderr(unsupported + "\n");
694
- return Promise.resolve(1);
826
+ return Promise.resolve(2);
695
827
  }
696
828
  if (errors[0] !== void 0) {
697
829
  opts.stderr(errors[0] + "\n");
698
- return Promise.resolve(1);
830
+ return Promise.resolve(2);
699
831
  }
700
832
  const extra = positionals[0];
701
833
  if (extra !== void 0) {
702
834
  opts.stderr(`unexpected argument: ${extra}\n`);
703
- return Promise.resolve(1);
835
+ return Promise.resolve(2);
704
836
  }
705
837
  const variant = resolveVariant(keygenPolicy, flags);
706
838
  if (typeof variant === "string") {
707
839
  opts.stderr(variant + "\n");
708
- return Promise.resolve(1);
840
+ return Promise.resolve(2);
709
841
  }
710
842
  const bits = parseBits(values);
711
843
  if (typeof bits === "string") {
712
844
  opts.stderr(bits + "\n");
713
- return Promise.resolve(1);
845
+ return Promise.resolve(2);
714
846
  }
715
847
  const format = parseKeyFormatFromFlag(values);
716
848
  if (isKeyFormatError(format)) {
717
849
  opts.stderr(format + "\n");
718
- return Promise.resolve(1);
850
+ return Promise.resolve(2);
719
851
  }
720
852
  /* v8 ignore next 4 -- defensive guard; all keygenPolicy variants have key defined */
721
853
  if (variant.key === void 0) {
@@ -724,6 +856,7 @@ function runKeygen(args, opts) {
724
856
  }
725
857
  const bytes = new Uint8Array(bits / 8);
726
858
  crypto.getRandomValues(bytes);
859
+ opts.stderr("Warning: secret key material — redirect to a file (chmod 0600) and avoid shell history.\n");
727
860
  opts.stdout(variant.key.encode(bytes, format) + "\n");
728
861
  return Promise.resolve(0);
729
862
  }
@@ -744,15 +877,20 @@ const commands = [
744
877
  }
745
878
  ];
746
879
  async function run(opts) {
747
- const [subcommand, ...rest] = opts.argv;
748
- const command = commands.find((candidate) => candidate.names.includes(subcommand ?? ""));
749
- if (command !== void 0) return command.run(rest, opts);
750
- if (subcommand === void 0 || subcommand === "--help" || subcommand === "-h") {
751
- opts.stdout(usage());
752
- return 0;
880
+ try {
881
+ const [subcommand, ...rest] = opts.argv;
882
+ const command = commands.find((candidate) => candidate.names.includes(subcommand ?? ""));
883
+ if (command !== void 0) return await command.run(rest, opts);
884
+ if (subcommand === void 0 || subcommand === "--help" || subcommand === "-h") {
885
+ opts.stdout(usage());
886
+ return 0;
887
+ }
888
+ opts.stderr(usage());
889
+ return 2;
890
+ } catch (err) {
891
+ opts.stderr(formatCliError(err) + "\n");
892
+ return 1;
753
893
  }
754
- opts.stderr(usage());
755
- return 1;
756
894
  }
757
895
  //#endregion
758
896
  //#region bin/cli.ts