@smonn/ids 0.12.2 → 0.13.0

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 (80) hide show
  1. package/README.md +32 -2
  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 +235 -103
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/codec-shell-DvrTDa65.mjs.map +1 -1
  7. package/dist/{digest-CknNw2wa.mjs → digest-Drnof-l_.mjs} +8 -23
  8. package/dist/digest-Drnof-l_.mjs.map +1 -0
  9. package/dist/digest.d.mts +3 -3
  10. package/dist/digest.d.mts.map +1 -1
  11. package/dist/digest.mjs +1 -1
  12. package/dist/drizzle.d.mts +3 -3
  13. package/dist/drizzle.d.mts.map +1 -1
  14. package/dist/drizzle.mjs.map +1 -1
  15. package/dist/error-Cp5qYZcv.mjs.map +1 -1
  16. package/dist/{error-JIPylU_E.d.mts → error-Dqyho9vp.d.mts} +7 -2
  17. package/dist/error-Dqyho9vp.d.mts.map +1 -0
  18. package/dist/express.d.mts +2 -2
  19. package/dist/fastify.d.mts +2 -2
  20. package/dist/graphql.d.mts +3 -3
  21. package/dist/graphql.mjs +2 -2
  22. package/dist/graphql.mjs.map +1 -1
  23. package/dist/hono.d.mts +2 -2
  24. package/dist/hono.mjs +1 -2
  25. package/dist/hono.mjs.map +1 -1
  26. package/dist/index.d.mts +21 -5
  27. package/dist/index.d.mts.map +1 -1
  28. package/dist/index.mjs +1 -1
  29. package/dist/{key-material-f29JIyrz.mjs → key-material-DsukgnR5.mjs} +53 -2
  30. package/dist/key-material-DsukgnR5.mjs.map +1 -0
  31. package/dist/kysely.d.mts +3 -3
  32. package/dist/kysely.d.mts.map +1 -1
  33. package/dist/kysely.mjs.map +1 -1
  34. package/dist/mikro-orm.d.mts +3 -3
  35. package/dist/mikro-orm.d.mts.map +1 -1
  36. package/dist/mikro-orm.mjs.map +1 -1
  37. package/dist/nestjs.d.mts +2 -2
  38. package/dist/nestjs.mjs +1 -1
  39. package/dist/nestjs.mjs.map +1 -1
  40. package/dist/{opaque-BQVNoIIh.mjs → opaque-D7y5cgzT.mjs} +3 -26
  41. package/dist/opaque-D7y5cgzT.mjs.map +1 -0
  42. package/dist/opaque.d.mts +22 -4
  43. package/dist/opaque.d.mts.map +1 -1
  44. package/dist/opaque.mjs +1 -1
  45. package/dist/prisma.d.mts +32 -27
  46. package/dist/prisma.d.mts.map +1 -1
  47. package/dist/prisma.mjs +11 -15
  48. package/dist/prisma.mjs.map +1 -1
  49. package/dist/{reverse-DsPd7Lco.mjs → reverse-DrAofYWV.mjs} +10 -3
  50. package/dist/reverse-DrAofYWV.mjs.map +1 -0
  51. package/dist/reverse.d.mts +20 -4
  52. package/dist/reverse.d.mts.map +1 -1
  53. package/dist/reverse.mjs +1 -1
  54. package/dist/{signed-4h2BnlWx.mjs → signed-B2Aa3zMg.mjs} +10 -31
  55. package/dist/signed-B2Aa3zMg.mjs.map +1 -0
  56. package/dist/signed.d.mts +13 -4
  57. package/dist/signed.d.mts.map +1 -1
  58. package/dist/signed.mjs +1 -1
  59. package/dist/{timestamp-Cg9nRfnK.mjs → timestamp-YPd58344.mjs} +10 -3
  60. package/dist/timestamp-YPd58344.mjs.map +1 -0
  61. package/dist/typeorm.d.mts +2 -2
  62. package/dist/typeorm.d.mts.map +1 -1
  63. package/dist/typeorm.mjs.map +1 -1
  64. package/dist/{types-g7CiQDyE.d.mts → types-wplmOgOK.d.mts} +20 -3
  65. package/dist/types-wplmOgOK.d.mts.map +1 -0
  66. package/dist/{wrapped-BQ-lNECo.mjs → wrapped-BjmVzuYc.mjs} +17 -75
  67. package/dist/wrapped-BjmVzuYc.mjs.map +1 -0
  68. package/dist/wrapped.d.mts +31 -5
  69. package/dist/wrapped.d.mts.map +1 -1
  70. package/dist/wrapped.mjs +1 -1
  71. package/package.json +80 -27
  72. package/dist/digest-CknNw2wa.mjs.map +0 -1
  73. package/dist/error-JIPylU_E.d.mts.map +0 -1
  74. package/dist/key-material-f29JIyrz.mjs.map +0 -1
  75. package/dist/opaque-BQVNoIIh.mjs.map +0 -1
  76. package/dist/reverse-DsPd7Lco.mjs.map +0 -1
  77. package/dist/signed-4h2BnlWx.mjs.map +0 -1
  78. package/dist/timestamp-Cg9nRfnK.mjs.map +0 -1
  79. package/dist/types-g7CiQDyE.d.mts.map +0 -1
  80. package/dist/wrapped-BQ-lNECo.mjs.map +0 -1
