@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.
- package/README.md +17 -5
- package/dist/{adapter-types-CIc-4O-P.d.mts → adapter-types-Bia_w9sg.d.mts} +2 -2
- package/dist/{adapter-types-CIc-4O-P.d.mts.map → adapter-types-Bia_w9sg.d.mts.map} +1 -1
- package/dist/cli.mjs +82 -15
- package/dist/cli.mjs.map +1 -1
- package/dist/{codec-shell-C2NKQEx2.mjs → codec-shell-BRZkuQeP.mjs} +89 -7
- package/dist/codec-shell-BRZkuQeP.mjs.map +1 -0
- package/dist/{digest-DsGeXfk3.mjs → digest-CLJEGBxo.mjs} +7 -4
- package/dist/{digest-DsGeXfk3.mjs.map → digest-CLJEGBxo.mjs.map} +1 -1
- package/dist/digest.d.mts +19 -2
- package/dist/digest.d.mts.map +1 -1
- package/dist/digest.mjs +1 -1
- package/dist/drizzle.d.mts +3 -3
- package/dist/{error-Dqyho9vp.d.mts → error-CifcKKOG.d.mts} +2 -2
- package/dist/{error-Dqyho9vp.d.mts.map → error-CifcKKOG.d.mts.map} +1 -1
- package/dist/express.d.mts +2 -2
- package/dist/fastify.d.mts +2 -2
- package/dist/graphql.d.mts +2 -2
- package/dist/hono.d.mts +2 -2
- package/dist/index.d.mts +19 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{key-material-DvjACe89.mjs → key-material-1wOKJ1o-.mjs} +2 -2
- package/dist/{key-material-DvjACe89.mjs.map → key-material-1wOKJ1o-.mjs.map} +1 -1
- package/dist/kysely.d.mts +3 -3
- package/dist/mikro-orm.d.mts +3 -3
- package/dist/nestjs.d.mts +2 -2
- package/dist/{opaque-BW3Uzeeb.mjs → opaque-COAcIIY4.mjs} +14 -5
- package/dist/opaque-COAcIIY4.mjs.map +1 -0
- package/dist/opaque.d.mts +26 -2
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +1 -1
- package/dist/prisma.d.mts +3 -3
- package/dist/{reverse-BW8g_cln.mjs → reverse-CT-El3hi.mjs} +7 -4
- package/dist/{reverse-BW8g_cln.mjs.map → reverse-CT-El3hi.mjs.map} +1 -1
- package/dist/reverse.d.mts +19 -2
- package/dist/reverse.d.mts.map +1 -1
- package/dist/reverse.mjs +1 -1
- package/dist/{rng-BHFxX1Fc.mjs → rng-6GyNT4zS.mjs} +2 -2
- package/dist/{rng-BHFxX1Fc.mjs.map → rng-6GyNT4zS.mjs.map} +1 -1
- package/dist/{signed-BTz3ZFYE.mjs → signed-Dkdteu1y.mjs} +8 -5
- package/dist/{signed-BTz3ZFYE.mjs.map → signed-Dkdteu1y.mjs.map} +1 -1
- package/dist/signed.d.mts +19 -2
- package/dist/signed.d.mts.map +1 -1
- package/dist/signed.mjs +1 -1
- package/dist/{timestamp-CleAIdZI.mjs → timestamp-RXXwHfHO.mjs} +7 -4
- package/dist/{timestamp-CleAIdZI.mjs.map → timestamp-RXXwHfHO.mjs.map} +1 -1
- package/dist/typeorm.d.mts +2 -2
- package/dist/{types-wplmOgOK.d.mts → types-hGBnCpJj.d.mts} +3 -3
- package/dist/{types-wplmOgOK.d.mts.map → types-hGBnCpJj.d.mts.map} +1 -1
- package/dist/{wrapped-DPlsv1x-.mjs → wrapped-Oj2hC1vB.mjs} +15 -4
- package/dist/wrapped-Oj2hC1vB.mjs.map +1 -0
- package/dist/wrapped.d.mts +27 -2
- package/dist/wrapped.d.mts.map +1 -1
- package/dist/wrapped.mjs +1 -1
- package/package.json +3 -2
- package/spec/vectors.json +97 -0
- package/dist/codec-shell-C2NKQEx2.mjs.map +0 -1
- package/dist/opaque-BW3Uzeeb.mjs.map +0 -1
- 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 `
|
|
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(); // "
|
|
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("
|
|
29
|
+
const r = users.safeParse("USR_06F80Z92D2DBSQQG28T5CY4TQG");
|
|
30
30
|
if (r.ok) {
|
|
31
|
-
r.id; // "
|
|
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"`)
|
|
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-
|
|
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-
|
|
20
|
+
//# sourceMappingURL=adapter-types-Bia_w9sg.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter-types-
|
|
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-
|
|
4
|
-
import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-
|
|
5
|
-
import { t as createReverseTimestampId } from "./reverse-
|
|
6
|
-
import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-
|
|
7
|
-
import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-
|
|
8
|
-
import { i as importDigestKey, n as decodeDigestKey, r as encodeDigestKey, t as createDigestId } from "./digest-
|
|
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: [
|
|
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),
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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"
|