@smonn/ids 0.13.1 → 0.14.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 (60) hide show
  1. package/README.md +17 -5
  2. package/dist/{adapter-types-CIc-4O-P.d.mts → adapter-types-Bia_w9sg.d.mts} +2 -2
  3. package/dist/{adapter-types-CIc-4O-P.d.mts.map → adapter-types-Bia_w9sg.d.mts.map} +1 -1
  4. package/dist/cli.mjs +82 -15
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{codec-shell-C2NKQEx2.mjs → codec-shell-BRZkuQeP.mjs} +89 -7
  7. package/dist/codec-shell-BRZkuQeP.mjs.map +1 -0
  8. package/dist/{digest-DsGeXfk3.mjs → digest-CLJEGBxo.mjs} +7 -4
  9. package/dist/{digest-DsGeXfk3.mjs.map → digest-CLJEGBxo.mjs.map} +1 -1
  10. package/dist/digest.d.mts +19 -2
  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/{error-Dqyho9vp.d.mts → error-CifcKKOG.d.mts} +2 -2
  15. package/dist/{error-Dqyho9vp.d.mts.map → error-CifcKKOG.d.mts.map} +1 -1
  16. package/dist/express.d.mts +2 -2
  17. package/dist/fastify.d.mts +2 -2
  18. package/dist/graphql.d.mts +2 -2
  19. package/dist/hono.d.mts +2 -2
  20. package/dist/index.d.mts +19 -2
  21. package/dist/index.d.mts.map +1 -1
  22. package/dist/index.mjs +1 -1
  23. package/dist/{key-material-DvjACe89.mjs → key-material-1wOKJ1o-.mjs} +2 -2
  24. package/dist/{key-material-DvjACe89.mjs.map → key-material-1wOKJ1o-.mjs.map} +1 -1
  25. package/dist/kysely.d.mts +3 -3
  26. package/dist/mikro-orm.d.mts +3 -3
  27. package/dist/nestjs.d.mts +2 -2
  28. package/dist/{opaque-BW3Uzeeb.mjs → opaque-COAcIIY4.mjs} +14 -5
  29. package/dist/opaque-COAcIIY4.mjs.map +1 -0
  30. package/dist/opaque.d.mts +26 -2
  31. package/dist/opaque.d.mts.map +1 -1
  32. package/dist/opaque.mjs +1 -1
  33. package/dist/prisma.d.mts +3 -3
  34. package/dist/{reverse-BW8g_cln.mjs → reverse-CT-El3hi.mjs} +7 -4
  35. package/dist/{reverse-BW8g_cln.mjs.map → reverse-CT-El3hi.mjs.map} +1 -1
  36. package/dist/reverse.d.mts +19 -2
  37. package/dist/reverse.d.mts.map +1 -1
  38. package/dist/reverse.mjs +1 -1
  39. package/dist/{rng-BHFxX1Fc.mjs → rng-6GyNT4zS.mjs} +2 -2
  40. package/dist/{rng-BHFxX1Fc.mjs.map → rng-6GyNT4zS.mjs.map} +1 -1
  41. package/dist/{signed-BTz3ZFYE.mjs → signed-Dkdteu1y.mjs} +8 -5
  42. package/dist/{signed-BTz3ZFYE.mjs.map → signed-Dkdteu1y.mjs.map} +1 -1
  43. package/dist/signed.d.mts +19 -2
  44. package/dist/signed.d.mts.map +1 -1
  45. package/dist/signed.mjs +1 -1
  46. package/dist/{timestamp-CleAIdZI.mjs → timestamp-RXXwHfHO.mjs} +7 -4
  47. package/dist/{timestamp-CleAIdZI.mjs.map → timestamp-RXXwHfHO.mjs.map} +1 -1
  48. package/dist/typeorm.d.mts +2 -2
  49. package/dist/{types-wplmOgOK.d.mts → types-hGBnCpJj.d.mts} +3 -3
  50. package/dist/{types-wplmOgOK.d.mts.map → types-hGBnCpJj.d.mts.map} +1 -1
  51. package/dist/{wrapped-DPlsv1x-.mjs → wrapped-Oj2hC1vB.mjs} +15 -4
  52. package/dist/wrapped-Oj2hC1vB.mjs.map +1 -0
  53. package/dist/wrapped.d.mts +27 -2
  54. package/dist/wrapped.d.mts.map +1 -1
  55. package/dist/wrapped.mjs +1 -1
  56. package/package.json +3 -2
  57. package/spec/vectors.json +97 -0
  58. package/dist/codec-shell-C2NKQEx2.mjs.map +0 -1
  59. package/dist/opaque-BW3Uzeeb.mjs.map +0 -1
  60. package/dist/wrapped-DPlsv1x-.mjs.map +0 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@ Public-facing branded IDs for TypeScript apps. Type-safe, sortable, and codec-pl
