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