@smonn/ids 0.9.1 → 0.9.3
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 +60 -1003
- package/dist/cli.mjs +355 -575
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -5,6 +5,40 @@ import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as
|
|
|
5
5
|
import { t as createReverseTimestampId } from "./reverse-d5uEoIET.mjs";
|
|
6
6
|
import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-BnRSC03a.mjs";
|
|
7
7
|
import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-BI9UXnAm.mjs";
|
|
8
|
+
//#region src/cli/key-io.ts
|
|
9
|
+
function isKeyFormatError(result) {
|
|
10
|
+
return result !== "hex" && result !== "base64url";
|
|
11
|
+
}
|
|
12
|
+
function parseKeyFormatFlag(values) {
|
|
13
|
+
const fromFlag = values.get("--key-format");
|
|
14
|
+
if (fromFlag === void 0) return void 0;
|
|
15
|
+
if (fromFlag === "") return "--key-format requires a value";
|
|
16
|
+
if (fromFlag === "hex" || fromFlag === "base64url") return fromFlag;
|
|
17
|
+
return `--key-format must be hex or base64url, got '${fromFlag}'`;
|
|
18
|
+
}
|
|
19
|
+
function parseKeyFormatFromFlag(values) {
|
|
20
|
+
const fromFlag = parseKeyFormatFlag(values);
|
|
21
|
+
if (fromFlag === void 0) return "hex";
|
|
22
|
+
return fromFlag;
|
|
23
|
+
}
|
|
24
|
+
function parseKeyFormat(values, opts, facet) {
|
|
25
|
+
const fromFlag = parseKeyFormatFlag(values);
|
|
26
|
+
if (fromFlag !== void 0) return fromFlag;
|
|
27
|
+
const fromEnv = (opts.env ?? process.env)[facet.formatEnvVar];
|
|
28
|
+
if (fromEnv === void 0 || fromEnv === "") return "hex";
|
|
29
|
+
if (fromEnv === "hex" || fromEnv === "base64url") return fromEnv;
|
|
30
|
+
return `${facet.formatEnvVar} must be hex or base64url, got '${fromEnv}'`;
|
|
31
|
+
}
|
|
32
|
+
async function loadKey(opts, format, facet) {
|
|
33
|
+
const raw = (opts.env ?? process.env)[facet.envVar];
|
|
34
|
+
if (raw === void 0 || raw === "") return `missing ${facet.envVar} environment variable`;
|
|
35
|
+
try {
|
|
36
|
+
return await facet.import(facet.decode(raw, format));
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return err.message;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
8
42
|
//#region src/cli/codec-options.ts
|
|
9
43
|
function codecOpts(opts) {
|
|
10
44
|
const o = { allowDuplicateBrand: true };
|
|
@@ -13,73 +47,6 @@ function codecOpts(opts) {
|
|
|
13
47
|
return o;
|
|
14
48
|
}
|
|
15
49
|
//#endregion
|
|
16
|
-
//#region src/cli/format.ts
|
|
17
|
-
function formatCliError(err) {
|
|
18
|
-
return isIdsError(err) ? `${err.code}: ${err.message}` : err instanceof Error ? err.message : String(err);
|
|
19
|
-
}
|
|
20
|
-
function formatWrappedInspectOutput(result) {
|
|
21
|
-
const inputLine = describeInputForm(result.input, result.canonical);
|
|
22
|
-
return [
|
|
23
|
-
`brand: ${result.brand}`,
|
|
24
|
-
`lookup-key: ${result.lookupKey.toString()}`,
|
|
25
|
-
`canonical: ${result.canonical}`,
|
|
26
|
-
`input: ${inputLine}`,
|
|
27
|
-
""
|
|
28
|
-
].join("\n");
|
|
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
|
-
}
|
|
38
|
-
function formatInspectOutput(result) {
|
|
39
|
-
const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
|
|
40
|
-
const inputLine = describeInputForm(result.input, result.canonical);
|
|
41
|
-
return [
|
|
42
|
-
`brand: ${result.brand}`,
|
|
43
|
-
`timestamp: ${result.timestamp.toISOString()} (${relative})`,
|
|
44
|
-
`canonical: ${result.canonical}`,
|
|
45
|
-
`input: ${inputLine}`,
|
|
46
|
-
""
|
|
47
|
-
].join("\n");
|
|
48
|
-
}
|
|
49
|
-
function describeInputForm(input, canonical) {
|
|
50
|
-
if (input === canonical) return "canonical";
|
|
51
|
-
const notes = [];
|
|
52
|
-
if (input !== input.toLowerCase()) notes.push("was uppercase");
|
|
53
|
-
if (/[ilo]/i.test(input.slice(4))) notes.push("used Crockford aliases");
|
|
54
|
-
return `not canonical (${notes.join(" + ")})`;
|
|
55
|
-
}
|
|
56
|
-
const msPerMinute = 60 * 1e3;
|
|
57
|
-
const msPerHour = 60 * msPerMinute;
|
|
58
|
-
const msPerDay = 24 * msPerHour;
|
|
59
|
-
const daysPerMonth = 30.44;
|
|
60
|
-
const monthsPerYear = 12;
|
|
61
|
-
function formatRelative(thenMs, nowMs) {
|
|
62
|
-
const diff = nowMs - thenMs;
|
|
63
|
-
const abs = Math.abs(diff);
|
|
64
|
-
const suffix = diff < 0 ? "from now" : "ago";
|
|
65
|
-
const head = headUnits(abs);
|
|
66
|
-
return head === "" ? "just now" : `${head} ${suffix}`;
|
|
67
|
-
}
|
|
68
|
-
function headUnits(abs) {
|
|
69
|
-
if (abs < msPerMinute) return "";
|
|
70
|
-
if (abs < msPerHour) return unit(Math.round(abs / msPerMinute), "minute");
|
|
71
|
-
if (abs < msPerDay) return unit(Math.round(abs / msPerHour), "hour");
|
|
72
|
-
if (abs < msPerDay * daysPerMonth) return unit(Math.round(abs / msPerDay), "day");
|
|
73
|
-
const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));
|
|
74
|
-
if (totalMonths < monthsPerYear) return unit(totalMonths, "month");
|
|
75
|
-
const years = Math.floor(totalMonths / monthsPerYear);
|
|
76
|
-
const months = totalMonths % monthsPerYear;
|
|
77
|
-
return months === 0 ? unit(years, "year") : `${unit(years, "year")} ${unit(months, "month")}`;
|
|
78
|
-
}
|
|
79
|
-
function unit(n, name) {
|
|
80
|
-
return `${n} ${n === 1 ? name : `${name}s`}`;
|
|
81
|
-
}
|
|
82
|
-
//#endregion
|
|
83
50
|
//#region src/cli/constants.ts
|
|
84
51
|
const maxGenerateCount = 1e4;
|
|
85
52
|
//#endregion
|
|
@@ -188,527 +155,84 @@ function isKindError(result) {
|
|
|
188
155
|
return result !== "u32" && result !== "i32" && result !== "u64" && result !== "i64";
|
|
189
156
|
}
|
|
190
157
|
//#endregion
|
|
191
|
-
//#region src/cli/
|
|
192
|
-
function
|
|
193
|
-
return
|
|
194
|
-
}
|
|
195
|
-
function parseKeyFormatFlag(values) {
|
|
196
|
-
const fromFlag = values.get("--key-format");
|
|
197
|
-
if (fromFlag === void 0) return void 0;
|
|
198
|
-
if (fromFlag === "") return "--key-format requires a value";
|
|
199
|
-
if (fromFlag === "hex" || fromFlag === "base64url") return fromFlag;
|
|
200
|
-
return `--key-format must be hex or base64url, got '${fromFlag}'`;
|
|
158
|
+
//#region src/cli/format.ts
|
|
159
|
+
function formatCliError(err) {
|
|
160
|
+
return isIdsError(err) ? `${err.code}: ${err.message}` : err instanceof Error ? err.message : String(err);
|
|
201
161
|
}
|
|
202
|
-
function
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
162
|
+
function formatWrappedInspectOutput(result) {
|
|
163
|
+
const inputLine = describeInputForm(result.input, result.canonical);
|
|
164
|
+
return [
|
|
165
|
+
`brand: ${result.brand}`,
|
|
166
|
+
`lookup-key: ${result.lookupKey.toString()}`,
|
|
167
|
+
`canonical: ${result.canonical}`,
|
|
168
|
+
`input: ${inputLine}`,
|
|
169
|
+
""
|
|
170
|
+
].join("\n");
|
|
206
171
|
}
|
|
207
|
-
function
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return
|
|
172
|
+
function formatSignedInspectOutput(result) {
|
|
173
|
+
const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
|
|
174
|
+
const inputLine = describeInputForm(result.input, result.canonical);
|
|
175
|
+
const lines = [`brand: ${result.brand}`, `timestamp: ${result.timestamp.toISOString()} (${relative})`];
|
|
176
|
+
lines.push(`verification: ${result.verification}`);
|
|
177
|
+
lines.push(`canonical: ${result.canonical}`, `input: ${inputLine}`, "");
|
|
178
|
+
return lines.join("\n");
|
|
214
179
|
}
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
180
|
+
function formatInspectOutput(result) {
|
|
181
|
+
const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
|
|
182
|
+
const inputLine = describeInputForm(result.input, result.canonical);
|
|
183
|
+
return [
|
|
184
|
+
`brand: ${result.brand}`,
|
|
185
|
+
`timestamp: ${result.timestamp.toISOString()} (${relative})`,
|
|
186
|
+
`canonical: ${result.canonical}`,
|
|
187
|
+
`input: ${inputLine}`,
|
|
188
|
+
""
|
|
189
|
+
].join("\n");
|
|
223
190
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
import: importOpaqueKey
|
|
231
|
-
};
|
|
232
|
-
function parseOpaqueKeyFormat(values, opts) {
|
|
233
|
-
return parseKeyFormat(values, opts, opaqueFacet);
|
|
191
|
+
function describeInputForm(input, canonical) {
|
|
192
|
+
if (input === canonical) return "canonical";
|
|
193
|
+
const notes = [];
|
|
194
|
+
if (input !== input.toLowerCase()) notes.push("was uppercase");
|
|
195
|
+
if (/[ilo]/i.test(input.slice(4))) notes.push("used Crockford aliases");
|
|
196
|
+
return `not canonical (${notes.join(" + ")})`;
|
|
234
197
|
}
|
|
235
|
-
|
|
236
|
-
|
|
198
|
+
const msPerMinute = 60 * 1e3;
|
|
199
|
+
const msPerHour = 60 * msPerMinute;
|
|
200
|
+
const msPerDay = 24 * msPerHour;
|
|
201
|
+
const daysPerMonth = 30.44;
|
|
202
|
+
const monthsPerYear = 12;
|
|
203
|
+
function formatRelative(thenMs, nowMs) {
|
|
204
|
+
const diff = nowMs - thenMs;
|
|
205
|
+
const abs = Math.abs(diff);
|
|
206
|
+
const suffix = diff < 0 ? "from now" : "ago";
|
|
207
|
+
const head = headUnits(abs);
|
|
208
|
+
return head === "" ? "just now" : `${head} ${suffix}`;
|
|
237
209
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return
|
|
210
|
+
function headUnits(abs) {
|
|
211
|
+
if (abs < msPerMinute) return "";
|
|
212
|
+
if (abs < msPerHour) return unit(Math.round(abs / msPerMinute), "minute");
|
|
213
|
+
if (abs < msPerDay) return unit(Math.round(abs / msPerHour), "hour");
|
|
214
|
+
if (abs < msPerDay * daysPerMonth) return unit(Math.round(abs / msPerDay), "day");
|
|
215
|
+
const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));
|
|
216
|
+
if (totalMonths < monthsPerYear) return unit(totalMonths, "month");
|
|
217
|
+
const years = Math.floor(totalMonths / monthsPerYear);
|
|
218
|
+
const months = totalMonths % monthsPerYear;
|
|
219
|
+
return months === 0 ? unit(years, "year") : `${unit(years, "year")} ${unit(months, "month")}`;
|
|
248
220
|
}
|
|
249
|
-
|
|
250
|
-
return
|
|
221
|
+
function unit(n, name) {
|
|
222
|
+
return `${n} ${n === 1 ? name : `${name}s`}`;
|
|
251
223
|
}
|
|
252
224
|
//#endregion
|
|
253
|
-
//#region src/cli/
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
"-c",
|
|
258
|
-
"--bits",
|
|
259
|
-
"--key-format",
|
|
260
|
-
"--kind"
|
|
261
|
-
]));
|
|
262
|
-
const unsupported = unsupportedFlagForCommand("generate", flags, /* @__PURE__ */ new Set([
|
|
263
|
-
"--count",
|
|
264
|
-
"-c",
|
|
265
|
-
"--opaque",
|
|
266
|
-
"--reverse",
|
|
267
|
-
"--signed",
|
|
268
|
-
"--key-format"
|
|
269
|
-
]));
|
|
270
|
-
if (unsupported !== void 0) {
|
|
271
|
-
opts.stderr(unsupported + "\n");
|
|
272
|
-
return Promise.resolve(1);
|
|
273
|
-
}
|
|
274
|
-
if (errors[0] !== void 0) {
|
|
275
|
-
opts.stderr(errors[0] + "\n");
|
|
276
|
-
return Promise.resolve(1);
|
|
277
|
-
}
|
|
278
|
-
const extra = positionals[1];
|
|
279
|
-
if (extra !== void 0) {
|
|
280
|
-
opts.stderr(`unexpected argument: ${extra}\n`);
|
|
281
|
-
return Promise.resolve(1);
|
|
282
|
-
}
|
|
283
|
-
const [brand] = positionals;
|
|
284
|
-
const count = parseCount(values);
|
|
285
|
-
if (typeof count === "string") {
|
|
286
|
-
opts.stderr(count + "\n");
|
|
287
|
-
return Promise.resolve(1);
|
|
288
|
-
}
|
|
289
|
-
const opaque = flags.has("--opaque");
|
|
290
|
-
const reverse = flags.has("--reverse");
|
|
291
|
-
const signed = flags.has("--signed");
|
|
292
|
-
if (reverse && opaque) {
|
|
293
|
-
opts.stderr("cannot use --reverse and --opaque together\n");
|
|
294
|
-
return Promise.resolve(1);
|
|
295
|
-
}
|
|
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");
|
|
306
|
-
return Promise.resolve(1);
|
|
307
|
-
}
|
|
308
|
-
if (opaque) {
|
|
309
|
-
const format = parseOpaqueKeyFormat(values, opts);
|
|
310
|
-
if (isKeyFormatError(format)) {
|
|
311
|
-
opts.stderr(format + "\n");
|
|
312
|
-
return Promise.resolve(1);
|
|
313
|
-
}
|
|
314
|
-
return runOpaqueGenerate(brand ?? "", count, format, opts);
|
|
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
|
-
}
|
|
324
|
-
if (reverse) {
|
|
325
|
-
let codec;
|
|
225
|
+
//#region src/cli/variants.ts
|
|
226
|
+
const timestampVariant = {
|
|
227
|
+
inspectMode: "readable",
|
|
228
|
+
construct(brand, opts) {
|
|
326
229
|
try {
|
|
327
|
-
|
|
230
|
+
return createTimestampId(brand, codecOpts(opts));
|
|
328
231
|
} catch (err) {
|
|
329
|
-
|
|
330
|
-
return Promise.resolve(1);
|
|
232
|
+
return formatCliError(err);
|
|
331
233
|
}
|
|
332
|
-
for (let i = 0; i < count; i++) opts.stdout(codec.generate() + "\n");
|
|
333
|
-
return Promise.resolve(0);
|
|
334
|
-
}
|
|
335
|
-
let codec;
|
|
336
|
-
try {
|
|
337
|
-
codec = createTimestampId(brand ?? "", codecOpts(opts));
|
|
338
|
-
} catch (err) {
|
|
339
|
-
opts.stderr(formatCliError(err) + "\n");
|
|
340
|
-
return Promise.resolve(1);
|
|
341
|
-
}
|
|
342
|
-
for (let i = 0; i < count; i++) opts.stdout(codec.generate() + "\n");
|
|
343
|
-
return Promise.resolve(0);
|
|
344
|
-
}
|
|
345
|
-
async function runOpaqueGenerate(brand, count, format, opts) {
|
|
346
|
-
const keyResult = await loadOpaqueKey(opts, format);
|
|
347
|
-
if (typeof keyResult === "string") {
|
|
348
|
-
opts.stderr(keyResult + "\n");
|
|
349
|
-
return 1;
|
|
350
234
|
}
|
|
351
|
-
let codec;
|
|
352
|
-
try {
|
|
353
|
-
codec = createOpaqueTimestampId(brand, {
|
|
354
|
-
key: keyResult,
|
|
355
|
-
...codecOpts(opts)
|
|
356
|
-
});
|
|
357
|
-
} catch (err) {
|
|
358
|
-
opts.stderr(formatCliError(err) + "\n");
|
|
359
|
-
return 1;
|
|
360
|
-
}
|
|
361
|
-
for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
|
|
362
|
-
return 0;
|
|
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
|
-
}
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region src/cli/wrapping-key.ts
|
|
386
|
-
const wrappingFacet = {
|
|
387
|
-
envVar: "IDS_WRAPPING_KEY",
|
|
388
|
-
formatEnvVar: "IDS_WRAPPING_KEY_FORMAT",
|
|
389
|
-
decode: decodeWrappingKey,
|
|
390
|
-
import: importWrappingKey
|
|
391
235
|
};
|
|
392
|
-
function parseWrappingKeyFormat(values, opts) {
|
|
393
|
-
return parseKeyFormat(values, opts, wrappingFacet);
|
|
394
|
-
}
|
|
395
|
-
async function loadWrappingKey(opts, format) {
|
|
396
|
-
return loadKey(opts, format, wrappingFacet);
|
|
397
|
-
}
|
|
398
|
-
//#endregion
|
|
399
|
-
//#region src/cli/usage.ts
|
|
400
|
-
function usage() {
|
|
401
|
-
return [
|
|
402
|
-
"Usage: ids <subcommand> [args]",
|
|
403
|
-
"",
|
|
404
|
-
"Subcommands:",
|
|
405
|
-
" inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
|
|
406
|
-
" Decode an ID and print brand, timestamp (or lookup key), and canonical form.",
|
|
407
|
-
" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
|
|
408
|
-
" --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
|
|
409
|
-
" --kind is required with --wrapped: u32, i32, u64, or i64.",
|
|
410
|
-
" --reverse decodes a Reverse Timestamp ID (newest-first sort order).",
|
|
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]",
|
|
414
|
-
` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
|
|
415
|
-
" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
|
|
416
|
-
" --reverse mints Reverse Timestamp IDs (newest-first sort order).",
|
|
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).",
|
|
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).",
|
|
422
|
-
""
|
|
423
|
-
].join("\n");
|
|
424
|
-
}
|
|
425
|
-
//#endregion
|
|
426
|
-
//#region src/cli/commands/inspect.ts
|
|
427
|
-
function runInspect(args, opts) {
|
|
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([
|
|
436
|
-
"--opaque",
|
|
437
|
-
"--wrapped",
|
|
438
|
-
"--reverse",
|
|
439
|
-
"--signed",
|
|
440
|
-
"--kind",
|
|
441
|
-
"--key-format"
|
|
442
|
-
]));
|
|
443
|
-
if (unsupported !== void 0) {
|
|
444
|
-
opts.stderr(unsupported + "\n");
|
|
445
|
-
return Promise.resolve(1);
|
|
446
|
-
}
|
|
447
|
-
if (errors[0] !== void 0) {
|
|
448
|
-
opts.stderr(errors[0] + "\n");
|
|
449
|
-
return Promise.resolve(1);
|
|
450
|
-
}
|
|
451
|
-
const [input] = positionals;
|
|
452
|
-
if (input === void 0) {
|
|
453
|
-
opts.stderr(usage());
|
|
454
|
-
return Promise.resolve(1);
|
|
455
|
-
}
|
|
456
|
-
const extra = positionals[1];
|
|
457
|
-
if (extra !== void 0) {
|
|
458
|
-
opts.stderr(`unexpected argument: ${extra}\n`);
|
|
459
|
-
return Promise.resolve(1);
|
|
460
|
-
}
|
|
461
|
-
const opaque = flags.has("--opaque");
|
|
462
|
-
const wrapped = flags.has("--wrapped");
|
|
463
|
-
const reverse = flags.has("--reverse");
|
|
464
|
-
const signed = flags.has("--signed");
|
|
465
|
-
if (opaque && wrapped) {
|
|
466
|
-
opts.stderr("cannot use --wrapped and --opaque together\n");
|
|
467
|
-
return Promise.resolve(1);
|
|
468
|
-
}
|
|
469
|
-
if (reverse && opaque) {
|
|
470
|
-
opts.stderr("cannot use --reverse and --opaque together\n");
|
|
471
|
-
return Promise.resolve(1);
|
|
472
|
-
}
|
|
473
|
-
if (reverse && wrapped) {
|
|
474
|
-
opts.stderr("cannot use --reverse and --wrapped together\n");
|
|
475
|
-
return Promise.resolve(1);
|
|
476
|
-
}
|
|
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");
|
|
491
|
-
return Promise.resolve(1);
|
|
492
|
-
}
|
|
493
|
-
const brand = input.slice(0, 3).toLowerCase();
|
|
494
|
-
if (wrapped) {
|
|
495
|
-
const kind = parseKind(values);
|
|
496
|
-
if (kind === void 0) {
|
|
497
|
-
opts.stderr("--kind is required with --wrapped\n");
|
|
498
|
-
return Promise.resolve(1);
|
|
499
|
-
}
|
|
500
|
-
if (isKindError(kind)) {
|
|
501
|
-
opts.stderr(kind + "\n");
|
|
502
|
-
return Promise.resolve(1);
|
|
503
|
-
}
|
|
504
|
-
const format = parseWrappingKeyFormat(values, opts);
|
|
505
|
-
if (isKeyFormatError(format)) {
|
|
506
|
-
opts.stderr(format + "\n");
|
|
507
|
-
return Promise.resolve(1);
|
|
508
|
-
}
|
|
509
|
-
return runWrappedInspect(brand, input, kind, format, opts);
|
|
510
|
-
}
|
|
511
|
-
if (opaque) {
|
|
512
|
-
const format = parseOpaqueKeyFormat(values, opts);
|
|
513
|
-
if (isKeyFormatError(format)) {
|
|
514
|
-
opts.stderr(format + "\n");
|
|
515
|
-
return Promise.resolve(1);
|
|
516
|
-
}
|
|
517
|
-
return runOpaqueInspect(brand, input, format, opts);
|
|
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
|
-
}
|
|
527
|
-
if (reverse) {
|
|
528
|
-
let reverseCodec;
|
|
529
|
-
try {
|
|
530
|
-
reverseCodec = createReverseTimestampId(brand, codecOpts(opts));
|
|
531
|
-
} catch (err) {
|
|
532
|
-
opts.stderr(formatCliError(err) + "\n");
|
|
533
|
-
return Promise.resolve(1);
|
|
534
|
-
}
|
|
535
|
-
const reverseValidation = reverseCodec["~standard"].validate(input);
|
|
536
|
-
if (reverseValidation.issues) {
|
|
537
|
-
opts.stderr(reverseValidation.issues[0].message + "\n");
|
|
538
|
-
return Promise.resolve(1);
|
|
539
|
-
}
|
|
540
|
-
const reverseCanonical = reverseValidation.value;
|
|
541
|
-
const reverseTimestamp = reverseCodec.extractTimestamp(reverseCanonical);
|
|
542
|
-
const reverseNowMs = (opts.now ?? Date.now)();
|
|
543
|
-
opts.stdout(formatInspectOutput({
|
|
544
|
-
brand,
|
|
545
|
-
timestamp: reverseTimestamp,
|
|
546
|
-
canonical: reverseCanonical,
|
|
547
|
-
input,
|
|
548
|
-
nowMs: reverseNowMs
|
|
549
|
-
}));
|
|
550
|
-
return Promise.resolve(0);
|
|
551
|
-
}
|
|
552
|
-
let codec;
|
|
553
|
-
try {
|
|
554
|
-
codec = createTimestampId(brand, codecOpts(opts));
|
|
555
|
-
} catch (err) {
|
|
556
|
-
opts.stderr(formatCliError(err) + "\n");
|
|
557
|
-
return Promise.resolve(1);
|
|
558
|
-
}
|
|
559
|
-
const validation = codec["~standard"].validate(input);
|
|
560
|
-
if (validation.issues) {
|
|
561
|
-
opts.stderr(validation.issues[0].message + "\n");
|
|
562
|
-
return Promise.resolve(1);
|
|
563
|
-
}
|
|
564
|
-
const canonical = validation.value;
|
|
565
|
-
const timestamp = codec.extractTimestamp(canonical);
|
|
566
|
-
const nowMs = (opts.now ?? Date.now)();
|
|
567
|
-
opts.stdout(formatInspectOutput({
|
|
568
|
-
brand,
|
|
569
|
-
timestamp,
|
|
570
|
-
canonical,
|
|
571
|
-
input,
|
|
572
|
-
nowMs
|
|
573
|
-
}));
|
|
574
|
-
return Promise.resolve(0);
|
|
575
|
-
}
|
|
576
|
-
async function runWrappedInspect(brand, input, kind, format, opts) {
|
|
577
|
-
const keyResult = await loadWrappingKey(opts, format);
|
|
578
|
-
if (typeof keyResult === "string") {
|
|
579
|
-
opts.stderr(keyResult + "\n");
|
|
580
|
-
return 1;
|
|
581
|
-
}
|
|
582
|
-
let codec;
|
|
583
|
-
try {
|
|
584
|
-
codec = createWrappedKeyId(brand, {
|
|
585
|
-
kind,
|
|
586
|
-
keys: [keyResult],
|
|
587
|
-
allowDuplicateBrand: true
|
|
588
|
-
});
|
|
589
|
-
} catch (err) {
|
|
590
|
-
opts.stderr(formatCliError(err) + "\n");
|
|
591
|
-
return 1;
|
|
592
|
-
}
|
|
593
|
-
const validation = codec["~standard"].validate(input);
|
|
594
|
-
if (validation.issues) {
|
|
595
|
-
opts.stderr(validation.issues[0].message + "\n");
|
|
596
|
-
return 1;
|
|
597
|
-
}
|
|
598
|
-
const canonical = validation.value;
|
|
599
|
-
let lookupKey;
|
|
600
|
-
try {
|
|
601
|
-
lookupKey = await codec.unwrap(canonical);
|
|
602
|
-
} catch (err) {
|
|
603
|
-
opts.stderr(formatCliError(err) + "\n");
|
|
604
|
-
return 1;
|
|
605
|
-
}
|
|
606
|
-
opts.stdout(formatWrappedInspectOutput({
|
|
607
|
-
brand,
|
|
608
|
-
lookupKey,
|
|
609
|
-
canonical,
|
|
610
|
-
input
|
|
611
|
-
}));
|
|
612
|
-
return 0;
|
|
613
|
-
}
|
|
614
|
-
async function runOpaqueInspect(brand, input, format, opts) {
|
|
615
|
-
const keyResult = await loadOpaqueKey(opts, format);
|
|
616
|
-
if (typeof keyResult === "string") {
|
|
617
|
-
opts.stderr(keyResult + "\n");
|
|
618
|
-
return 1;
|
|
619
|
-
}
|
|
620
|
-
let codec;
|
|
621
|
-
try {
|
|
622
|
-
codec = createOpaqueTimestampId(brand, {
|
|
623
|
-
key: keyResult,
|
|
624
|
-
...codecOpts(opts)
|
|
625
|
-
});
|
|
626
|
-
} catch (err) {
|
|
627
|
-
opts.stderr(formatCliError(err) + "\n");
|
|
628
|
-
return 1;
|
|
629
|
-
}
|
|
630
|
-
const validation = codec["~standard"].validate(input);
|
|
631
|
-
if (validation.issues) {
|
|
632
|
-
opts.stderr(validation.issues[0].message + "\n");
|
|
633
|
-
return 1;
|
|
634
|
-
}
|
|
635
|
-
const canonical = validation.value;
|
|
636
|
-
const timestamp = await codec.extractTimestamp(canonical);
|
|
637
|
-
const nowMs = (opts.now ?? Date.now)();
|
|
638
|
-
opts.stderr("note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp\n");
|
|
639
|
-
opts.stdout(formatInspectOutput({
|
|
640
|
-
brand,
|
|
641
|
-
timestamp,
|
|
642
|
-
canonical,
|
|
643
|
-
input,
|
|
644
|
-
nowMs
|
|
645
|
-
}));
|
|
646
|
-
return 0;
|
|
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
236
|
const opaqueVariant = {
|
|
713
237
|
flag: "--opaque",
|
|
714
238
|
key: {
|
|
@@ -794,6 +318,25 @@ const conflictPriorityOrder = [
|
|
|
794
318
|
wrappedVariant,
|
|
795
319
|
opaqueVariant
|
|
796
320
|
];
|
|
321
|
+
const generatePolicy = {
|
|
322
|
+
default: timestampVariant,
|
|
323
|
+
selectable: [
|
|
324
|
+
opaqueVariant,
|
|
325
|
+
reverseVariant,
|
|
326
|
+
signedVariant
|
|
327
|
+
],
|
|
328
|
+
intrinsicFlags: ["--count", "-c"]
|
|
329
|
+
};
|
|
330
|
+
const inspectPolicy = {
|
|
331
|
+
default: timestampVariant,
|
|
332
|
+
selectable: [
|
|
333
|
+
reverseVariant,
|
|
334
|
+
wrappedVariant,
|
|
335
|
+
opaqueVariant,
|
|
336
|
+
signedVariant
|
|
337
|
+
],
|
|
338
|
+
intrinsicFlags: []
|
|
339
|
+
};
|
|
797
340
|
const keygenPolicy = {
|
|
798
341
|
default: opaqueVariant,
|
|
799
342
|
selectable: [wrappedVariant, signedVariant],
|
|
@@ -813,11 +356,248 @@ function deriveAllowedFlags(policy) {
|
|
|
813
356
|
return flags;
|
|
814
357
|
}
|
|
815
358
|
function resolveVariant(policy, flags) {
|
|
816
|
-
const selected = conflictPriorityOrder.filter((v) => policy.selectable.
|
|
359
|
+
const selected = conflictPriorityOrder.filter((v) => policy.selectable.some((d) => d === v) && v.flag !== void 0 && flags.has(v.flag));
|
|
817
360
|
if (selected.length === 0) return policy.default;
|
|
818
361
|
if (selected.length === 1) return selected[0];
|
|
819
362
|
return `cannot use ${selected[0].flag} and ${selected[1].flag} together`;
|
|
820
363
|
}
|
|
364
|
+
async function buildCodec(variant, brand, values, opts) {
|
|
365
|
+
let key;
|
|
366
|
+
if (variant.key !== void 0) {
|
|
367
|
+
const format = parseKeyFormat(values, opts, variant.key);
|
|
368
|
+
if (isKeyFormatError(format)) return format;
|
|
369
|
+
const keyResult = await loadKey(opts, format, variant.key);
|
|
370
|
+
if (typeof keyResult === "string") return keyResult;
|
|
371
|
+
key = keyResult;
|
|
372
|
+
}
|
|
373
|
+
return variant.construct(brand, opts, key, values);
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/cli/commands/generate.ts
|
|
377
|
+
async function runGenerate(args, opts) {
|
|
378
|
+
const allowedFlags = deriveAllowedFlags(generatePolicy);
|
|
379
|
+
const selectorFlags = new Set(generatePolicy.selectable.map((v) => v.flag).filter((f) => f !== void 0));
|
|
380
|
+
const { flags, values, positionals, errors } = splitFlags(args, new Set([...allowedFlags].filter((f) => !selectorFlags.has(f))));
|
|
381
|
+
const unsupported = unsupportedFlagForCommand("generate", flags, allowedFlags);
|
|
382
|
+
if (unsupported !== void 0) {
|
|
383
|
+
opts.stderr(unsupported + "\n");
|
|
384
|
+
return 1;
|
|
385
|
+
}
|
|
386
|
+
if (errors[0] !== void 0) {
|
|
387
|
+
opts.stderr(errors[0] + "\n");
|
|
388
|
+
return 1;
|
|
389
|
+
}
|
|
390
|
+
const extra = positionals[1];
|
|
391
|
+
if (extra !== void 0) {
|
|
392
|
+
opts.stderr(`unexpected argument: ${extra}\n`);
|
|
393
|
+
return 1;
|
|
394
|
+
}
|
|
395
|
+
const [brand] = positionals;
|
|
396
|
+
const count = parseCount(values);
|
|
397
|
+
if (typeof count === "string") {
|
|
398
|
+
opts.stderr(count + "\n");
|
|
399
|
+
return 1;
|
|
400
|
+
}
|
|
401
|
+
const variant = resolveVariant(generatePolicy, flags);
|
|
402
|
+
if (typeof variant === "string") {
|
|
403
|
+
opts.stderr(variant + "\n");
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
if (variant.key === void 0 && flags.has("--key-format")) {
|
|
407
|
+
opts.stderr("--key-format requires --opaque or --signed\n");
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
const codec = await buildCodec(variant, brand ?? "", values, opts);
|
|
411
|
+
if (typeof codec === "string") {
|
|
412
|
+
opts.stderr(codec + "\n");
|
|
413
|
+
return 1;
|
|
414
|
+
}
|
|
415
|
+
for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
|
|
416
|
+
return 0;
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/cli/usage.ts
|
|
420
|
+
function usage() {
|
|
421
|
+
return [
|
|
422
|
+
"Usage: ids <subcommand> [args]",
|
|
423
|
+
"",
|
|
424
|
+
"Subcommands:",
|
|
425
|
+
" inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
|
|
426
|
+
" Decode an ID and print brand, timestamp (or lookup key), and canonical form.",
|
|
427
|
+
" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
|
|
428
|
+
" --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
|
|
429
|
+
" --kind is required with --wrapped: u32, i32, u64, or i64.",
|
|
430
|
+
" --reverse decodes a Reverse Timestamp ID (newest-first sort order).",
|
|
431
|
+
" --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
|
|
432
|
+
" Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
|
|
433
|
+
" generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--key-format hex|base64url]",
|
|
434
|
+
` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
|
|
435
|
+
" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
|
|
436
|
+
" --reverse mints Reverse Timestamp IDs (newest-first sort order).",
|
|
437
|
+
" --signed mints Signed Timestamp IDs; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
|
|
438
|
+
" keygen, k [--wrapped] [--signed] [--bits 128|192|256] [--key-format hex|base64url]",
|
|
439
|
+
" Emit a random key for importOpaqueKey, importWrappingKey, or importSigningKey (stdout only).",
|
|
440
|
+
" --wrapped emits a wrapping key for importWrappingKey instead (IDS_WRAPPING_KEY).",
|
|
441
|
+
" --signed emits a signing key for importSigningKey instead (IDS_SIGNING_KEY; hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
|
|
442
|
+
""
|
|
443
|
+
].join("\n");
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region src/cli/commands/inspect.ts
|
|
447
|
+
async function runInspect(args, opts) {
|
|
448
|
+
const allowedFlags = deriveAllowedFlags(inspectPolicy);
|
|
449
|
+
const selectorFlags = new Set(inspectPolicy.selectable.map((v) => v.flag).filter((f) => f !== void 0));
|
|
450
|
+
const { flags, values, positionals, errors } = splitFlags(args, new Set([...allowedFlags].filter((f) => !selectorFlags.has(f))));
|
|
451
|
+
const unsupported = unsupportedFlagForCommand("inspect", flags, allowedFlags);
|
|
452
|
+
if (unsupported !== void 0) {
|
|
453
|
+
opts.stderr(unsupported + "\n");
|
|
454
|
+
return 1;
|
|
455
|
+
}
|
|
456
|
+
if (errors[0] !== void 0) {
|
|
457
|
+
opts.stderr(errors[0] + "\n");
|
|
458
|
+
return 1;
|
|
459
|
+
}
|
|
460
|
+
const [input] = positionals;
|
|
461
|
+
if (input === void 0) {
|
|
462
|
+
opts.stderr(usage());
|
|
463
|
+
return 1;
|
|
464
|
+
}
|
|
465
|
+
const extra = positionals[1];
|
|
466
|
+
if (extra !== void 0) {
|
|
467
|
+
opts.stderr(`unexpected argument: ${extra}\n`);
|
|
468
|
+
return 1;
|
|
469
|
+
}
|
|
470
|
+
const variant = resolveVariant(inspectPolicy, flags);
|
|
471
|
+
if (typeof variant === "string") {
|
|
472
|
+
opts.stderr(variant + "\n");
|
|
473
|
+
return 1;
|
|
474
|
+
}
|
|
475
|
+
if (variant.key === void 0 && flags.has("--key-format")) {
|
|
476
|
+
opts.stderr("--key-format requires --opaque, --wrapped, or --signed\n");
|
|
477
|
+
return 1;
|
|
478
|
+
}
|
|
479
|
+
const brand = input.slice(0, 3).toLowerCase();
|
|
480
|
+
let verifyTimestamp;
|
|
481
|
+
let verifyCanonical;
|
|
482
|
+
let verifyNowMs;
|
|
483
|
+
if (variant.inspectMode === "verify") {
|
|
484
|
+
const fmtCheck = parseKeyFormat(values, opts, variant.key);
|
|
485
|
+
if (isKeyFormatError(fmtCheck)) {
|
|
486
|
+
opts.stderr(fmtCheck + "\n");
|
|
487
|
+
return 1;
|
|
488
|
+
}
|
|
489
|
+
let tsCodec;
|
|
490
|
+
try {
|
|
491
|
+
tsCodec = createTimestampId(brand, codecOpts(opts));
|
|
492
|
+
} catch (err) {
|
|
493
|
+
opts.stderr(formatCliError(err) + "\n");
|
|
494
|
+
return 1;
|
|
495
|
+
}
|
|
496
|
+
const structValidation = tsCodec["~standard"].validate(input);
|
|
497
|
+
if (structValidation.issues) {
|
|
498
|
+
opts.stderr(structValidation.issues[0].message + "\n");
|
|
499
|
+
return 1;
|
|
500
|
+
}
|
|
501
|
+
verifyCanonical = structValidation.value;
|
|
502
|
+
verifyTimestamp = tsCodec.extractTimestamp(verifyCanonical);
|
|
503
|
+
verifyNowMs = (opts.now ?? Date.now)();
|
|
504
|
+
}
|
|
505
|
+
const codecOrError = await buildCodec(variant, brand, values, opts);
|
|
506
|
+
if (typeof codecOrError === "string") {
|
|
507
|
+
if (variant.inspectMode === "verify") opts.stdout(formatSignedInspectOutput({
|
|
508
|
+
brand,
|
|
509
|
+
timestamp: verifyTimestamp,
|
|
510
|
+
canonical: verifyCanonical,
|
|
511
|
+
input,
|
|
512
|
+
nowMs: verifyNowMs,
|
|
513
|
+
verification: "unavailable"
|
|
514
|
+
}));
|
|
515
|
+
opts.stderr(codecOrError + "\n");
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
let canonical;
|
|
519
|
+
if (variant.inspectMode !== "verify") {
|
|
520
|
+
const validation = codecOrError["~standard"].validate(input);
|
|
521
|
+
if (validation.issues) {
|
|
522
|
+
opts.stderr(validation.issues[0].message + "\n");
|
|
523
|
+
return 1;
|
|
524
|
+
}
|
|
525
|
+
canonical = validation.value;
|
|
526
|
+
}
|
|
527
|
+
switch (variant.inspectMode) {
|
|
528
|
+
case "readable": {
|
|
529
|
+
const timestamp = codecOrError.extractTimestamp(canonical);
|
|
530
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
531
|
+
opts.stdout(formatInspectOutput({
|
|
532
|
+
brand,
|
|
533
|
+
timestamp,
|
|
534
|
+
canonical,
|
|
535
|
+
input,
|
|
536
|
+
nowMs
|
|
537
|
+
}));
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
case "keyed-readable": {
|
|
541
|
+
const timestamp = await codecOrError.extractTimestamp(canonical);
|
|
542
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
543
|
+
opts.stderr("note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp\n");
|
|
544
|
+
opts.stdout(formatInspectOutput({
|
|
545
|
+
brand,
|
|
546
|
+
timestamp,
|
|
547
|
+
canonical,
|
|
548
|
+
input,
|
|
549
|
+
nowMs
|
|
550
|
+
}));
|
|
551
|
+
return 0;
|
|
552
|
+
}
|
|
553
|
+
case "unwrap": {
|
|
554
|
+
let lookupKey;
|
|
555
|
+
try {
|
|
556
|
+
lookupKey = await codecOrError.unwrap(canonical);
|
|
557
|
+
} catch (err) {
|
|
558
|
+
opts.stderr(formatCliError(err) + "\n");
|
|
559
|
+
return 1;
|
|
560
|
+
}
|
|
561
|
+
opts.stdout(formatWrappedInspectOutput({
|
|
562
|
+
brand,
|
|
563
|
+
lookupKey,
|
|
564
|
+
canonical,
|
|
565
|
+
input
|
|
566
|
+
}));
|
|
567
|
+
return 0;
|
|
568
|
+
}
|
|
569
|
+
case "verify": {
|
|
570
|
+
const verifyResult = await codecOrError.safeVerify(input);
|
|
571
|
+
if (!verifyResult.ok) {
|
|
572
|
+
/* v8 ignore next 4 -- defensive: both codecs share the same wire parse so ParseError
|
|
573
|
+
is unreachable after the createTimestampId pre-validation above passes */
|
|
574
|
+
if (verifyResult.error !== "verification_failed") {
|
|
575
|
+
opts.stderr(verifyResult.error + "\n");
|
|
576
|
+
return 1;
|
|
577
|
+
}
|
|
578
|
+
opts.stdout(formatSignedInspectOutput({
|
|
579
|
+
brand,
|
|
580
|
+
timestamp: verifyTimestamp,
|
|
581
|
+
canonical: verifyCanonical,
|
|
582
|
+
input,
|
|
583
|
+
nowMs: verifyNowMs,
|
|
584
|
+
verification: "failed"
|
|
585
|
+
}));
|
|
586
|
+
opts.stderr("verification_failed: verification failed\n");
|
|
587
|
+
return 1;
|
|
588
|
+
}
|
|
589
|
+
opts.stdout(formatSignedInspectOutput({
|
|
590
|
+
brand,
|
|
591
|
+
timestamp: verifyTimestamp,
|
|
592
|
+
canonical: verifyResult.id,
|
|
593
|
+
input,
|
|
594
|
+
nowMs: verifyNowMs,
|
|
595
|
+
verification: "ok"
|
|
596
|
+
}));
|
|
597
|
+
return 0;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
821
601
|
//#endregion
|
|
822
602
|
//#region src/cli/commands/keygen.ts
|
|
823
603
|
function runKeygen(args, opts) {
|