8
8
  pnpm add @smonn/ids
9
9
  ```
10
10
 
11
- Each ID looks like `usr_01h7b3k9rqxn4cw3p9r8t2sgkw`: a three-letter brand, an underscore, then 26 Crockford base32 characters of payload. The default Timestamp codec encodes a 48-bit millisecond Unix timestamp followed by 80 random bits — the same byte layout as a [ULID](https://github.com/ulid/spec).
11
+ Each ID looks like `usr_06f80z92d2dbsqqg28t5cy4tqg`: a three-letter brand, an underscore, then 26 Crockford base32 characters of payload. The default Timestamp codec encodes a 48-bit millisecond Unix timestamp followed by 80 random bits — the same byte layout as a [ULID](https://github.com/ulid/spec).
12
12
 
13
13
  ## Quickstart
14
14
 
@@ -18,7 +18,7 @@ import { type Id, createTimestampId } from "@smonn/ids";
18
18
  const users = createTimestampId("usr");
19
19
 
20
20
  // Generate — sortable by creation time via ORDER BY id
21
- const id = users.generate(); // "usr_01h7b3k9rqxn4cw3p9r8t2sgkw"
21
+ const id = users.generate(); // "usr_06f80z92d2dbsqqg28t5cy4tqg"
22
22
 
23
23
  // Branded: Id<"usr"> and Id<"org"> are not interchangeable
24
24
  function loadUser(id: Id<"usr">) {
@@ -26,9 +26,9 @@ function loadUser(id: Id<"usr">) {
26
26
  }
27
27
 
28
28
  // Validate untrusted input — lenient in, canonical out
29
- const r = users.safeParse("USR_01H7B3K9RQXN1CW3P9R8T2SGKW");
29
+ const r = users.safeParse("USR_06F80Z92D2DBSQQG28T5CY4TQG");
30
30
  if (r.ok) {
31
- r.id; // "usr_01h7b3k9rqxn1cw3p9r8t2sgkw" as Id<"usr">
31
+ r.id; // "usr_06f80z92d2dbsqqg28t5cy4tqg" as Id<"usr">
32
32
  }
33
33
  ```
34
34
 
