@smonn/ids 0.8.0 → 0.9.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 (71) hide show
  1. package/README.md +210 -32
  2. package/dist/adapter-types-BY-wrYYB.mjs +27 -0
  3. package/dist/adapter-types-BY-wrYYB.mjs.map +1 -0
  4. package/dist/adapter-types-unUcmMXC.d.mts +20 -0
  5. package/dist/adapter-types-unUcmMXC.d.mts.map +1 -0
  6. package/dist/cli.mjs +342 -71
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/{codec-shell-DH-UO4UR.mjs → codec-shell-CW2sD6BU.mjs} +6 -5
  9. package/dist/codec-shell-CW2sD6BU.mjs.map +1 -0
  10. package/dist/drizzle.d.mts +33 -2
  11. package/dist/drizzle.d.mts.map +1 -0
  12. package/dist/drizzle.mjs +2 -3
  13. package/dist/drizzle.mjs.map +1 -1
  14. package/dist/express.d.mts +2 -5
  15. package/dist/express.d.mts.map +1 -1
  16. package/dist/express.mjs +3 -8
  17. package/dist/express.mjs.map +1 -1
  18. package/dist/fastify.d.mts +2 -5
  19. package/dist/fastify.d.mts.map +1 -1
  20. package/dist/fastify.mjs +3 -8
  21. package/dist/fastify.mjs.map +1 -1
  22. package/dist/hono.d.mts +2 -5
  23. package/dist/hono.d.mts.map +1 -1
  24. package/dist/hono.mjs +3 -8
  25. package/dist/hono.mjs.map +1 -1
  26. package/dist/index.mjs +1 -1
  27. package/dist/key-material-gOnqTNoV.mjs +137 -0
  28. package/dist/key-material-gOnqTNoV.mjs.map +1 -0
  29. package/dist/kysely.d.mts +1 -1
  30. package/dist/kysely.mjs +2 -3
  31. package/dist/kysely.mjs.map +1 -1
  32. package/dist/{opaque-uvjOFY_0.mjs → opaque-BpqxV8oB.mjs} +12 -48
  33. package/dist/opaque-BpqxV8oB.mjs.map +1 -0
  34. package/dist/opaque.d.mts +8 -0
  35. package/dist/opaque.d.mts.map +1 -1
  36. package/dist/opaque.mjs +1 -1
  37. package/dist/prisma.d.mts +4 -18
  38. package/dist/prisma.d.mts.map +1 -1
  39. package/dist/prisma.mjs +3 -5
  40. package/dist/prisma.mjs.map +1 -1
  41. package/dist/{reverse-BgFU6JHw.mjs → reverse-d5uEoIET.mjs} +5 -7
  42. package/dist/reverse-d5uEoIET.mjs.map +1 -0
  43. package/dist/reverse.d.mts.map +1 -1
  44. package/dist/reverse.mjs +1 -1
  45. package/dist/rng-CPJOx_nE.mjs +9 -0
  46. package/dist/rng-CPJOx_nE.mjs.map +1 -0
  47. package/dist/signed-BnRSC03a.mjs +207 -0
  48. package/dist/signed-BnRSC03a.mjs.map +1 -0
  49. package/dist/signed.d.mts.map +1 -1
  50. package/dist/signed.mjs +1 -255
  51. package/dist/{timestamp-B5_UCzc6.mjs → timestamp-BbZL8hwg.mjs} +5 -5
  52. package/dist/{timestamp-B5_UCzc6.mjs.map → timestamp-BbZL8hwg.mjs.map} +1 -1
  53. package/dist/{timestamp-bytes-BBY7JI33.mjs → timestamp-bytes-DoFjLjDp.mjs} +3 -2
  54. package/dist/timestamp-bytes-DoFjLjDp.mjs.map +1 -0
  55. package/dist/{wrapped-0vL72Nje.mjs → wrapped-BI9UXnAm.mjs} +33 -62
  56. package/dist/wrapped-BI9UXnAm.mjs.map +1 -0
  57. package/dist/wrapped.d.mts.map +1 -1
  58. package/dist/wrapped.mjs +1 -1
  59. package/package.json +5 -5
  60. package/dist/adapter-types-oHCCSgOO.d.mts +0 -12
  61. package/dist/adapter-types-oHCCSgOO.d.mts.map +0 -1
  62. package/dist/bytes-lhzKVaBV.mjs +0 -53
  63. package/dist/bytes-lhzKVaBV.mjs.map +0 -1
  64. package/dist/codec-shell-DH-UO4UR.mjs.map +0 -1
  65. package/dist/drizzle-CeSni5PB.d.mts +0 -44
  66. package/dist/drizzle-CeSni5PB.d.mts.map +0 -1
  67. package/dist/opaque-uvjOFY_0.mjs.map +0 -1
  68. package/dist/reverse-BgFU6JHw.mjs.map +0 -1
  69. package/dist/signed.mjs.map +0 -1
  70. package/dist/timestamp-bytes-BBY7JI33.mjs.map +0 -1
  71. package/dist/wrapped-0vL72Nje.mjs.map +0 -1