package/README.md CHANGED
@@ -44,8 +44,11 @@ JSON Schema.
44
44
  ## Choosing a codec
45
45
 
46
46
  All six codecs share the same `<brand>_<26 chars>` wire shape but make different
47
- trade-offs. They are wire-indistinguishable, so codec choice is a per-brand
48
- commitment.
47
+ trade-offs. They are wire-indistinguishable `safeParse`, `is`, and `parse`
48
+ cannot distinguish an Opaque Timestamp ID from a Timestamp ID at runtime.
49
+ Cross-codec confusion is undetectable by the library; the consumer is
50
+ responsible for routing a given ID to the correct codec for the brand. Codec
51
+ choice is therefore a per-brand commitment.
49
52
 
50
53
  | Codec | Import | Sort direction | Key required | Timestamp extractable |
51
54
  | ----------------- | -------------------- | ------------------------- | ------------------ | -------------------------- |
@@ -89,6 +92,33 @@ it slots into Zod, Valibot, ArkType, tRPC, and any validator-aware library.
89
92
  known creation time can compute the epoch offset. Use the Opaque Timestamp
90
93
  codec to hide creation time per-ID.
91
94
 
95
+ ## API surface
96
+
97
+ Exports from the main `@smonn/ids` entry point only. Codec-specific subpath
98
+ exports (`@smonn/ids/reverse`, `@smonn/ids/opaque`, `@smonn/ids/signed`,
99
+ `@smonn/ids/wrapped`, `@smonn/ids/digest`) and adapter subpaths are not listed
100
+ here.
101
+
102
+ ### Types
103
+
104
+ - `Id<Brand>` — Canonical branded ID string for `Brand`; produced by `generate()` and `safeParse()`.
105
+ - `ParseError` — Parse failure reason string (`"not_string"`, `"invalid_prefix"`, or `"invalid_base32"`) returned by `safeParse()`.
106
+ - `ParseResult<Brand>` — Discriminated union returned by `safeParse()`: `{ ok: true; id: Id<Brand> }` or `{ ok: false; error: ParseError }`.
107
+ - `JsonSchema` — Shape of the object returned by a codec's `toJsonSchema()`.
108
+ - `IdsErrorCode` — String-literal union of the eleven stable error codes carried by `IdsError`.
109
+ - `TimestampCodec<Brand>` — Interface of a brand-scoped Timestamp codec instance returned by `createTimestampId()`.
110
+ - `TimestampOptions` — Construction options for `createTimestampId()`: `now`, `rng`, and `allowDuplicateBrand`.
111
+ - `ValidBrand<S>` — Compile-time validation that `S` is a well-formed brand (three lowercase `a–z` characters); intersect it with a codec constructor's brand parameter (`brand: Brand & ValidBrand<Brand>`) to reject malformed brands at the type level.
112
+
113
+ ### Classes
114
+
115
+ - `IdsError` — Single error class thrown by caller-reachable failures; carries a stable `code: IdsErrorCode`. Use `isIdsError()` rather than `instanceof` to detect across realms.
116
+
117
+ ### Functions
118
+
119
+ - `isIdsError(value)` — Type guard for `IdsError`; uses an internal brand to survive ESM/CJS dual-package duplication where bare `instanceof` fails.
120
+ - `createTimestampId(brand, options?)` — Creates a Timestamp codec for `brand` (three lowercase `a–z` characters).
121
+
92
122
  ## Links