@@ -68,10 +68,13 @@ Framework and ORM adapters ship as optional subpath exports (each requires its o
68
68
 
69
69
  Every codec also implements [Standard Schema v1](https://standardschema.dev/), so it slots into Zod, Valibot, ArkType, tRPC, and any validator-aware library.
70
70
 
71
+ **Native `uuid` column storage:** an `Id<Brand>` can be persisted into a native `uuid` column via `codec.toUUID(id)` and read back as a branded ID via `codec.fromUUID(value)` or `codec.safeFromUUID(value)` — a lossless round-trip useful when migrating off UUID primary keys while keeping existing column types and indexes.
72
+
71
73
  ## What this is **not** for
72
74
 
73
75
  - **Internal surrogate primary keys.** If nobody outside your service sees the ID, the brand prefix and lenient parsing are dead weight. Use a `bigint` sequence.
74
76
  - **Wire-compatible ULIDs.** The byte layout is ULID-shaped, but the encoding is lowercase and brand-wrapped. Stock ULID parsers will reject these.
77
+ - **Spec-valid UUIDv7 output.** `toUUID` produces a **raw, unversioned** UUID — all 128 payload bits are preserved verbatim (lossless round-trip), which means the version/variant nibble positions hold real data, not `0x7`/`0b10`. It is **not** a spec-valid UUIDv7. Only the Timestamp codec happens to produce a UUID whose leading 48 bits are a real millisecond timestamp; only Timestamp and Reverse Timestamp produce time-sortable UUIDs. Importing a non-time-ordered UUID (e.g. a UUIDv4) into a timestamp-family codec via `fromUUID` yields a structurally valid `Id<Brand>` with a meaningless timestamp and random sort order — the same wire-indistinguishable contract that already governs codec variants.
75
78
  - **Distributed-trace / request-correlation IDs.** Use OpenTelemetry-format IDs.
76
79
  - **Hiding creation time with the Timestamp codec.** Anyone with one ID at a known creation time can compute the epoch offset. Use the Opaque Timestamp codec to hide creation time per-ID.
77
80
 
@@ -82,7 +85,7 @@ Exports from the main `@smonn/ids` entry point only. Codec-specific subpath expo
82
85
  ### Types
83
86
 
84
87
  - `Id<Brand>` — Canonical branded ID string for `Brand`; produced by `generate()` and `safeParse()`.
85
- - `ParseError` — Parse failure reason string (`"not_string"`, `"invalid_prefix"`, or `"invalid_base32"`) returned by `safeParse()`.
88
+ - `ParseError` — Parse failure reason string returned by `safeParse()` (`"not_string"`, `"invalid_prefix"`, or `"invalid_base32"`) and by `safeFromUUID()` (`"not_string"` or `"invalid_uuid"`).
86
89
  - `ParseResult<Brand>` — Discriminated union returned by `safeParse()`: `{ ok: true; id: Id<Brand> }` or `{ ok: false; error: ParseError }`.
87
90
  - `JsonSchema` — Shape of the object returned by a codec's `toJsonSchema()`.
88
91
  - `IdsErrorCode` — String-literal union of the eleven stable error codes carried by `IdsError`.
@@ -99,9 +102,18 @@ Exports from the main `@smonn/ids` entry point only. Codec-specific subpath expo
99
102
  - `isIdsError(value)` — Type guard for `IdsError`; uses an internal brand to survive ESM/CJS dual-package duplication where bare `instanceof` fails.
100
103
  - `createTimestampId(brand, options?)` — Creates a Timestamp codec for `brand` (three lowercase `a–z` characters).
101
104
 
105
+ ### Shared codec methods (all variants)
106
+
107
+ Every codec instance exposes the following UUID interop methods in addition to the codec-specific ones documented on the [full docs site](https://ids.smonn.se):
108
+
109
+ - `toUUID(id)` — Takes a trusted `Id<Brand>`, returns the 16-byte payload reinterpreted as a canonical lowercase-hyphenated UUID `string`. Total — cannot fail. The brand is shed; the output is a plain `string`, not a branded type.
110
+ - `fromUUID(value)` — Takes an untrusted `string`, returns `Id<Brand>`. Throws `IdsError` (`code: "invalid_id"`) with a `ParseError` on `cause` (`"invalid_uuid"`, or `"not_string"` for untyped JavaScript callers) on malformed input.
111
+ - `safeFromUUID(value)` — Takes `unknown`, returns `ParseResult<Brand>` (`{ ok: true; id }` or `{ ok: false; error: ParseError }`). Never throws.
112
+
102
113
  ## Links
103
114
 
104
115
  - **[Documentation](https://ids.smonn.se)** — full guides, API reference, and playground
116
+ - **[SPEC.md](./SPEC.md)** — descriptive wire-format specification
105
117
  - **[Design decisions](./docs/adr/)** — recorded ADRs
106
118
  - **[CONTEXT.md](./CONTEXT.md)** — glossary of the project's vocabulary
107
119
  - **[Contributing](./CONTRIBUTING.md)** · **[Security](./SECURITY.md)**
@@ -1,4 +1,4 @@
1
- import { i as ParseResult } from "./types-wplmOgOK.mjs";
1
+ import { i as ParseResult } from "./types-hGBnCpJj.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-CIc-4O-P.d.mts.map
20
+ //# sourceMappingURL=adapter-types-Bia_w9sg.d.mts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"adapter-types-Bia_w9sg.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,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as isIdsError } from "./error-Cp5qYZcv.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";
3
+ import { t as createTimestampId } from "./timestamp-RXXwHfHO.mjs";
4
+ import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-COAcIIY4.mjs";
5
+ import { t as createReverseTimestampId } from "./reverse-CT-El3hi.mjs";
6
+ import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-Dkdteu1y.mjs";
7
+ import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-Oj2hC1vB.mjs";
8
+ import { i as importDigestKey, n as decodeDigestKey, r as encodeDigestKey, t as createDigestId } from "./digest-CLJEGBxo.mjs";
9
9
  //#region src/cli/format.ts
10
10
  const invalidIdPrefix = "invalid_id: ";
11
11
  function formatCliError(err) {
@@ -17,6 +17,7 @@ function formatWrappedInspectOutput(result) {
17
17
  `brand: ${result.brand}`,
18
18
  `lookup-key: ${result.lookupKey.toString()}`,
19
19
  `canonical: ${result.canonical}`,
20
+ `uuid: ${result.uuid}`,
20
21
  `input: ${inputLine}`,
21
22
  ""
22
23
  ].join("\n");
@@ -26,7 +27,7 @@ function formatSignedInspectOutput(result) {
26
27
  const inputLine = describeInputForm(result.input, result.canonical);
27
28
  const lines = [`brand: ${result.brand}`, `timestamp: ${result.timestamp.toISOString()} (${relative})`];
28
29
  lines.push(`verification: ${result.verification}`);
29
- lines.push(`canonical: ${result.canonical}`, `input: ${inputLine}`, "");
30
+ lines.push(`canonical: ${result.canonical}`, `uuid: ${result.uuid}`, `input: ${inputLine}`, "");
30
31
  return lines.join("\n");
31
32
  }
32
33
  function formatInspectOutput(result) {
@@ -36,6 +37,7 @@ function formatInspectOutput(result) {
36
37
  `brand: ${result.brand}`,
37
38
  `timestamp: ${result.timestamp.toISOString()} (${relative})`,
38
39
  `canonical: ${result.canonical}`,
40
+ `uuid: ${result.uuid}`,
39
41
  `input: ${inputLine}`,
40
42
  ""
41
43
  ].join("\n");
@@ -203,7 +205,10 @@ const knownFlags = /* @__PURE__ */ new Set([
203
205
  "--key-format",
204
206
  "--count",
205
207
  "-c",
206
- "--bits"
208
+ "--bits",
209
+ "--uuid",
210
+ "--from-uuid",
211
+ "--brand"
207
212
  ]);
208
213
  function unsupportedFlagForCommand(command, flags, allowed) {
209
214
  for (const flag of flags) if (!allowed.has(flag)) return knownFlags.has(flag) ? `unsupported flag for ${command}: ${flag}` : `unsupported flag: ${flag}`;
@@ -396,6 +401,7 @@ const digestVariant = {
396
401
  });
397
402
  return {
398
403
  safeParse: (v) => codec.safeParse(v),
404
+ toUUID: (id) => codec.toUUID(id),
399
405
  async generate() {
400
406
  const material = await (opts.readStdin ?? (() => Promise.resolve("")))();
401
407
  return codec.digest(material);
@@ -421,7 +427,11 @@ const generatePolicy = {
421
427
  signedVariant,
422
428
  digestVariant
423
429
  ],
424
- intrinsicFlags: ["--count", "-c"]
430
+ intrinsicFlags: [
431
+ "--count",
432
+ "-c",
433
+ "--uuid"
434
+ ]
425
435
  };
426
436
  const inspectPolicy = {
427
437
  default: timestampVariant,
@@ -431,7 +441,7 @@ const inspectPolicy = {
431
441
  opaqueVariant,
432
442
  signedVariant
433
443
  ],
434
- intrinsicFlags: []
444
+ intrinsicFlags: ["--from-uuid", "--brand"]
435
445
  };
436
446
  const keygenPolicy = {
437
447
  default: opaqueVariant,
@@ -493,8 +503,9 @@ async function buildCodec(variant, brand, values, opts) {
493
503
  function usageInspect() {
494
504
  return [
495
505
  "Usage: ids inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
506
+ " ids inspect --from-uuid <uuid> --brand <brand>",
496
507
  "",
497
- " Decode an ID and print brand, timestamp (or lookup key), and canonical form.",
508
+ " Decode an ID and print brand, timestamp (or lookup key), canonical form, and UUID.",
498
509
  " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
499
510
  " --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
500
511
  " --kind is required with --wrapped: u32, i32, u64, or i64.",
@@ -502,12 +513,14 @@ function usageInspect() {
502
513
  " --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
503
514
  " Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
504
515
  " Note: --digest is not supported for inspect (Digest IDs are one-way; there is no reverse path).",
516
+ " --from-uuid <uuid> converts a UUID back to a canonical Id<Brand>. Requires --brand <brand>.",
517
+ " --brand <brand> specifies the entity type brand for --from-uuid (e.g. usr).",
505
518
  ""
506
519
  ].join("\n");
507
520
  }
508
521
  function usageGenerate() {
509
522
  return [
510
- `Usage: ids generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--key-format hex|base64url]`,
523
+ `Usage: ids generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--uuid] [--key-format hex|base64url]`,
511
524
  "",
512
525
  ` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
513
526
  " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
@@ -518,6 +531,7 @@ function usageGenerate() {
518
531
  " Reads the digest key from IDS_DIGEST_KEY (hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
519
532
  " Same material + ns + key always produces the same ID. Digest IDs are one-way.",
520
533
  " --count N > 1 is rejected: same material always produces the same ID.",
534
+ " --uuid emits the raw UUID form of each generated ID instead of the canonical ID.",
521
535
  ""
522
536
  ].join("\n");
523
537
  }
@@ -538,7 +552,8 @@ function usage() {
538
552
  "",
539
553
  "Subcommands:",
540
554
  " 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.",
555
+ " inspect, i --from-uuid <uuid> --brand <brand>",
556
+ " Decode an ID and print brand, timestamp (or lookup key), canonical form, and UUID.",
542
557
  " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
543
558
  " --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
544
559
  " --kind is required with --wrapped: u32, i32, u64, or i64.",
@@ -546,7 +561,9 @@ function usage() {
546
561
  " --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
547
562
  " Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
548
563
  " 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]",
564
+ " --from-uuid <uuid> converts a UUID back to a canonical Id<Brand>. Requires --brand <brand>.",
565
+ " --brand <brand> specifies the entity type brand for --from-uuid (e.g. usr).",
566
+ " generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--uuid] [--key-format hex|base64url]",
550
567
  ` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
551
568
  " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
552
569
  " --reverse mints Reverse Timestamp IDs (newest-first sort order).",
@@ -556,6 +573,7 @@ function usage() {
556
573
  " Reads the digest key from IDS_DIGEST_KEY (hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
557
574
  " Same material + ns + key always produces the same ID. Digest IDs are one-way.",
558
575
  " --count N > 1 is rejected: same material always produces the same ID.",
576
+ " --uuid emits the raw UUID form of each generated ID instead of the canonical ID.",
559
577
  " keygen, k [--wrapped] [--signed] [--digest] [--bits 128|192|256] [--key-format hex|base64url]",
560
578
  " Emit a random key for importOpaqueKey, importWrappingKey, importSigningKey, or importDigestKey (key on stdout; warning on stderr).",
561
579
  " Safe handling: redirect stdout to a 0600 file (e.g. ids keygen > key.hex && chmod 0600 key.hex);",
@@ -635,7 +653,14 @@ async function runGenerate(args, opts) {
635
653
  opts.stderr(codec.message + "\n");
636
654
  return codec.kind === "usage" ? 2 : 1;
637
655
  }
638
- for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
656
+ const emitUuid = flags.has("--uuid");
657
+ for (let i = 0; i < count; i++) {
658
+ const id = await codec.generate();
659
+ if (emitUuid) {
660
+ const uuid = codec.toUUID(id);
661
+ opts.stdout(uuid + "\n");
662
+ } else opts.stdout(id + "\n");
663
+ }
639
664
  return 0;
640
665
  }
641
666
  //#endregion
@@ -657,6 +682,32 @@ async function runInspect(args, opts) {
657
682
  opts.stderr(errors[0] + "\n");
658
683
  return 2;
659
684
  }
685
+ const fromUuidValue = values.get("--from-uuid");
686
+ if (fromUuidValue !== void 0) {
687
+ if (fromUuidValue === "") {
688
+ opts.stderr("--from-uuid requires a value\n");
689
+ return 2;
690
+ }
691
+ const brandValue = values.get("--brand");
692
+ if (brandValue === void 0 || brandValue === "") {
693
+ opts.stderr("--from-uuid requires --brand\n");
694
+ return 2;
695
+ }
696
+ let tsCodec;
697
+ try {
698
+ tsCodec = createTimestampId(brandValue, codecOpts(opts));
699
+ } catch (err) {
700
+ opts.stderr(formatCliError(err) + "\n");
701
+ return 1;
702
+ }
703
+ const result = tsCodec.safeFromUUID(fromUuidValue);
704
+ if (!result.ok) {
705
+ opts.stderr("invalid_uuid: not a valid RFC 9562 UUID\n");
706
+ return 1;
707
+ }
708
+ opts.stdout(result.id + "\n");
709
+ return 0;
710
+ }
660
711
  const [input] = positionals;
661
712
  if (input === void 0) {
662
713
  opts.stderr(usageInspect());
@@ -681,6 +732,7 @@ async function runInspect(args, opts) {
681
732
  let verifyTimestamp;
682
733
  let verifyCanonical;
683
734
  let verifyNowMs;
735
+ let verifyTsCodec;
684
736
  if (cap.mode === "verify") {
685
737
  const fmtCheck = parseKeyFormat(values, opts, variant.key);
686
738
  if (isKeyFormatError(fmtCheck)) {
@@ -699,6 +751,7 @@ async function runInspect(args, opts) {
699
751
  opts.stderr(invalidIdPrefix + structValidation.issues[0].message + "\n");
700
752
  return 1;
701
753
  }
754
+ verifyTsCodec = tsCodec;
702
755
  verifyCanonical = structValidation.value;
703
756
  verifyTimestamp = tsCodec.extractTimestamp(verifyCanonical);
704
757
  verifyNowMs = (opts.now ?? Date.now)();
@@ -706,10 +759,12 @@ async function runInspect(args, opts) {
706
759
  const codecOrError = await buildCodec(variant, brand, values, opts);
707
760
  if (isCodecError(codecOrError)) {
708
761
  if (cap.mode === "verify") {
762
+ const uuid = verifyTsCodec.toUUID(verifyCanonical);
709
763
  opts.stdout(formatSignedInspectOutput({
710
764
  brand,
711
765
  timestamp: verifyTimestamp,
712
766
  canonical: verifyCanonical,
767
+ uuid,
713
768
  input,
714
769
  nowMs: verifyNowMs,
715
770
  verification: "unavailable"
@@ -729,15 +784,20 @@ async function runInspect(args, opts) {
729
784
  }
730
785
  canonical = parsed.value;
731
786
  }
787
+ function codecToUUID(id) {
788
+ return codecOrError.toUUID(id);
789
+ }
732
790
  switch (cap.mode) {
733
791
  case "readable": {
734
792
  const timestamp = cap.extractTimestamp(codecOrError, canonical);
735
793
  const nowMs = (opts.now ?? Date.now)();
794
+ const uuid = codecToUUID(canonical);
736
795
  opts.stderr(cap.note + "\n");
737
796
  opts.stdout(formatInspectOutput({
738
797
  brand,
739
798
  timestamp,
740
799
  canonical,
800
+ uuid,
741
801
  input,
742
802
  nowMs
743
803
  }));
@@ -746,11 +806,13 @@ async function runInspect(args, opts) {
746
806
  case "keyed-readable": {
747
807
  const timestamp = await cap.extractTimestamp(codecOrError, canonical);
748
808
  const nowMs = (opts.now ?? Date.now)();
809
+ const uuid = codecToUUID(canonical);
749
810
  opts.stderr(cap.note + "\n");
750
811
  opts.stdout(formatInspectOutput({
751
812
  brand,
752
813
  timestamp,
753
814
  canonical,
815
+ uuid,
754
816
  input,
755
817
  nowMs
756
818
  }));
@@ -764,15 +826,18 @@ async function runInspect(args, opts) {
764
826
  opts.stderr(formatCliError(err) + "\n");
765
827
  return 1;
766
828
  }
829
+ const uuid = codecToUUID(canonical);
767
830
  opts.stdout(formatWrappedInspectOutput({
768
831
  brand,
769
832
  lookupKey,
770
833
  canonical,
834
+ uuid,
771
835
  input
772
836
  }));
773
837
  return 0;
774
838
  }
775
839
  case "verify": {
840
+ const uuid = verifyTsCodec.toUUID(verifyCanonical);
776
841
  const verifyResult = await cap.safeVerify(codecOrError, input);
777
842
  if (!verifyResult.ok) {
778
843
  /* v8 ignore next 4 -- defensive: both codecs share the same wire parse so ParseError
@@ -785,6 +850,7 @@ async function runInspect(args, opts) {
785
850
  brand,
786
851
  timestamp: verifyTimestamp,
787
852
  canonical: verifyCanonical,
853
+ uuid,
788
854
  input,
789
855
  nowMs: verifyNowMs,
790
856
  verification: "failed"
@@ -796,6 +862,7 @@ async function runInspect(args, opts) {
796
862
  brand,
797
863
  timestamp: verifyTimestamp,
798
864
  canonical: verifyResult.id,
865
+ uuid,
799
866
  input,
800
867
  nowMs: verifyNowMs,
801
868
  verification: "ok"