package/dist/cli.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as isIdsError } from "./error-Cp5qYZcv.mjs";
3
- import { t as createTimestampId } from "./timestamp-B5_UCzc6.mjs";
4
- import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-uvjOFY_0.mjs";
5
- import { t as createReverseTimestampId } from "./reverse-BgFU6JHw.mjs";
6
- import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-0vL72Nje.mjs";
3
+ import { t as createTimestampId } from "./timestamp-BbZL8hwg.mjs";
4
+ import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-BpqxV8oB.mjs";
5
+ import { t as createReverseTimestampId } from "./reverse-d5uEoIET.mjs";
6
+ import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-BnRSC03a.mjs";
7
+ import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-BI9UXnAm.mjs";
7
8
  //#region src/cli/codec-options.ts
8
9
  function codecOpts(opts) {
9
10
  const o = { allowDuplicateBrand: true };
@@ -26,6 +27,14 @@ function formatWrappedInspectOutput(result) {
26
27
  ""
27
28
  ].join("\n");
28
29
  }
30
+ function formatSignedInspectOutput(result) {
31
+ const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
32
+ const inputLine = describeInputForm(result.input, result.canonical);
33
+ const lines = [`brand: ${result.brand}`, `timestamp: ${result.timestamp.toISOString()} (${relative})`];
34
+ lines.push(`verification: ${result.verification}`);
35
+ lines.push(`canonical: ${result.canonical}`, `input: ${inputLine}`, "");
36
+ return lines.join("\n");
37
+ }
29
38
  function formatInspectOutput(result) {
30
39
  const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
31
40
  const inputLine = describeInputForm(result.input, result.canonical);
@@ -86,19 +95,12 @@ function splitFlagToken(arg) {
86
95
  inlineValue: arg.slice(eq + 1)
87
96
  };
88
97
  }