93
123
 
94
124
  - **[Documentation](https://ids.smonn.se)** — full guides, API reference, and playground
@@ -1,4 +1,4 @@
1
- import { i as ParseResult } from "./types-g7CiQDyE.mjs";
1
+ import { i as ParseResult } from "./types-wplmOgOK.mjs";
2
2
 
3
3
  //#region src/adapters/adapter-types.d.ts
4
4
  /** Discriminated failure value passed to `onError` and emitted to the framework's error handler. */
@@ -17,4 +17,4 @@ type IdCodec<Brand extends string> = {
17
17
  type IdColumnCodec<Brand extends string> = IdCodec<Brand>;
18
18
  //#endregion
19
19
  export { IdColumnCodec as n, IdParamFailure as r, IdCodec as t };
20
- //# sourceMappingURL=adapter-types-CdYJM6Sf.d.mts.map
20
+ //# sourceMappingURL=adapter-types-CIc-4O-P.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter-types-CdYJM6Sf.d.mts","names":[],"sources":["../src/adapters/adapter-types.ts"],"mappings":";;;;KAIY,cAAA;EAAA,SACG,MAAA;EAAA,SAAmC,MAAA;AAAA;EAAA,SACnC,MAAA;EAAA,SAA8B,MAAA;AAAA;;KAGjC,OAAA;EACV,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;AADzC;AAAA,KAKY,aAAA,yBAAsC,OAAA,CAAQ,KAAA"}
1
+ {"version":3,"file":"adapter-types-CIc-4O-P.d.mts","names":[],"sources":["../src/adapters/adapter-types.ts"],"mappings":";;;;KAIY,cAAA;EAAA,SACG,MAAA;EAAA,SAAmC,MAAA;AAAA;EAAA,SACnC,MAAA;EAAA,SAA8B,MAAA;AAAA;;KAGjC,OAAA;EACV,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;AADzC;AAAA,KAKY,aAAA,yBAAsC,OAAA,CAAQ,KAAA"}
package/dist/cli.mjs CHANGED
@@ -1,12 +1,17 @@
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-YPd58344.mjs";
4
+ import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-D7y5cgzT.mjs";
5
+ import { t as createReverseTimestampId } from "./reverse-DrAofYWV.mjs";
6
+ import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-B2Aa3zMg.mjs";
7
+ import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-BjmVzuYc.mjs";
8
+ import { i as importDigestKey, n as decodeDigestKey, r as encodeDigestKey, t as createDigestId } from "./digest-Drnof-l_.mjs";
9
9
  //#region src/cli/key-io.ts
10
+ function isLoadKeyError(value) {
11
+ if (typeof value !== "object" || value === null) return false;
12
+ const kind = value.kind;
13
+ return kind === "missing" || kind === "import-failure";
14
+ }
10
15
  function isKeyFormatError(result) {
11
16
  return result !== "hex" && result !== "base64url";
12
17
  }
@@ -32,11 +37,17 @@ function parseKeyFormat(values, opts, facet) {
32
37
  }
33
38
  async function loadKey(opts, format, facet) {
34
39
  const raw = (opts.env ?? process.env)[facet.envVar];
35
- if (raw === void 0 || raw === "") return `missing ${facet.envVar} environment variable`;
40
+ if (raw === void 0 || raw === "") return {
41
+ kind: "missing",
42
+ message: `missing ${facet.envVar} environment variable`
43
+ };
36
44
  try {
37
45
  return await facet.import(facet.decode(raw, format));
38
46
  } catch (err) {
39
- return err.message;
47
+ return {
48
+ kind: "import-failure",
49
+ message: err.message
50
+ };
40
51
  }
41
52
  }
42
53
  //#endregion
@@ -235,8 +246,20 @@ function unit(n, name) {
235
246
  }
236
247
  //#endregion
237
248
  //#region src/cli/variants.ts
249
+ function standardValidate(codec, input) {
250
+ const result = codec["~standard"].validate(input);
251
+ if (result.issues) return { issue: result.issues[0].message };
252
+ return { value: result.value };
253
+ }
238
254
  const timestampVariant = {
239
- inspectMode: "readable",
255
+ inspect: {
256
+ mode: "readable",
257
+ 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",
258
+ validate: standardValidate,
259
+ extractTimestamp(codec, id) {
260
+ return codec.extractTimestamp(id);
261
+ }
262
+ },
240
263
  construct(brand, opts) {
241
264
  try {
242
265
  return createTimestampId(brand, codecOpts(opts));
@@ -254,7 +277,14 @@ const opaqueVariant = {
254
277
  decode: decodeOpaqueKey,
255
278
  import: importOpaqueKey
256
279
  },
257
- inspectMode: "keyed-readable",
280
+ inspect: {
281
+ mode: "keyed-readable",
282
+ note: "note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp",
283
+ validate: standardValidate,
284
+ extractTimestamp(codec, id) {
285
+ return codec.extractTimestamp(id);
286
+ }
287
+ },
258
288
  construct(brand, opts, key) {
259
289
  try {
260
290
  return createOpaqueTimestampId(brand, {
@@ -268,7 +298,14 @@ const opaqueVariant = {
268
298
  };
269
299
  const reverseVariant = {
270
300
  flag: "--reverse",
271
- inspectMode: "readable",
301
+ inspect: {
302
+ mode: "readable",
303
+ 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",
304
+ validate: standardValidate,
305
+ extractTimestamp(codec, id) {
306
+ return codec.extractTimestamp(id);
307
+ }
308
+ },
272
309
  construct(brand, opts) {
273
310
  try {
274
311
  return createReverseTimestampId(brand, codecOpts(opts));
@@ -286,7 +323,13 @@ const wrappedVariant = {
286
323
  decode: decodeWrappingKey,
287
324
  import: importWrappingKey
288
325
  },
289
- inspectMode: "unwrap",
326
+ inspect: {
327
+ mode: "unwrap",
328
+ validate: standardValidate,
329
+ unwrap(codec, id) {
330
+ return codec.unwrap(id);
331
+ }
332
+ },
290
333
  extraFlags: ["--kind"],
291
334
  construct(brand, _opts, key, values) {
292
335
  const kind = parseKind(values ?? /* @__PURE__ */ new Map());
@@ -312,7 +355,12 @@ const signedVariant = {
312
355
  decode: decodeSigningKey,
313
356
  import: importSigningKey
314
357
  },
315
- inspectMode: "verify",
358
+ inspect: {
359
+ mode: "verify",
360
+ safeVerify(codec, id) {
361
+ return codec.safeVerify(id);
362
+ }
363
+ },
316
364
  construct(brand, opts, key) {
317
365
  try {
318
366
  return createSignedTimestampId(brand, {
@@ -333,7 +381,7 @@ const digestVariant = {
333
381
  decode: decodeDigestKey,
334
382
  import: importDigestKey
335
383
  },
336
- inspectMode: "unsupported",
384
+ inspect: { mode: "unsupported" },
337
385
  extraFlags: ["--ns"],
338
386
  construct(brand, opts, key, values) {
339
387
  const ns = parseNs(values ?? /* @__PURE__ */ new Map());
@@ -347,8 +395,9 @@ const digestVariant = {
347
395
  });
348
396
  return {
349
397
  safeParse: (v) => codec.safeParse(v),
350
- generate() {
351
- return (opts.readStdin ?? (() => Promise.resolve("")))().then((material) => codec.digest(material));
398
+ async generate() {
399
+ const material = await (opts.readStdin ?? (() => Promise.resolve("")))();
400
+ return codec.digest(material);
352
401
  }
353
402
  };
354
403
  } catch (err) {
@@ -394,6 +443,11 @@ const keygenPolicy = {
394
443
  };
395
444
  //#endregion
396
445
  //#region src/cli/dispatch.ts
446
+ function isCodecError(v) {
447
+ if (typeof v !== "object" || v === null) return false;
448
+ const kind = v.kind;
449
+ return (kind === "usage" || kind === "runtime") && "message" in v;
450
+ }
397
451
  function deriveAllowedFlags(policy) {
398
452
  const flags = new Set(policy.intrinsicFlags);
399
453
  let hasKeyed = policy.default.key !== void 0;
@@ -415,12 +469,106 @@ async function buildCodec(variant, brand, values, opts) {
415
469
  let key;
416
470
  if (variant.key !== void 0) {
417
471
  const format = parseKeyFormat(values, opts, variant.key);
418
- if (isKeyFormatError(format)) return format;
472
+ if (isKeyFormatError(format)) return {
473
+ kind: "usage",
474
+ message: format
475
+ };
419
476
  const keyResult = await loadKey(opts, format, variant.key);
420
- if (typeof keyResult === "string") return keyResult;
477
+ if (isLoadKeyError(keyResult)) return {
478
+ kind: keyResult.kind === "missing" ? "usage" : "runtime",
479
+ message: keyResult.message
480
+ };
421
481
  key = keyResult;
422
482
  }
423
- return variant.construct(brand, opts, key, values);
483
+ const codecOrError = variant.construct(brand, opts, key, values);
484
+ if (typeof codecOrError === "string") return {
485
+ kind: codecOrError.startsWith("--") ? "usage" : "runtime",
486
+ message: codecOrError
487
+ };
488
+ return codecOrError;
489
+ }
490
+ //#endregion
491
+ //#region src/cli/usage.ts
492
+ function usageInspect() {
493
+ return [
494
+ "Usage: ids inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
495
+ "",
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
+ ""
505
+ ].join("\n");
506
+ }
507
+ function usageGenerate() {
508
+ return [
509
+ `Usage: ids generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--key-format hex|base64url]`,
510
+ "",
511
+ ` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
512
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
513
+ " --reverse mints Reverse Timestamp IDs (newest-first sort order).",
514
+ " --signed mints Signed Timestamp IDs; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
515
+ " --digest mints a deterministic Digest ID from material read on stdin.",
516
+ " --ns <ns> is required: the namespace domain separator (non-secret, non-empty).",
517
+ " Reads the digest key from IDS_DIGEST_KEY (hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
518
+ " Same material + ns + key always produces the same ID. Digest IDs are one-way.",
519
+ " --count N > 1 is rejected: same material always produces the same ID.",
520
+ ""
521
+ ].join("\n");
522
+ }
523
+ function usageKeygen() {
524
+ return [
525
+ "Usage: ids keygen, k [--wrapped] [--signed] [--digest] [--bits 128|192|256] [--key-format hex|base64url]",
526
+ "",
527
+ " Emit a random key for importOpaqueKey, importWrappingKey, importSigningKey, or importDigestKey (stdout only).",
528
+ " --wrapped emits a wrapping key for importWrappingKey instead (IDS_WRAPPING_KEY).",
529
+ " --signed emits a signing key for importSigningKey instead (IDS_SIGNING_KEY; hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
530
+ " --digest emits a digest key for importDigestKey instead (IDS_DIGEST_KEY; hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
531
+ ""
532
+ ].join("\n");
533
+ }
534
+ function usage() {
535
+ return [
536
+ "Usage: ids <subcommand> [args]",
537
+ "",
538
+ "Subcommands:",
539
+ " inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
540
+ " Decode an ID and print brand, timestamp (or lookup key), and canonical form.",
541
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
542
+ " --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
543
+ " --kind is required with --wrapped: u32, i32, u64, or i64.",
544
+ " --reverse decodes a Reverse Timestamp ID (newest-first sort order).",
545
+ " --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
546
+ " Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
547
+ " Note: --digest is not supported for inspect (Digest IDs are one-way; there is no reverse path).",
548
+ " generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--key-format hex|base64url]",
549
+ ` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
550
+ " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
551
+ " --reverse mints Reverse Timestamp IDs (newest-first sort order).",
552
+ " --signed mints Signed Timestamp IDs; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
553
+ " --digest mints a deterministic Digest ID from material read on stdin.",
554
+ " --ns <ns> is required: the namespace domain separator (non-secret, non-empty).",
555
+ " Reads the digest key from IDS_DIGEST_KEY (hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
556
+ " Same material + ns + key always produces the same ID. Digest IDs are one-way.",
557
+ " --count N > 1 is rejected: same material always produces the same ID.",
558
+ " keygen, k [--wrapped] [--signed] [--digest] [--bits 128|192|256] [--key-format hex|base64url]",
559
+ " Emit a random key for importOpaqueKey, importWrappingKey, importSigningKey, or importDigestKey (key on stdout; warning on stderr).",
560
+ " Safe handling: redirect stdout to a 0600 file (e.g. ids keygen > key.hex && chmod 0600 key.hex);",
561
+ " do not let the key appear in shell history or CI logs. A warning is printed to stderr on every run.",
562
+ " --wrapped emits a wrapping key for importWrappingKey instead (IDS_WRAPPING_KEY).",
563
+ " --signed emits a signing key for importSigningKey instead (IDS_SIGNING_KEY; hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
564
+ " --digest emits a digest key for importDigestKey instead (IDS_DIGEST_KEY; hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
565
+ "",
566
+ "Exit codes:",
567
+ " 0 Success",
568
+ " 1 Runtime/operational error (codec failure, bad key material, verification failure)",
569
+ " 2 Usage/argument error (unknown subcommand, unrecognised flag, bad flag value, missing required arg)",
570
+ ""
571
+ ].join("\n");
424
572
  }
425
573
  //#endregion
426
574
  //#region src/cli/commands/generate.ts
@@ -437,131 +585,106 @@ function readProcessStdin() {
437
585
  return stdinCache;
438
586
  }
439
587
  async function runGenerate(args, opts) {
588
+ if (args.includes("--help") || args.includes("-h")) {
589
+ opts.stdout(usageGenerate());
590
+ return 0;
591
+ }
440
592
  const allowedFlags = deriveAllowedFlags(generatePolicy);
441
593
  const selectorFlags = new Set(generatePolicy.selectable.map((v) => v.flag).filter((f) => f !== void 0));
442
594
  const { flags, values, positionals, errors } = splitFlags(args, new Set([...allowedFlags].filter((f) => !selectorFlags.has(f))));
443
595
  const unsupported = unsupportedFlagForCommand("generate", flags, allowedFlags);
444
596
  if (unsupported !== void 0) {
445
597
  opts.stderr(unsupported + "\n");
446
- return 1;
598
+ return 2;
447
599
  }
448
600
  if (errors[0] !== void 0) {
449
601
  opts.stderr(errors[0] + "\n");
450
- return 1;
602
+ return 2;
451
603
  }
452
604
  const extra = positionals[1];
453
605
  if (extra !== void 0) {
454
606
  opts.stderr(`unexpected argument: ${extra}\n`);
455
- return 1;
607
+ return 2;
456
608
  }
457
609
  const [brand] = positionals;
458
610
  const count = parseCount(values);
459
611
  if (typeof count === "string") {
460
612
  opts.stderr(count + "\n");
461
- return 1;
613
+ return 2;
462
614
  }
463
615
  const variant = resolveVariant(generatePolicy, flags);
464
616
  if (typeof variant === "string") {
465
617
  opts.stderr(variant + "\n");
466
- return 1;
618
+ return 2;
467
619
  }
468
620
  if (variant.key === void 0 && flags.has("--key-format")) {
469
621
  opts.stderr("--key-format requires --opaque, --signed, or --digest\n");
470
- return 1;
622
+ return 2;
471
623
  }
472
624
  if (flags.has("--digest") && count > 1) {
473
625
  opts.stderr("--count N > 1 is rejected with --digest: same material always produces the same ID\n");
474
- return 1;
626
+ return 2;
475
627
  }
476
628
  const optsWithStdin = {
477
629
  ...opts,
478
630
  readStdin: opts.readStdin ?? readProcessStdin
479
631
  };
480
632
  const codec = await buildCodec(variant, brand ?? "", values, optsWithStdin);
481
- if (typeof codec === "string") {
482
- opts.stderr(codec + "\n");
483
- return 1;
633
+ if (isCodecError(codec)) {
634
+ opts.stderr(codec.message + "\n");
635
+ return codec.kind === "usage" ? 2 : 1;
484
636
  }
485
637
  for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
486
638
  return 0;
487
639
  }
488
640
  //#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
641
  //#region src/cli/commands/inspect.ts
524
642
  async function runInspect(args, opts) {
643
+ if (args.includes("--help") || args.includes("-h")) {
644
+ opts.stdout(usageInspect());
645
+ return 0;
646
+ }
525
647
  const allowedFlags = deriveAllowedFlags(inspectPolicy);
526
648
  const selectorFlags = new Set(inspectPolicy.selectable.map((v) => v.flag).filter((f) => f !== void 0));
527
649
  const { flags, values, positionals, errors } = splitFlags(args, new Set([...allowedFlags].filter((f) => !selectorFlags.has(f))));
528
650
  const unsupported = unsupportedFlagForCommand("inspect", flags, allowedFlags);
529
651
  if (unsupported !== void 0) {
530
652
  opts.stderr(unsupported + "\n");
531
- return 1;
653
+ return 2;
532
654
  }
533
655
  if (errors[0] !== void 0) {
534
656
  opts.stderr(errors[0] + "\n");
535
- return 1;
657
+ return 2;
536
658
  }
537
659
  const [input] = positionals;
538
660
  if (input === void 0) {
539
- opts.stderr(usage());
540
- return 1;
661
+ opts.stderr(usageInspect());
662
+ return 2;
541
663
  }
542
664
  const extra = positionals[1];
543
665
  if (extra !== void 0) {
544
666
  opts.stderr(`unexpected argument: ${extra}\n`);
545
- return 1;
667
+ return 2;
546
668
  }
547
669
  const variant = resolveVariant(inspectPolicy, flags);
548
670
  if (typeof variant === "string") {
549
671
  opts.stderr(variant + "\n");
550
- return 1;
672
+ return 2;
551
673
  }
552
674
  if (variant.key === void 0 && flags.has("--key-format")) {
553
675
  opts.stderr("--key-format requires --opaque, --wrapped, or --signed\n");
554
- return 1;
676
+ return 2;
555
677
  }
556
678
  const brand = input.slice(0, 3).toLowerCase();
679
+ const cap = variant.inspect;
557
680
  let verifyTimestamp;
558
681
  let verifyCanonical;
559
682
  let verifyNowMs;
560
- if (variant.inspectMode === "verify") {
683
+ if (cap.mode === "verify") {
561
684
  const fmtCheck = parseKeyFormat(values, opts, variant.key);
562
685
  if (isKeyFormatError(fmtCheck)) {
563
686
  opts.stderr(fmtCheck + "\n");
564
- return 1;
687
+ return 2;
565
688
  }
566
689
  let tsCodec;
567
690
  try {
@@ -580,32 +703,36 @@ async function runInspect(args, opts) {
580
703
  verifyNowMs = (opts.now ?? Date.now)();
581
704
  }
582
705
  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;
706
+ if (isCodecError(codecOrError)) {
707
+ if (cap.mode === "verify") {
708
+ opts.stdout(formatSignedInspectOutput({
709
+ brand,
710
+ timestamp: verifyTimestamp,
711
+ canonical: verifyCanonical,
712
+ input,
713
+ nowMs: verifyNowMs,
714
+ verification: "unavailable"
715
+ }));
716
+ opts.stderr(codecOrError.message + "\n");
717
+ return 1;
718
+ }
719
+ opts.stderr(codecOrError.message + "\n");
720
+ return codecOrError.kind === "usage" ? 2 : 1;
594
721
  }
595
722
  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");
723
+ if (cap.mode !== "verify" && cap.mode !== "unsupported") {
724
+ const parsed = cap.validate(codecOrError, input);
725
+ if ("issue" in parsed) {
726
+ opts.stderr(parsed.issue + "\n");
600
727
  return 1;
601
728
  }
602
- canonical = validation.value;
729
+ canonical = parsed.value;
603
730
  }
604
- switch (variant.inspectMode) {
731
+ switch (cap.mode) {
605
732
  case "readable": {
606
- const timestamp = codecOrError.extractTimestamp(canonical);
733
+ const timestamp = cap.extractTimestamp(codecOrError, canonical);
607
734
  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");
735
+ opts.stderr(cap.note + "\n");
609
736
  opts.stdout(formatInspectOutput({
610
737
  brand,
611
738
  timestamp,
@@ -616,9 +743,9 @@ async function runInspect(args, opts) {
616
743
  return 0;
617
744
  }
618
745
  case "keyed-readable": {
619
- const timestamp = await codecOrError.extractTimestamp(canonical);
746
+ const timestamp = await cap.extractTimestamp(codecOrError, canonical);
620
747
  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");
748
+ opts.stderr(cap.note + "\n");
622
749
  opts.stdout(formatInspectOutput({
623
750
  brand,
624
751
  timestamp,
@@ -631,7 +758,7 @@ async function runInspect(args, opts) {
631
758
  case "unwrap": {
632
759
  let lookupKey;
633
760
  try {
634
- lookupKey = await codecOrError.unwrap(canonical);
761
+ lookupKey = await cap.unwrap(codecOrError, canonical);
635
762
  } catch (err) {
636
763
  opts.stderr(formatCliError(err) + "\n");
637
764
  return 1;
@@ -645,7 +772,7 @@ async function runInspect(args, opts) {
645
772
  return 0;
646
773
  }
647
774
  case "verify": {
648
- const verifyResult = await codecOrError.safeVerify(input);
775
+ const verifyResult = await cap.safeVerify(codecOrError, input);
649
776
  if (!verifyResult.ok) {
650
777
  /* v8 ignore next 4 -- defensive: both codecs share the same wire parse so ParseError
651
778
  is unreachable after the createTimestampId pre-validation above passes */
@@ -684,38 +811,42 @@ async function runInspect(args, opts) {
684
811
  }
685
812
  //#endregion
686
813
  //#region src/cli/commands/keygen.ts
687
- function runKeygen(args, opts) {
814
+ async function runKeygen(args, opts) {
815
+ if (args.includes("--help") || args.includes("-h")) {
816
+ opts.stdout(usageKeygen());
817
+ return Promise.resolve(0);
818
+ }
688
819
  const allowedFlags = deriveAllowedFlags(keygenPolicy);
689
820
  const variantExtraFlags = new Set(keygenPolicy.selectable.flatMap((v) => v.extraFlags ?? []));
690
821
  const { flags, values, positionals, errors } = splitFlags(args, allowedFlags);
691
822
  const unsupported = unsupportedFlagForCommand("keygen", flags, new Set([...allowedFlags].filter((f) => !variantExtraFlags.has(f))));
692
823
  if (unsupported !== void 0) {
693
824
  opts.stderr(unsupported + "\n");
694
- return Promise.resolve(1);
825
+ return Promise.resolve(2);
695
826
  }
696
827
  if (errors[0] !== void 0) {
697
828
  opts.stderr(errors[0] + "\n");
698
- return Promise.resolve(1);
829
+ return Promise.resolve(2);
699
830
  }
700
831
  const extra = positionals[0];
701
832
  if (extra !== void 0) {
702
833
  opts.stderr(`unexpected argument: ${extra}\n`);
703
- return Promise.resolve(1);
834
+ return Promise.resolve(2);
704
835
  }
705
836
  const variant = resolveVariant(keygenPolicy, flags);
706
837
  if (typeof variant === "string") {
707
838
  opts.stderr(variant + "\n");
708
- return Promise.resolve(1);
839
+ return Promise.resolve(2);
709
840
  }
710
841
  const bits = parseBits(values);
711
842
  if (typeof bits === "string") {
712
843
  opts.stderr(bits + "\n");
713
- return Promise.resolve(1);
844
+ return Promise.resolve(2);
714
845
  }
715
846
  const format = parseKeyFormatFromFlag(values);
716
847
  if (isKeyFormatError(format)) {
717
848
  opts.stderr(format + "\n");
718
- return Promise.resolve(1);
849
+ return Promise.resolve(2);
719
850
  }
720
851
  /* v8 ignore next 4 -- defensive guard; all keygenPolicy variants have key defined */
721
852
  if (variant.key === void 0) {
@@ -724,6 +855,7 @@ function runKeygen(args, opts) {
724
855
  }
725
856
  const bytes = new Uint8Array(bits / 8);
726
857
  crypto.getRandomValues(bytes);
858
+ opts.stderr("Warning: secret key material — redirect to a file (chmod 0600) and avoid shell history.\n");
727
859
  opts.stdout(variant.key.encode(bytes, format) + "\n");
728
860
  return Promise.resolve(0);
729
861
  }
@@ -752,7 +884,7 @@ async function run(opts) {
752
884
  return 0;
753
885
  }
754
886
  opts.stderr(usage());
755
- return 1;
887
+ return 2;
756
888
  }
757
889
  //#endregion
758
890
  //#region bin/cli.ts