89
- function splitFlags(args) {
98
+ function splitFlags(args, valueFlags) {
90
99
  const flags = /* @__PURE__ */ new Set();
91
100
  const values = /* @__PURE__ */ new Map();
92
101
  const positionals = [];
93
102
  const errors = [];
94
103
  const seenFlags = /* @__PURE__ */ new Set();
95
- const valueFlags = new Set([
96
- "--count",
97
- "-c",
98
- "--bits",
99
- "--key-format",
100
- "--kind"
101
- ]);
102
104
  const addFlag = (flag) => {
103
105
  const canonical = canonicalFlag(flag);
104
106
  if (seenFlags.has(canonical)) errors.push(`duplicate flag: ${canonical}`);
@@ -108,11 +110,6 @@ function splitFlags(args) {
108
110
  for (let i = 0; i < args.length; i++) {
109
111
  const raw = args[i];
110
112
  const { flag, inlineValue } = splitFlagToken(raw);
111
- if (flag === "--opaque" || flag === "--wrapped" || flag === "--reverse") {
112
- addFlag(flag);
113
- if (inlineValue !== void 0) errors.push(`flag does not take a value: ${flag}`);
114
- continue;
115
- }
116
113
  if (valueFlags.has(flag)) {
117
114
  if (inlineValue !== void 0) {
118
115
  addFlag(flag);
@@ -132,6 +129,7 @@ function splitFlags(args) {
132
129
  }
133
130
  if (flag.startsWith("-")) {
134
131
  addFlag(flag);
132
+ if (inlineValue !== void 0) errors.push(`flag does not take a value: ${flag}`);
135
133
  continue;
136
134
  }
137
135
  positionals.push(raw);
@@ -147,10 +145,11 @@ function canonicalFlag(flag) {
147
145
  if (flag === "-c") return "--count";
148
146
  return flag;
149
147
  }
150
- const knownFlags = new Set([
148
+ const knownFlags = /* @__PURE__ */ new Set([
151
149
  "--opaque",
152
150
  "--wrapped",
153
151
  "--reverse",
152
+ "--signed",
154
153
  "--kind",
155
154
  "--key-format",
156
155
  "--count",
@@ -189,48 +188,83 @@ function isKindError(result) {
189
188
  return result !== "u32" && result !== "i32" && result !== "u64" && result !== "i64";
190
189
  }
191
190
  //#endregion
192
- //#region src/cli/opaque-key.ts
191
+ //#region src/cli/key-io.ts
193
192
  function isKeyFormatError(result) {
194
193
  return result !== "hex" && result !== "base64url";
195
194
  }
196
- function parseKeyFormatFlag$1(values) {
195
+ function parseKeyFormatFlag(values) {
197
196
  const fromFlag = values.get("--key-format");
198
197
  if (fromFlag === void 0) return void 0;
199
198
  if (fromFlag === "") return "--key-format requires a value";
200
199
  if (fromFlag === "hex" || fromFlag === "base64url") return fromFlag;
201
200
  return `--key-format must be hex or base64url, got '${fromFlag}'`;
202
201
  }
203
- function parseKeygenFormat(values) {
204
- const fromFlag = parseKeyFormatFlag$1(values);
202
+ function parseKeyFormatFromFlag(values) {
203
+ const fromFlag = parseKeyFormatFlag(values);
205
204
  if (fromFlag === void 0) return "hex";
206
205
  return fromFlag;
207
206
  }
208
- function parseOpaqueKeyFormat(values, opts) {
209
- const fromFlag = parseKeyFormatFlag$1(values);
207
+ function parseKeyFormat(values, opts, facet) {
208
+ const fromFlag = parseKeyFormatFlag(values);
210
209
  if (fromFlag !== void 0) return fromFlag;
211
- const fromEnv = (opts.env ?? process.env).IDS_KEY_FORMAT;
210
+ const fromEnv = (opts.env ?? process.env)[facet.formatEnvVar];
212
211
  if (fromEnv === void 0 || fromEnv === "") return "hex";
213
212
  if (fromEnv === "hex" || fromEnv === "base64url") return fromEnv;
214
- return `IDS_KEY_FORMAT must be hex or base64url, got '${fromEnv}'`;
213
+ return `${facet.formatEnvVar} must be hex or base64url, got '${fromEnv}'`;
215
214
  }
216
- async function loadOpaqueKey(opts, format) {
217
- const raw = (opts.env ?? process.env).IDS_KEY;
218
- if (raw === void 0 || raw === "") return "missing IDS_KEY environment variable";
215
+ async function loadKey(opts, format, facet) {
216
+ const raw = (opts.env ?? process.env)[facet.envVar];
217
+ if (raw === void 0 || raw === "") return `missing ${facet.envVar} environment variable`;
219
218
  try {
220
- return importOpaqueKey(decodeOpaqueKey(raw, format));
219
+ return await facet.import(facet.decode(raw, format));
221
220
  } catch (err) {
222
221
  return err.message;
223
222
  }
224
223
  }
225
224
  //#endregion
225
+ //#region src/cli/opaque-key.ts
226
+ const opaqueFacet = {
227
+ envVar: "IDS_KEY",
228
+ formatEnvVar: "IDS_KEY_FORMAT",
229
+ decode: decodeOpaqueKey,
230
+ import: importOpaqueKey
231
+ };
232
+ function parseOpaqueKeyFormat(values, opts) {
233
+ return parseKeyFormat(values, opts, opaqueFacet);
234
+ }
235
+ async function loadOpaqueKey(opts, format) {
236
+ return loadKey(opts, format, opaqueFacet);
237
+ }
238
+ //#endregion
239
+ //#region src/cli/signing-key.ts
240
+ const signingFacet = {
241
+ envVar: "IDS_SIGNING_KEY",
242
+ formatEnvVar: "IDS_SIGNING_KEY_FORMAT",
243
+ decode: decodeSigningKey,
244
+ import: importSigningKey
245
+ };
246
+ function parseSigningKeyFormat(values, opts) {
247
+ return parseKeyFormat(values, opts, signingFacet);
248
+ }
249
+ async function loadSigningKey(opts, format) {
250
+ return loadKey(opts, format, signingFacet);
251
+ }
252
+ //#endregion
226
253
  //#region src/cli/commands/generate.ts
227
254
  function runGenerate(args, opts) {
228
- const { flags, values, positionals, errors } = splitFlags(args);
229
- const unsupported = unsupportedFlagForCommand("generate", flags, new Set([
255
+ const { flags, values, positionals, errors } = splitFlags(args, /* @__PURE__ */ new Set([
256
+ "--count",
257
+ "-c",
258
+ "--bits",
259
+ "--key-format",
260
+ "--kind"
261
+ ]));
262
+ const unsupported = unsupportedFlagForCommand("generate", flags, /* @__PURE__ */ new Set([
230
263
  "--count",
231
264
  "-c",
232
265
  "--opaque",
233
266
  "--reverse",
267
+ "--signed",
234
268
  "--key-format"
235
269
  ]));
236
270
  if (unsupported !== void 0) {
@@ -254,12 +288,21 @@ function runGenerate(args, opts) {
254
288
  }
255
289
  const opaque = flags.has("--opaque");
256
290
  const reverse = flags.has("--reverse");
291
+ const signed = flags.has("--signed");
257
292
  if (reverse && opaque) {
258
293
  opts.stderr("cannot use --reverse and --opaque together\n");
259
294
  return Promise.resolve(1);
260
295
  }
261
- if (!opaque && flags.has("--key-format")) {
262
- opts.stderr("--key-format requires --opaque\n");
296
+ if (signed && opaque) {
297
+ opts.stderr("cannot use --signed and --opaque together\n");
298
+ return Promise.resolve(1);
299
+ }
300
+ if (signed && reverse) {
301
+ opts.stderr("cannot use --signed and --reverse together\n");
302
+ return Promise.resolve(1);
303
+ }
304
+ if (!opaque && !signed && flags.has("--key-format")) {
305
+ opts.stderr("--key-format requires --opaque or --signed\n");
263
306
  return Promise.resolve(1);
264
307
  }
265
308
  if (opaque) {
@@ -270,6 +313,14 @@ function runGenerate(args, opts) {
270
313
  }
271
314
  return runOpaqueGenerate(brand ?? "", count, format, opts);
272
315
  }
316
+ if (signed) {
317
+ const format = parseSigningKeyFormat(values, opts);
318
+ if (isKeyFormatError(format)) {
319
+ opts.stderr(format + "\n");
320
+ return Promise.resolve(1);
321
+ }
322
+ return runSignedGenerate(brand ?? "", count, format, opts);
323
+ }
273
324
  if (reverse) {
274
325
  let codec;
275
326
  try {
@@ -310,31 +361,39 @@ async function runOpaqueGenerate(brand, count, format, opts) {
310
361
  for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
311
362
  return 0;
312
363
  }
364
+ async function runSignedGenerate(brand, count, format, opts) {
365
+ const keyResult = await loadSigningKey(opts, format);
366
+ if (typeof keyResult === "string") {
367
+ opts.stderr(keyResult + "\n");
368
+ return 1;
369
+ }
370
+ let codec;
371
+ try {
372
+ codec = createSignedTimestampId(brand, {
373
+ keys: [keyResult],
374
+ allowDuplicateBrand: true,
375
+ ...codecOpts(opts)
376
+ });
377
+ } catch (err) {
378
+ opts.stderr(formatCliError(err) + "\n");
379
+ return 1;
380
+ }
381
+ for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
382
+ return 0;
383
+ }
313
384
  //#endregion
314
385
  //#region src/cli/wrapping-key.ts
315
- function parseKeyFormatFlag(values) {
316
- const fromFlag = values.get("--key-format");
317
- if (fromFlag === void 0) return void 0;
318
- if (fromFlag === "") return "--key-format requires a value";
319
- if (fromFlag === "hex" || fromFlag === "base64url") return fromFlag;
320
- return `--key-format must be hex or base64url, got '${fromFlag}'`;
321
- }
386
+ const wrappingFacet = {
387
+ envVar: "IDS_WRAPPING_KEY",
388
+ formatEnvVar: "IDS_WRAPPING_KEY_FORMAT",
389
+ decode: decodeWrappingKey,
390
+ import: importWrappingKey
391
+ };
322
392
  function parseWrappingKeyFormat(values, opts) {
323
- const fromFlag = parseKeyFormatFlag(values);
324
- if (fromFlag !== void 0) return fromFlag;
325
- const fromEnv = (opts.env ?? process.env).IDS_WRAPPING_KEY_FORMAT;
326
- if (fromEnv === void 0 || fromEnv === "") return "hex";
327
- if (fromEnv === "hex" || fromEnv === "base64url") return fromEnv;
328
- return `IDS_WRAPPING_KEY_FORMAT must be hex or base64url, got '${fromEnv}'`;
393
+ return parseKeyFormat(values, opts, wrappingFacet);
329
394
  }
330
395
  async function loadWrappingKey(opts, format) {
331
- const raw = (opts.env ?? process.env).IDS_WRAPPING_KEY;
332
- if (raw === void 0 || raw === "") return "missing IDS_WRAPPING_KEY environment variable";
333
- try {
334
- return importWrappingKey(decodeWrappingKey(raw, format));
335
- } catch (err) {
336
- return err.message;
337
- }
396
+ return loadKey(opts, format, wrappingFacet);
338
397
  }
339
398
  //#endregion
340
399
  //#region src/cli/usage.ts
@@ -343,30 +402,41 @@ function usage() {
343
402
  "Usage: ids <subcommand> [args]",
344
403
  "",
345
404
  "Subcommands:",
346
- " inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--key-format hex|base64url]",
405
+ " inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
347
406
  " Decode an ID and print brand, timestamp (or lookup key), and canonical form.",
348
407
  " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
349
408
  " --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
350
409
  " --kind is required with --wrapped: u32, i32, u64, or i64.",
351
410
  " --reverse decodes a Reverse Timestamp ID (newest-first sort order).",
352
- " generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--key-format hex|base64url]",
411
+ " --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
412
+ " Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
413
+ " generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--key-format hex|base64url]",
353
414
  ` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
354
415
  " --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
355
416
  " --reverse mints Reverse Timestamp IDs (newest-first sort order).",
356
- " keygen, k [--wrapped] [--bits 128|192|256] [--key-format hex|base64url]",
357
- " Emit a random AES key for importOpaqueKey (stdout only).",
417
+ " --signed mints Signed Timestamp IDs; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
418
+ " keygen, k [--wrapped] [--signed] [--bits 128|192|256] [--key-format hex|base64url]",
419
+ " Emit a random key for importOpaqueKey, importWrappingKey, or importSigningKey (stdout only).",
358
420
  " --wrapped emits a wrapping key for importWrappingKey instead (IDS_WRAPPING_KEY).",
421
+ " --signed emits a signing key for importSigningKey instead (IDS_SIGNING_KEY; hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
359
422
  ""
360
423
  ].join("\n");
361
424
  }
362
425
  //#endregion
363
426
  //#region src/cli/commands/inspect.ts
364
427
  function runInspect(args, opts) {
365
- const { flags, values, positionals, errors } = splitFlags(args);
366
- const unsupported = unsupportedFlagForCommand("inspect", flags, new Set([
428
+ const { flags, values, positionals, errors } = splitFlags(args, /* @__PURE__ */ new Set([
429
+ "--count",
430
+ "-c",
431
+ "--bits",
432
+ "--key-format",
433
+ "--kind"
434
+ ]));
435
+ const unsupported = unsupportedFlagForCommand("inspect", flags, /* @__PURE__ */ new Set([
367
436
  "--opaque",
368
437
  "--wrapped",
369
438
  "--reverse",
439
+ "--signed",
370
440
  "--kind",
371
441
  "--key-format"
372
442
  ]));
@@ -391,6 +461,7 @@ function runInspect(args, opts) {
391
461
  const opaque = flags.has("--opaque");
392
462
  const wrapped = flags.has("--wrapped");
393
463
  const reverse = flags.has("--reverse");
464
+ const signed = flags.has("--signed");
394
465
  if (opaque && wrapped) {
395
466
  opts.stderr("cannot use --wrapped and --opaque together\n");
396
467
  return Promise.resolve(1);
@@ -403,8 +474,20 @@ function runInspect(args, opts) {
403
474
  opts.stderr("cannot use --reverse and --wrapped together\n");
404
475
  return Promise.resolve(1);
405
476
  }
406
- if (!opaque && !wrapped && flags.has("--key-format")) {
407
- opts.stderr("--key-format requires --opaque or --wrapped\n");
477
+ if (signed && opaque) {
478
+ opts.stderr("cannot use --signed and --opaque together\n");
479
+ return Promise.resolve(1);
480
+ }
481
+ if (signed && wrapped) {
482
+ opts.stderr("cannot use --signed and --wrapped together\n");
483
+ return Promise.resolve(1);
484
+ }
485
+ if (signed && reverse) {
486
+ opts.stderr("cannot use --signed and --reverse together\n");
487
+ return Promise.resolve(1);
488
+ }
489
+ if (!opaque && !wrapped && !signed && flags.has("--key-format")) {
490
+ opts.stderr("--key-format requires --opaque, --wrapped, or --signed\n");
408
491
  return Promise.resolve(1);
409
492
  }
410
493
  const brand = input.slice(0, 3).toLowerCase();
@@ -433,6 +516,14 @@ function runInspect(args, opts) {
433
516
  }
434
517
  return runOpaqueInspect(brand, input, format, opts);
435
518
  }
519
+ if (signed) {
520
+ const format = parseSigningKeyFormat(values, opts);
521
+ if (isKeyFormatError(format)) {
522
+ opts.stderr(format + "\n");
523
+ return Promise.resolve(1);
524
+ }
525
+ return runSignedInspect(brand, input, format, opts);
526
+ }
436
527
  if (reverse) {
437
528
  let reverseCodec;
438
529
  try {
@@ -554,15 +645,186 @@ async function runOpaqueInspect(brand, input, format, opts) {
554
645
  }));
555
646
  return 0;
556
647
  }
648
+ async function runSignedInspect(brand, input, format, opts) {
649
+ let structCodec;
650
+ try {
651
+ structCodec = createTimestampId(brand, codecOpts(opts));
652
+ } catch (err) {
653
+ opts.stderr(formatCliError(err) + "\n");
654
+ return 1;
655
+ }
656
+ const validation = structCodec["~standard"].validate(input);
657
+ if (validation.issues) {
658
+ opts.stderr(validation.issues[0].message + "\n");
659
+ return 1;
660
+ }
661
+ const canonical = validation.value;
662
+ const timestamp = structCodec.extractTimestamp(canonical);
663
+ const nowMs = (opts.now ?? Date.now)();
664
+ const keyResult = await loadSigningKey(opts, format);
665
+ if (typeof keyResult === "string") {
666
+ opts.stdout(formatSignedInspectOutput({
667
+ brand,
668
+ timestamp,
669
+ canonical,
670
+ input,
671
+ nowMs,
672
+ verification: "unavailable"
673
+ }));
674
+ opts.stderr(keyResult + "\n");
675
+ return 1;
676
+ }
677
+ const verifyResult = await createSignedTimestampId(brand, {
678
+ keys: [keyResult],
679
+ allowDuplicateBrand: true,
680
+ ...codecOpts(opts)
681
+ }).safeVerify(input);
682
+ if (!verifyResult.ok) {
683
+ /* v8 ignore next 4 -- defensive: both codecs share the same wire parse so ParseError
684
+ is unreachable after the createTimestampId pre-validation above passes */
685
+ if (verifyResult.error !== "verification_failed") {
686
+ opts.stderr(verifyResult.error + "\n");
687
+ return 1;
688
+ }
689
+ opts.stdout(formatSignedInspectOutput({
690
+ brand,
691
+ timestamp,
692
+ canonical,
693
+ input,
694
+ nowMs,
695
+ verification: "failed"
696
+ }));
697
+ opts.stderr("verification_failed: verification failed\n");
698
+ return 1;
699
+ }
700
+ opts.stdout(formatSignedInspectOutput({
701
+ brand,
702
+ timestamp,
703
+ canonical: verifyResult.id,
704
+ input,
705
+ nowMs,
706
+ verification: "ok"
707
+ }));
708
+ return 0;
709
+ }
710
+ //#endregion
711
+ //#region src/cli/variants.ts
712
+ const opaqueVariant = {
713
+ flag: "--opaque",
714
+ key: {
715
+ envVar: "IDS_KEY",
716
+ formatEnvVar: "IDS_KEY_FORMAT",
717
+ encode: encodeOpaqueKey,
718
+ decode: decodeOpaqueKey,
719
+ import: importOpaqueKey
720
+ },
721
+ inspectMode: "keyed-readable",
722
+ construct(brand, opts, key) {
723
+ try {
724
+ return createOpaqueTimestampId(brand, {
725
+ key,
726
+ ...codecOpts(opts)
727
+ });
728
+ } catch (err) {
729
+ return formatCliError(err);
730
+ }
731
+ }
732
+ };
733
+ const reverseVariant = {
734
+ flag: "--reverse",
735
+ inspectMode: "readable",
736
+ construct(brand, opts) {
737
+ try {
738
+ return createReverseTimestampId(brand, codecOpts(opts));
739
+ } catch (err) {
740
+ return formatCliError(err);
741
+ }
742
+ }
743
+ };
744
+ const wrappedVariant = {
745
+ flag: "--wrapped",
746
+ key: {
747
+ envVar: "IDS_WRAPPING_KEY",
748
+ formatEnvVar: "IDS_WRAPPING_KEY_FORMAT",
749
+ encode: encodeWrappingKey,
750
+ decode: decodeWrappingKey,
751
+ import: importWrappingKey
752
+ },
753
+ inspectMode: "unwrap",
754
+ extraFlags: ["--kind"],
755
+ construct(brand, _opts, key, values) {
756
+ const kind = parseKind(values ?? /* @__PURE__ */ new Map());
757
+ if (kind === void 0) return "--kind is required with --wrapped";
758
+ if (isKindError(kind)) return kind;
759
+ try {
760
+ return createWrappedKeyId(brand, {
761
+ kind,
762
+ keys: [key],
763
+ allowDuplicateBrand: true
764
+ });
765
+ } catch (err) {
766
+ return formatCliError(err);
767
+ }
768
+ }
769
+ };
770
+ const signedVariant = {
771
+ flag: "--signed",
772
+ key: {
773
+ envVar: "IDS_SIGNING_KEY",
774
+ formatEnvVar: "IDS_SIGNING_KEY_FORMAT",
775
+ encode: encodeSigningKey,
776
+ decode: decodeSigningKey,
777
+ import: importSigningKey
778
+ },
779
+ inspectMode: "verify",
780
+ construct(brand, opts, key) {
781
+ try {
782
+ return createSignedTimestampId(brand, {
783
+ keys: [key],
784
+ ...codecOpts(opts)
785
+ });
786
+ } catch (err) {
787
+ return formatCliError(err);
788
+ }
789
+ }
790
+ };
791
+ const conflictPriorityOrder = [
792
+ signedVariant,
793
+ reverseVariant,
794
+ wrappedVariant,
795
+ opaqueVariant
796
+ ];
797
+ const keygenPolicy = {
798
+ default: opaqueVariant,
799
+ selectable: [wrappedVariant, signedVariant],
800
+ intrinsicFlags: ["--bits"]
801
+ };
802
+ //#endregion
803
+ //#region src/cli/dispatch.ts
804
+ function deriveAllowedFlags(policy) {
805
+ const flags = new Set(policy.intrinsicFlags);
806
+ let hasKeyed = policy.default.key !== void 0;
807
+ for (const v of policy.selectable) {
808
+ if (v.flag !== void 0) flags.add(v.flag);
809
+ if (v.key !== void 0) hasKeyed = true;
810
+ if (v.extraFlags !== void 0) for (const f of v.extraFlags) flags.add(f);
811
+ }
812
+ if (hasKeyed) flags.add("--key-format");
813
+ return flags;
814
+ }
815
+ function resolveVariant(policy, flags) {
816
+ const selected = conflictPriorityOrder.filter((v) => policy.selectable.includes(v) && v.flag !== void 0 && flags.has(v.flag));
817
+ if (selected.length === 0) return policy.default;
818
+ if (selected.length === 1) return selected[0];
819
+ return `cannot use ${selected[0].flag} and ${selected[1].flag} together`;
820
+ }
557
821
  //#endregion
558
822
  //#region src/cli/commands/keygen.ts
559
823
  function runKeygen(args, opts) {
560
- const { flags, values, positionals, errors } = splitFlags(args);
561
- const unsupported = unsupportedFlagForCommand("keygen", flags, new Set([
562
- "--wrapped",
563
- "--bits",
564
- "--key-format"
565
- ]));
824
+ const allowedFlags = deriveAllowedFlags(keygenPolicy);
825
+ const variantExtraFlags = new Set(keygenPolicy.selectable.flatMap((v) => v.extraFlags ?? []));
826
+ const { flags, values, positionals, errors } = splitFlags(args, allowedFlags);
827
+ const unsupported = unsupportedFlagForCommand("keygen", flags, new Set([...allowedFlags].filter((f) => !variantExtraFlags.has(f))));
566
828
  if (unsupported !== void 0) {
567
829
  opts.stderr(unsupported + "\n");
568
830
  return Promise.resolve(1);
@@ -576,20 +838,29 @@ function runKeygen(args, opts) {
576
838
  opts.stderr(`unexpected argument: ${extra}\n`);
577
839
  return Promise.resolve(1);
578
840
  }
841
+ const variant = resolveVariant(keygenPolicy, flags);
842
+ if (typeof variant === "string") {
843
+ opts.stderr(variant + "\n");
844
+ return Promise.resolve(1);
845
+ }
579
846
  const bits = parseBits(values);
580
847
  if (typeof bits === "string") {
581
848
  opts.stderr(bits + "\n");
582
849
  return Promise.resolve(1);
583
850
  }
584
- const format = parseKeygenFormat(values);
851
+ const format = parseKeyFormatFromFlag(values);
585
852
  if (isKeyFormatError(format)) {
586
853
  opts.stderr(format + "\n");
587
854
  return Promise.resolve(1);
588
855
  }
856
+ /* v8 ignore next 4 -- defensive guard; all keygenPolicy variants have key defined */
857
+ if (variant.key === void 0) {
858
+ opts.stderr("internal: keygen policy variant has no key facet\n");
859
+ return Promise.resolve(1);
860
+ }
589
861
  const bytes = new Uint8Array(bits / 8);
590
862
  crypto.getRandomValues(bytes);
591
- const wrapped = flags.has("--wrapped");
592
- opts.stdout((wrapped ? encodeWrappingKey(bytes, format) : encodeOpaqueKey(bytes, format)) + "\n");
863
+ opts.stdout(variant.key.encode(bytes, format) + "\n");
593
864
  return Promise.resolve(0);
594
865
  }
595
866
  //#endregion