@smonn/ids 0.13.0 → 0.14.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 +36 -44
- package/dist/{adapter-types-CIc-4O-P.d.mts → adapter-types-Bia_w9sg.d.mts} +2 -2
- package/dist/{adapter-types-CIc-4O-P.d.mts.map → adapter-types-Bia_w9sg.d.mts.map} +1 -1
- package/dist/cli.mjs +165 -92
- package/dist/cli.mjs.map +1 -1
- package/dist/{codec-shell-DvrTDa65.mjs → codec-shell-BRZkuQeP.mjs} +93 -7
- package/dist/codec-shell-BRZkuQeP.mjs.map +1 -0
- package/dist/{digest-Drnof-l_.mjs → digest-CLJEGBxo.mjs} +7 -4
- package/dist/{digest-Drnof-l_.mjs.map → digest-CLJEGBxo.mjs.map} +1 -1
- package/dist/digest.d.mts +19 -2
- package/dist/digest.d.mts.map +1 -1
- package/dist/digest.mjs +1 -1
- package/dist/drizzle.d.mts +3 -3
- package/dist/{error-Dqyho9vp.d.mts → error-CifcKKOG.d.mts} +2 -2
- package/dist/{error-Dqyho9vp.d.mts.map → error-CifcKKOG.d.mts.map} +1 -1
- package/dist/express.d.mts +2 -2
- package/dist/fastify.d.mts +2 -2
- package/dist/graphql.d.mts +2 -2
- package/dist/hono.d.mts +2 -2
- package/dist/index.d.mts +19 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{key-material-DsukgnR5.mjs → key-material-1wOKJ1o-.mjs} +2 -2
- package/dist/key-material-1wOKJ1o-.mjs.map +1 -0
- package/dist/kysely.d.mts +3 -3
- package/dist/mikro-orm.d.mts +3 -3
- package/dist/nestjs.d.mts +2 -2
- package/dist/{opaque-D7y5cgzT.mjs → opaque-COAcIIY4.mjs} +14 -5
- package/dist/opaque-COAcIIY4.mjs.map +1 -0
- package/dist/opaque.d.mts +26 -2
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +1 -1
- package/dist/prisma.d.mts +3 -3
- package/dist/{reverse-DrAofYWV.mjs → reverse-CT-El3hi.mjs} +7 -4
- package/dist/{reverse-DrAofYWV.mjs.map → reverse-CT-El3hi.mjs.map} +1 -1
- package/dist/reverse.d.mts +19 -2
- package/dist/reverse.d.mts.map +1 -1
- package/dist/reverse.mjs +1 -1
- package/dist/{rng-Clos6uC0.mjs → rng-6GyNT4zS.mjs} +2 -2
- package/dist/{rng-Clos6uC0.mjs.map → rng-6GyNT4zS.mjs.map} +1 -1
- package/dist/{signed-B2Aa3zMg.mjs → signed-Dkdteu1y.mjs} +8 -5
- package/dist/{signed-B2Aa3zMg.mjs.map → signed-Dkdteu1y.mjs.map} +1 -1
- package/dist/signed.d.mts +19 -2
- package/dist/signed.d.mts.map +1 -1
- package/dist/signed.mjs +1 -1
- package/dist/{timestamp-YPd58344.mjs → timestamp-RXXwHfHO.mjs} +7 -4
- package/dist/{timestamp-YPd58344.mjs.map → timestamp-RXXwHfHO.mjs.map} +1 -1
- package/dist/typeorm.d.mts +2 -2
- package/dist/{types-wplmOgOK.d.mts → types-hGBnCpJj.d.mts} +3 -3
- package/dist/{types-wplmOgOK.d.mts.map → types-hGBnCpJj.d.mts.map} +1 -1
- package/dist/{wrapped-BjmVzuYc.mjs → wrapped-Oj2hC1vB.mjs} +15 -4
- package/dist/wrapped-Oj2hC1vB.mjs.map +1 -0
- package/dist/wrapped.d.mts +27 -2
- package/dist/wrapped.d.mts.map +1 -1
- package/dist/wrapped.mjs +1 -1
- package/package.json +1 -1
- package/dist/codec-shell-DvrTDa65.mjs.map +0 -1
- package/dist/key-material-DsukgnR5.mjs.map +0 -1
- package/dist/opaque-D7y5cgzT.mjs.map +0 -1
- package/dist/wrapped-BjmVzuYc.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -8,10 +8,7 @@ Public-facing branded IDs for TypeScript apps. Type-safe, sortable, and codec-pl
|
|
|
8
8
|
pnpm add @smonn/ids
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Each ID looks like `
|
|
12
|
-
underscore, then 26 Crockford base32 characters of payload. The default
|
|
13
|
-
Timestamp codec encodes a 48-bit millisecond Unix timestamp followed by 80
|
|
14
|
-
random bits — the same byte layout as a [ULID](https://github.com/ulid/spec).
|
|
11
|
+
Each ID looks like `usr_06f80z92d2dbsqqg28t5cy4tqg`: a three-letter brand, an underscore, then 26 Crockford base32 characters of payload. The default Timestamp codec encodes a 48-bit millisecond Unix timestamp followed by 80 random bits — the same byte layout as a [ULID](https://github.com/ulid/spec).
|
|
15
12
|
|
|
16
13
|
## Quickstart
|
|
17
14
|
|
|
@@ -21,7 +18,7 @@ import { type Id, createTimestampId } from "@smonn/ids";
|
|
|
21
18
|
const users = createTimestampId("usr");
|
|
22
19
|
|
|
23
20
|
// Generate — sortable by creation time via ORDER BY id
|
|
24
|
-
const id = users.generate(); // "
|
|
21
|
+
const id = users.generate(); // "usr_06f80z92d2dbsqqg28t5cy4tqg"
|
|
25
22
|
|
|
26
23
|
// Branded: Id<"usr"> and Id<"org"> are not interchangeable
|
|
27
24
|
function loadUser(id: Id<"usr">) {
|
|
@@ -29,35 +26,28 @@ function loadUser(id: Id<"usr">) {
|
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
// Validate untrusted input — lenient in, canonical out
|
|
32
|
-
const r = users.safeParse("
|
|
29
|
+
const r = users.safeParse("USR_06F80Z92D2DBSQQG28T5CY4TQG");
|
|
33
30
|
if (r.ok) {
|
|
34
|
-
r.id; // "
|
|
31
|
+
r.id; // "usr_06f80z92d2dbsqqg28t5cy4tqg" as Id<"usr">
|
|
35
32
|
}
|
|
36
33
|
```
|
|
37
34
|
|
|
38
|
-
`safeParse` accepts mixed case and the Crockford visual aliases (`o → 0`,
|
|
39
|
-
`i → 1`, `l → 1`) and always returns the canonical lowercase form. See the
|
|
40
|
-
[Timestamp codec guide](https://ids.smonn.se/codecs/timestamp/) for sorting,
|
|
41
|
-
backfills (`generateAt`), range queries, structured errors, Standard Schema, and
|
|
42
|
-
JSON Schema.
|
|
35
|
+
`safeParse` accepts mixed case and the Crockford visual aliases (`o → 0`, `i → 1`, `l → 1`) and always returns the canonical lowercase form. See the [Timestamp codec guide](https://ids.smonn.se/codecs/timestamp/) for sorting, backfills (`generateAt`), range queries, structured errors, Standard Schema, and JSON Schema.
|
|
43
36
|
|
|
44
37
|
## Choosing a codec
|
|
45
38
|
|
|
46
|
-
All six codecs share the same `<brand>_<26 chars>` wire shape but make different
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
| Opaque Timestamp | `@smonn/ids/opaque` | None (encrypted) | Yes (AES key) | With key only |
|
|
59
|
-
| Wrapped key | `@smonn/ids/wrapped` | None | Yes (wrapping key) | N/A — not timestamp-family |
|
|
60
|
-
| Digest | `@smonn/ids/digest` | None | Yes (digest key) | N/A — not timestamp-family |
|
|
39
|
+
All six codecs share the same `<brand>_<26 chars>` wire shape but make different trade-offs. They are wire-indistinguishable — `safeParse`, `is`, and `parse` cannot distinguish an Opaque Timestamp ID from a Timestamp ID at runtime. Cross-codec confusion is undetectable by the library; the consumer is responsible for routing a given ID to the correct codec for the brand. Codec choice is therefore a per-brand commitment.
|
|
40
|
+
|
|
41
|
+
| Codec | Import | Sort direction | Key required | Timestamp extractable |
|
|
42
|
+
| --- | --- | --- | --- | --- |
|
|
43
|
+
| Timestamp | `@smonn/ids` | Ascending (oldest-first) | No | Always (plaintext) |
|
|
44
|
+
| Reverse Timestamp | `@smonn/ids/reverse` | Descending (newest-first) | No | Always (plaintext) |
|
|
45
|
+
| Signed Timestamp | `@smonn/ids/signed` | Ascending (oldest-first) | Yes (signing key) | Always (plaintext) |
|
|
46
|
+
| Opaque Timestamp | `@smonn/ids/opaque` | None (encrypted) | Yes (AES key) | With key only |
|
|
47
|
+
| Wrapped key | `@smonn/ids/wrapped` | None | Yes (wrapping key) | N/A — not timestamp-family |
|
|
48
|
+
| Digest | `@smonn/ids/digest` | None | Yes (digest key) | N/A — not timestamp-family |
|
|
49
|
+
|
|
50
|
+
The Timestamp codec is the default and ships from the root `@smonn/ids` entry — it has no `/timestamp` subpath by design. If you try `import ... from "@smonn/ids/timestamp"` you will get a module-resolution error; use `@smonn/ids` directly. Every other codec uses a named subpath (`/reverse`, `/signed`, `/opaque`, `/wrapped`, `/digest`); this asymmetry is intentional and permanent.
|
|
61
51
|
|
|
62
52
|
- **Newest-first scans** on forward-only KV stores → [Reverse Timestamp](https://ids.smonn.se/codecs/reverse/)
|
|
63
53
|
- **Tamper-evident share links** verified without a DB lookup → [Signed Timestamp](https://ids.smonn.se/codecs/signed/) (integrity)
|
|
@@ -69,40 +59,33 @@ Try them all live in the [playground](https://ids.smonn.se/playground/).
|
|
|
69
59
|
|
|
70
60
|
## Integrations
|
|
71
61
|
|
|
72
|
-
Framework and ORM adapters ship as optional subpath exports (each requires its
|
|
73
|
-
own peer dependency):
|
|
62
|
+
Framework and ORM adapters ship as optional subpath exports (each requires its own peer dependency):
|
|
74
63
|
|
|
75
64
|
- **HTTP route params:** [Hono](https://ids.smonn.se/adapters/hono/), [Express](https://ids.smonn.se/adapters/express/), [Fastify](https://ids.smonn.se/adapters/fastify/) — `idParam` middleware; [NestJS](https://ids.smonn.se/adapters/nestjs/) — `ParseIdPipe`
|
|
76
65
|
- **ORM columns:** [Drizzle](https://ids.smonn.se/adapters/drizzle/) — `idColumn`, [Kysely](https://ids.smonn.se/adapters/kysely/) — `idColumn`, [MikroORM](https://ids.smonn.se/adapters/mikro-orm/) — `idType`, [Prisma](https://ids.smonn.se/adapters/prisma/) — `idField`, [TypeORM](https://ids.smonn.se/adapters/typeorm/) — `idTransformer`
|
|
77
66
|
- **GraphQL:** [GraphQL](https://ids.smonn.se/adapters/graphql/) — `idScalar` custom scalar
|
|
78
67
|
- **CLI:** brand-agnostic `inspect` / `generate` / `keygen` — `npx @smonn/ids --help` ([docs](https://ids.smonn.se/cli/))
|
|
79
68
|
|
|
80
|
-
Every codec also implements [Standard Schema v1](https://standardschema.dev/), so
|
|
81
|
-
|
|
69
|
+
Every codec also implements [Standard Schema v1](https://standardschema.dev/), so it slots into Zod, Valibot, ArkType, tRPC, and any validator-aware library.
|
|
70
|
+
|
|
71
|
+
**Native `uuid` column storage:** an `Id<Brand>` can be persisted into a native `uuid` column via `codec.toUUID(id)` and read back as a branded ID via `codec.fromUUID(value)` or `codec.safeFromUUID(value)` — a lossless round-trip useful when migrating off UUID primary keys while keeping existing column types and indexes.
|
|
82
72
|
|
|
83
73
|
## What this is **not** for
|
|
84
74
|
|
|
85
|
-
- **Internal surrogate primary keys.** If nobody outside your service sees the
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
- **Wire-compatible ULIDs.** The byte layout is ULID-shaped, but the encoding is
|
|
89
|
-
lowercase and brand-wrapped. Stock ULID parsers will reject these.
|
|
75
|
+
- **Internal surrogate primary keys.** If nobody outside your service sees the ID, the brand prefix and lenient parsing are dead weight. Use a `bigint` sequence.
|
|
76
|
+
- **Wire-compatible ULIDs.** The byte layout is ULID-shaped, but the encoding is lowercase and brand-wrapped. Stock ULID parsers will reject these.
|
|
77
|
+
- **Spec-valid UUIDv7 output.** `toUUID` produces a **raw, unversioned** UUID — all 128 payload bits are preserved verbatim (lossless round-trip), which means the version/variant nibble positions hold real data, not `0x7`/`0b10`. It is **not** a spec-valid UUIDv7. Only the Timestamp codec happens to produce a UUID whose leading 48 bits are a real millisecond timestamp; only Timestamp and Reverse Timestamp produce time-sortable UUIDs. Importing a non-time-ordered UUID (e.g. a UUIDv4) into a timestamp-family codec via `fromUUID` yields a structurally valid `Id<Brand>` with a meaningless timestamp and random sort order — the same wire-indistinguishable contract that already governs codec variants.
|
|
90
78
|
- **Distributed-trace / request-correlation IDs.** Use OpenTelemetry-format IDs.
|
|
91
|
-
- **Hiding creation time with the Timestamp codec.** Anyone with one ID at a
|
|
92
|
-
known creation time can compute the epoch offset. Use the Opaque Timestamp
|
|
93
|
-
codec to hide creation time per-ID.
|
|
79
|
+
- **Hiding creation time with the Timestamp codec.** Anyone with one ID at a known creation time can compute the epoch offset. Use the Opaque Timestamp codec to hide creation time per-ID.
|
|
94
80
|
|
|
95
81
|
## API surface
|
|
96
82
|
|
|
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.
|
|
83
|
+
Exports from the main `@smonn/ids` entry point only. Codec-specific subpath exports (`@smonn/ids/reverse`, `@smonn/ids/opaque`, `@smonn/ids/signed`, `@smonn/ids/wrapped`, `@smonn/ids/digest`) and adapter subpaths are not listed here.
|
|
101
84
|
|
|
102
85
|
### Types
|
|
103
86
|
|
|
104
87
|
- `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"`)
|
|
88
|
+
- `ParseError` — Parse failure reason string returned by `safeParse()` (`"not_string"`, `"invalid_prefix"`, or `"invalid_base32"`) and by `safeFromUUID()` (`"not_string"` or `"invalid_uuid"`).
|
|
106
89
|
- `ParseResult<Brand>` — Discriminated union returned by `safeParse()`: `{ ok: true; id: Id<Brand> }` or `{ ok: false; error: ParseError }`.
|
|
107
90
|
- `JsonSchema` — Shape of the object returned by a codec's `toJsonSchema()`.
|
|
108
91
|
- `IdsErrorCode` — String-literal union of the eleven stable error codes carried by `IdsError`.
|
|
@@ -119,9 +102,18 @@ here.
|
|
|
119
102
|
- `isIdsError(value)` — Type guard for `IdsError`; uses an internal brand to survive ESM/CJS dual-package duplication where bare `instanceof` fails.
|
|
120
103
|
- `createTimestampId(brand, options?)` — Creates a Timestamp codec for `brand` (three lowercase `a–z` characters).
|
|
121
104
|
|
|
105
|
+
### Shared codec methods (all variants)
|
|
106
|
+
|
|
107
|
+
Every codec instance exposes the following UUID interop methods in addition to the codec-specific ones documented on the [full docs site](https://ids.smonn.se):
|
|
108
|
+
|
|
109
|
+
- `toUUID(id)` — Takes a trusted `Id<Brand>`, returns the 16-byte payload reinterpreted as a canonical lowercase-hyphenated UUID `string`. Total — cannot fail. The brand is shed; the output is a plain `string`, not a branded type.
|
|
110
|
+
- `fromUUID(value)` — Takes an untrusted `string`, returns `Id<Brand>`. Throws `IdsError` (`code: "invalid_id"`) with a `ParseError` on `cause` (`"invalid_uuid"`, or `"not_string"` for untyped JavaScript callers) on malformed input.
|
|
111
|
+
- `safeFromUUID(value)` — Takes `unknown`, returns `ParseResult<Brand>` (`{ ok: true; id }` or `{ ok: false; error: ParseError }`). Never throws.
|
|
112
|
+
|
|
122
113
|
## Links
|
|
123
114
|
|
|
124
115
|
- **[Documentation](https://ids.smonn.se)** — full guides, API reference, and playground
|
|
116
|
+
- **[SPEC.md](./SPEC.md)** — descriptive wire-format specification
|
|
125
117
|
- **[Design decisions](./docs/adr/)** — recorded ADRs
|
|
126
118
|
- **[CONTEXT.md](./CONTEXT.md)** — glossary of the project's vocabulary
|
|
127
119
|
- **[Contributing](./CONTRIBUTING.md)** · **[Security](./SECURITY.md)**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as ParseResult } from "./types-
|
|
1
|
+
import { i as ParseResult } from "./types-hGBnCpJj.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/adapters/adapter-types.d.ts
|
|
4
4
|
/** Discriminated failure value passed to `onError` and emitted to the framework's error handler. */
|
|
@@ -17,4 +17,4 @@ type IdCodec<Brand extends string> = {
|
|
|
17
17
|
type IdColumnCodec<Brand extends string> = IdCodec<Brand>;
|
|
18
18
|
//#endregion
|
|
19
19
|
export { IdColumnCodec as n, IdParamFailure as r, IdCodec as t };
|
|
20
|
-
//# sourceMappingURL=adapter-types-
|
|
20
|
+
//# sourceMappingURL=adapter-types-Bia_w9sg.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter-types-
|
|
1
|
+
{"version":3,"file":"adapter-types-Bia_w9sg.d.mts","names":[],"sources":["../src/adapters/adapter-types.ts"],"mappings":";;;;KAIY,cAAA;EAAA,SACG,MAAA;EAAA,SAAmC,MAAA;AAAA;EAAA,SACnC,MAAA;EAAA,SAA8B,MAAA;AAAA;;KAGjC,OAAA;EACV,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;AADzC;AAAA,KAKY,aAAA,yBAAsC,OAAA,CAAQ,KAAA"}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { n as isIdsError } from "./error-Cp5qYZcv.mjs";
|
|
3
|
-
import { t as createTimestampId } from "./timestamp-
|
|
4
|
-
import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-
|
|
5
|
-
import { t as createReverseTimestampId } from "./reverse-
|
|
6
|
-
import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-
|
|
7
|
-
import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-
|
|
8
|
-
import { i as importDigestKey, n as decodeDigestKey, r as encodeDigestKey, t as createDigestId } from "./digest-
|
|
3
|
+
import { t as createTimestampId } from "./timestamp-RXXwHfHO.mjs";
|
|
4
|
+
import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-COAcIIY4.mjs";
|
|
5
|
+
import { t as createReverseTimestampId } from "./reverse-CT-El3hi.mjs";
|
|
6
|
+
import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-Dkdteu1y.mjs";
|
|
7
|
+
import { i as importWrappingKey, n as decodeWrappingKey, r as encodeWrappingKey, t as createWrappedKeyId } from "./wrapped-Oj2hC1vB.mjs";
|
|
8
|
+
import { i as importDigestKey, n as decodeDigestKey, r as encodeDigestKey, t as createDigestId } from "./digest-CLJEGBxo.mjs";
|
|
9
|
+
//#region src/cli/format.ts
|
|
10
|
+
const invalidIdPrefix = "invalid_id: ";
|
|
11
|
+
function formatCliError(err) {
|
|
12
|
+
return isIdsError(err) ? `${err.code}: ${err.message}` : err instanceof Error ? err.message : String(err);
|
|
13
|
+
}
|
|
14
|
+
function formatWrappedInspectOutput(result) {
|
|
15
|
+
const inputLine = describeInputForm(result.input, result.canonical);
|
|
16
|
+
return [
|
|
17
|
+
`brand: ${result.brand}`,
|
|
18
|
+
`lookup-key: ${result.lookupKey.toString()}`,
|
|
19
|
+
`canonical: ${result.canonical}`,
|
|
20
|
+
`uuid: ${result.uuid}`,
|
|
21
|
+
`input: ${inputLine}`,
|
|
22
|
+
""
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
function formatSignedInspectOutput(result) {
|
|
26
|
+
const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
|
|
27
|
+
const inputLine = describeInputForm(result.input, result.canonical);
|
|
28
|
+
const lines = [`brand: ${result.brand}`, `timestamp: ${result.timestamp.toISOString()} (${relative})`];
|
|
29
|
+
lines.push(`verification: ${result.verification}`);
|
|
30
|
+
lines.push(`canonical: ${result.canonical}`, `uuid: ${result.uuid}`, `input: ${inputLine}`, "");
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
function formatInspectOutput(result) {
|
|
34
|
+
const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
|
|
35
|
+
const inputLine = describeInputForm(result.input, result.canonical);
|
|
36
|
+
return [
|
|
37
|
+
`brand: ${result.brand}`,
|
|
38
|
+
`timestamp: ${result.timestamp.toISOString()} (${relative})`,
|
|
39
|
+
`canonical: ${result.canonical}`,
|
|
40
|
+
`uuid: ${result.uuid}`,
|
|
41
|
+
`input: ${inputLine}`,
|
|
42
|
+
""
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
function describeInputForm(input, canonical) {
|
|
46
|
+
if (input === canonical) return "canonical";
|
|
47
|
+
const notes = [];
|
|
48
|
+
if (input !== input.toLowerCase()) notes.push("was uppercase");
|
|
49
|
+
if (/[ilo]/i.test(input.slice(4))) notes.push("used Crockford aliases");
|
|
50
|
+
return `not canonical (${notes.join(" + ")})`;
|
|
51
|
+
}
|
|
52
|
+
const msPerMinute = 60 * 1e3;
|
|
53
|
+
const msPerHour = 60 * msPerMinute;
|
|
54
|
+
const msPerDay = 24 * msPerHour;
|
|
55
|
+
const daysPerMonth = 30.44;
|
|
56
|
+
const monthsPerYear = 12;
|
|
57
|
+
function formatRelative(thenMs, nowMs) {
|
|
58
|
+
const diff = nowMs - thenMs;
|
|
59
|
+
const abs = Math.abs(diff);
|
|
60
|
+
const suffix = diff < 0 ? "from now" : "ago";
|
|
61
|
+
const head = headUnits(abs);
|
|
62
|
+
return head === "" ? "just now" : `${head} ${suffix}`;
|
|
63
|
+
}
|
|
64
|
+
function headUnits(abs) {
|
|
65
|
+
if (abs < 6e4) return "";
|
|
66
|
+
if (abs < 36e5) return unit(Math.round(abs / msPerMinute), "minute");
|
|
67
|
+
if (abs < 864e5) return unit(Math.round(abs / msPerHour), "hour");
|
|
68
|
+
if (abs < 864e5 * daysPerMonth) return unit(Math.round(abs / msPerDay), "day");
|
|
69
|
+
const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));
|
|
70
|
+
if (totalMonths < monthsPerYear) return unit(totalMonths, "month");
|
|
71
|
+
const years = Math.floor(totalMonths / monthsPerYear);
|
|
72
|
+
const months = totalMonths % monthsPerYear;
|
|
73
|
+
return months === 0 ? unit(years, "year") : `${unit(years, "year")} ${unit(months, "month")}`;
|
|
74
|
+
}
|
|
75
|
+
function unit(n, name) {
|
|
76
|
+
return `${n} ${n === 1 ? name : `${name}s`}`;
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
9
79
|
//#region src/cli/key-io.ts
|
|
10
80
|
function isLoadKeyError(value) {
|
|
11
81
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -46,7 +116,7 @@ async function loadKey(opts, format, facet) {
|
|
|
46
116
|
} catch (err) {
|
|
47
117
|
return {
|
|
48
118
|
kind: "import-failure",
|
|
49
|
-
message: err
|
|
119
|
+
message: formatCliError(err)
|
|
50
120
|
};
|
|
51
121
|
}
|
|
52
122
|
}
|
|
@@ -135,7 +205,10 @@ const knownFlags = /* @__PURE__ */ new Set([
|
|
|
135
205
|
"--key-format",
|
|
136
206
|
"--count",
|
|
137
207
|
"-c",
|
|
138
|
-
"--bits"
|
|
208
|
+
"--bits",
|
|
209
|
+
"--uuid",
|
|
210
|
+
"--from-uuid",
|
|
211
|
+
"--brand"
|
|
139
212
|
]);
|
|
140
213
|
function unsupportedFlagForCommand(command, flags, allowed) {
|
|
141
214
|
for (const flag of flags) if (!allowed.has(flag)) return knownFlags.has(flag) ? `unsupported flag for ${command}: ${flag}` : `unsupported flag: ${flag}`;
|
|
@@ -178,77 +251,10 @@ function isNsError(result) {
|
|
|
178
251
|
return result === "--ns requires a value";
|
|
179
252
|
}
|
|
180
253
|
//#endregion
|
|
181
|
-
//#region src/cli/format.ts
|
|
182
|
-
function formatCliError(err) {
|
|
183
|
-
return isIdsError(err) ? `${err.code}: ${err.message}` : err instanceof Error ? err.message : String(err);
|
|
184
|
-
}
|
|
185
|
-
function formatWrappedInspectOutput(result) {
|
|
186
|
-
const inputLine = describeInputForm(result.input, result.canonical);
|
|
187
|
-
return [
|
|
188
|
-
`brand: ${result.brand}`,
|
|
189
|
-
`lookup-key: ${result.lookupKey.toString()}`,
|
|
190
|
-
`canonical: ${result.canonical}`,
|
|
191
|
-
`input: ${inputLine}`,
|
|
192
|
-
""
|
|
193
|
-
].join("\n");
|
|
194
|
-
}
|
|
195
|
-
function formatSignedInspectOutput(result) {
|
|
196
|
-
const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
|
|
197
|
-
const inputLine = describeInputForm(result.input, result.canonical);
|
|
198
|
-
const lines = [`brand: ${result.brand}`, `timestamp: ${result.timestamp.toISOString()} (${relative})`];
|
|
199
|
-
lines.push(`verification: ${result.verification}`);
|
|
200
|
-
lines.push(`canonical: ${result.canonical}`, `input: ${inputLine}`, "");
|
|
201
|
-
return lines.join("\n");
|
|
202
|
-
}
|
|
203
|
-
function formatInspectOutput(result) {
|
|
204
|
-
const relative = formatRelative(result.timestamp.getTime(), result.nowMs);
|
|
205
|
-
const inputLine = describeInputForm(result.input, result.canonical);
|
|
206
|
-
return [
|
|
207
|
-
`brand: ${result.brand}`,
|
|
208
|
-
`timestamp: ${result.timestamp.toISOString()} (${relative})`,
|
|
209
|
-
`canonical: ${result.canonical}`,
|
|
210
|
-
`input: ${inputLine}`,
|
|
211
|
-
""
|
|
212
|
-
].join("\n");
|
|
213
|
-
}
|
|
214
|
-
function describeInputForm(input, canonical) {
|
|
215
|
-
if (input === canonical) return "canonical";
|
|
216
|
-
const notes = [];
|
|
217
|
-
if (input !== input.toLowerCase()) notes.push("was uppercase");
|
|
218
|
-
if (/[ilo]/i.test(input.slice(4))) notes.push("used Crockford aliases");
|
|
219
|
-
return `not canonical (${notes.join(" + ")})`;
|
|
220
|
-
}
|
|
221
|
-
const msPerMinute = 60 * 1e3;
|
|
222
|
-
const msPerHour = 60 * msPerMinute;
|
|
223
|
-
const msPerDay = 24 * msPerHour;
|
|
224
|
-
const daysPerMonth = 30.44;
|
|
225
|
-
const monthsPerYear = 12;
|
|
226
|
-
function formatRelative(thenMs, nowMs) {
|
|
227
|
-
const diff = nowMs - thenMs;
|
|
228
|
-
const abs = Math.abs(diff);
|
|
229
|
-
const suffix = diff < 0 ? "from now" : "ago";
|
|
230
|
-
const head = headUnits(abs);
|
|
231
|
-
return head === "" ? "just now" : `${head} ${suffix}`;
|
|
232
|
-
}
|
|
233
|
-
function headUnits(abs) {
|
|
234
|
-
if (abs < 6e4) return "";
|
|
235
|
-
if (abs < 36e5) return unit(Math.round(abs / msPerMinute), "minute");
|
|
236
|
-
if (abs < 864e5) return unit(Math.round(abs / msPerHour), "hour");
|
|
237
|
-
if (abs < 864e5 * daysPerMonth) return unit(Math.round(abs / msPerDay), "day");
|
|
238
|
-
const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));
|
|
239
|
-
if (totalMonths < monthsPerYear) return unit(totalMonths, "month");
|
|
240
|
-
const years = Math.floor(totalMonths / monthsPerYear);
|
|
241
|
-
const months = totalMonths % monthsPerYear;
|
|
242
|
-
return months === 0 ? unit(years, "year") : `${unit(years, "year")} ${unit(months, "month")}`;
|
|
243
|
-
}
|
|
244
|
-
function unit(n, name) {
|
|
245
|
-
return `${n} ${n === 1 ? name : `${name}s`}`;
|
|
246
|
-
}
|
|
247
|
-
//#endregion
|
|
248
254
|
//#region src/cli/variants.ts
|
|
249
255
|
function standardValidate(codec, input) {
|
|
250
256
|
const result = codec["~standard"].validate(input);
|
|
251
|
-
if (result.issues) return { issue: result.issues[0].message };
|
|
257
|
+
if (result.issues) return { issue: invalidIdPrefix + result.issues[0].message };
|
|
252
258
|
return { value: result.value };
|
|
253
259
|
}
|
|
254
260
|
const timestampVariant = {
|
|
@@ -395,6 +401,7 @@ const digestVariant = {
|
|
|
395
401
|
});
|
|
396
402
|
return {
|
|
397
403
|
safeParse: (v) => codec.safeParse(v),
|
|
404
|
+
toUUID: (id) => codec.toUUID(id),
|
|
398
405
|
async generate() {
|
|
399
406
|
const material = await (opts.readStdin ?? (() => Promise.resolve("")))();
|
|
400
407
|
return codec.digest(material);
|
|
@@ -420,7 +427,11 @@ const generatePolicy = {
|
|
|
420
427
|
signedVariant,
|
|
421
428
|
digestVariant
|
|
422
429
|
],
|
|
423
|
-
intrinsicFlags: [
|
|
430
|
+
intrinsicFlags: [
|
|
431
|
+
"--count",
|
|
432
|
+
"-c",
|
|
433
|
+
"--uuid"
|
|
434
|
+
]
|
|
424
435
|
};
|
|
425
436
|
const inspectPolicy = {
|
|
426
437
|
default: timestampVariant,
|
|
@@ -430,7 +441,7 @@ const inspectPolicy = {
|
|
|
430
441
|
opaqueVariant,
|
|
431
442
|
signedVariant
|
|
432
443
|
],
|
|
433
|
-
intrinsicFlags: []
|
|
444
|
+
intrinsicFlags: ["--from-uuid", "--brand"]
|
|
434
445
|
};
|
|
435
446
|
const keygenPolicy = {
|
|
436
447
|
default: opaqueVariant,
|
|
@@ -492,8 +503,9 @@ async function buildCodec(variant, brand, values, opts) {
|
|
|
492
503
|
function usageInspect() {
|
|
493
504
|
return [
|
|
494
505
|
"Usage: ids inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
|
|
506
|
+
" ids inspect --from-uuid <uuid> --brand <brand>",
|
|
495
507
|
"",
|
|
496
|
-
" Decode an ID and print brand, timestamp (or lookup key),
|
|
508
|
+
" Decode an ID and print brand, timestamp (or lookup key), canonical form, and UUID.",
|
|
497
509
|
" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
|
|
498
510
|
" --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
|
|
499
511
|
" --kind is required with --wrapped: u32, i32, u64, or i64.",
|
|
@@ -501,12 +513,14 @@ function usageInspect() {
|
|
|
501
513
|
" --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
|
|
502
514
|
" Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
|
|
503
515
|
" Note: --digest is not supported for inspect (Digest IDs are one-way; there is no reverse path).",
|
|
516
|
+
" --from-uuid <uuid> converts a UUID back to a canonical Id<Brand>. Requires --brand <brand>.",
|
|
517
|
+
" --brand <brand> specifies the entity type brand for --from-uuid (e.g. usr).",
|
|
504
518
|
""
|
|
505
519
|
].join("\n");
|
|
506
520
|
}
|
|
507
521
|
function usageGenerate() {
|
|
508
522
|
return [
|
|
509
|
-
`Usage: ids generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--key-format hex|base64url]`,
|
|
523
|
+
`Usage: ids generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--uuid] [--key-format hex|base64url]`,
|
|
510
524
|
"",
|
|
511
525
|
` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
|
|
512
526
|
" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
|
|
@@ -517,6 +531,7 @@ function usageGenerate() {
|
|
|
517
531
|
" Reads the digest key from IDS_DIGEST_KEY (hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
|
|
518
532
|
" Same material + ns + key always produces the same ID. Digest IDs are one-way.",
|
|
519
533
|
" --count N > 1 is rejected: same material always produces the same ID.",
|
|
534
|
+
" --uuid emits the raw UUID form of each generated ID instead of the canonical ID.",
|
|
520
535
|
""
|
|
521
536
|
].join("\n");
|
|
522
537
|
}
|
|
@@ -537,7 +552,8 @@ function usage() {
|
|
|
537
552
|
"",
|
|
538
553
|
"Subcommands:",
|
|
539
554
|
" inspect, i <id> [--opaque] [--wrapped --kind u32|i32|u64|i64] [--reverse] [--signed] [--key-format hex|base64url]",
|
|
540
|
-
"
|
|
555
|
+
" inspect, i --from-uuid <uuid> --brand <brand>",
|
|
556
|
+
" Decode an ID and print brand, timestamp (or lookup key), canonical form, and UUID.",
|
|
541
557
|
" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
|
|
542
558
|
" --wrapped reads the wrapping key from IDS_WRAPPING_KEY (hex by default; IDS_WRAPPING_KEY_FORMAT or --key-format).",
|
|
543
559
|
" --kind is required with --wrapped: u32, i32, u64, or i64.",
|
|
@@ -545,7 +561,9 @@ function usage() {
|
|
|
545
561
|
" --signed decodes a Signed Timestamp ID; reads signing key from IDS_SIGNING_KEY (hex by default; IDS_SIGNING_KEY_FORMAT or --key-format).",
|
|
546
562
|
" Without IDS_SIGNING_KEY, --signed prints the timestamp only (no verification). With IDS_SIGNING_KEY, prints verification: ok or failed.",
|
|
547
563
|
" Note: --digest is not supported for inspect (Digest IDs are one-way; there is no reverse path).",
|
|
548
|
-
"
|
|
564
|
+
" --from-uuid <uuid> converts a UUID back to a canonical Id<Brand>. Requires --brand <brand>.",
|
|
565
|
+
" --brand <brand> specifies the entity type brand for --from-uuid (e.g. usr).",
|
|
566
|
+
" generate, g <brand> [--count, -c N] [--opaque] [--reverse] [--signed] [--digest --ns <ns>] [--uuid] [--key-format hex|base64url]",
|
|
549
567
|
` Mint 1..${maxGenerateCount} canonical IDs for the given brand.`,
|
|
550
568
|
" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
|
|
551
569
|
" --reverse mints Reverse Timestamp IDs (newest-first sort order).",
|
|
@@ -555,6 +573,7 @@ function usage() {
|
|
|
555
573
|
" Reads the digest key from IDS_DIGEST_KEY (hex by default; IDS_DIGEST_KEY_FORMAT or --key-format).",
|
|
556
574
|
" Same material + ns + key always produces the same ID. Digest IDs are one-way.",
|
|
557
575
|
" --count N > 1 is rejected: same material always produces the same ID.",
|
|
576
|
+
" --uuid emits the raw UUID form of each generated ID instead of the canonical ID.",
|
|
558
577
|
" keygen, k [--wrapped] [--signed] [--digest] [--bits 128|192|256] [--key-format hex|base64url]",
|
|
559
578
|
" Emit a random key for importOpaqueKey, importWrappingKey, importSigningKey, or importDigestKey (key on stdout; warning on stderr).",
|
|
560
579
|
" Safe handling: redirect stdout to a 0600 file (e.g. ids keygen > key.hex && chmod 0600 key.hex);",
|
|
@@ -634,7 +653,14 @@ async function runGenerate(args, opts) {
|
|
|
634
653
|
opts.stderr(codec.message + "\n");
|
|
635
654
|
return codec.kind === "usage" ? 2 : 1;
|
|
636
655
|
}
|
|
637
|
-
|
|
656
|
+
const emitUuid = flags.has("--uuid");
|
|
657
|
+
for (let i = 0; i < count; i++) {
|
|
658
|
+
const id = await codec.generate();
|
|
659
|
+
if (emitUuid) {
|
|
660
|
+
const uuid = codec.toUUID(id);
|
|
661
|
+
opts.stdout(uuid + "\n");
|
|
662
|
+
} else opts.stdout(id + "\n");
|
|
663
|
+
}
|
|
638
664
|
return 0;
|
|
639
665
|
}
|
|
640
666
|
//#endregion
|
|
@@ -656,6 +682,32 @@ async function runInspect(args, opts) {
|
|
|
656
682
|
opts.stderr(errors[0] + "\n");
|
|
657
683
|
return 2;
|
|
658
684
|
}
|
|
685
|
+
const fromUuidValue = values.get("--from-uuid");
|
|
686
|
+
if (fromUuidValue !== void 0) {
|
|
687
|
+
if (fromUuidValue === "") {
|
|
688
|
+
opts.stderr("--from-uuid requires a value\n");
|
|
689
|
+
return 2;
|
|
690
|
+
}
|
|
691
|
+
const brandValue = values.get("--brand");
|
|
692
|
+
if (brandValue === void 0 || brandValue === "") {
|
|
693
|
+
opts.stderr("--from-uuid requires --brand\n");
|
|
694
|
+
return 2;
|
|
695
|
+
}
|
|
696
|
+
let tsCodec;
|
|
697
|
+
try {
|
|
698
|
+
tsCodec = createTimestampId(brandValue, codecOpts(opts));
|
|
699
|
+
} catch (err) {
|
|
700
|
+
opts.stderr(formatCliError(err) + "\n");
|
|
701
|
+
return 1;
|
|
702
|
+
}
|
|
703
|
+
const result = tsCodec.safeFromUUID(fromUuidValue);
|
|
704
|
+
if (!result.ok) {
|
|
705
|
+
opts.stderr("invalid_uuid: not a valid RFC 9562 UUID\n");
|
|
706
|
+
return 1;
|
|
707
|
+
}
|
|
708
|
+
opts.stdout(result.id + "\n");
|
|
709
|
+
return 0;
|
|
710
|
+
}
|
|
659
711
|
const [input] = positionals;
|
|
660
712
|
if (input === void 0) {
|
|
661
713
|
opts.stderr(usageInspect());
|
|
@@ -680,6 +732,7 @@ async function runInspect(args, opts) {
|
|
|
680
732
|
let verifyTimestamp;
|
|
681
733
|
let verifyCanonical;
|
|
682
734
|
let verifyNowMs;
|
|
735
|
+
let verifyTsCodec;
|
|
683
736
|
if (cap.mode === "verify") {
|
|
684
737
|
const fmtCheck = parseKeyFormat(values, opts, variant.key);
|
|
685
738
|
if (isKeyFormatError(fmtCheck)) {
|
|
@@ -695,9 +748,10 @@ async function runInspect(args, opts) {
|
|
|
695
748
|
}
|
|
696
749
|
const structValidation = tsCodec["~standard"].validate(input);
|
|
697
750
|
if (structValidation.issues) {
|
|
698
|
-
opts.stderr(structValidation.issues[0].message + "\n");
|
|
751
|
+
opts.stderr(invalidIdPrefix + structValidation.issues[0].message + "\n");
|
|
699
752
|
return 1;
|
|
700
753
|
}
|
|
754
|
+
verifyTsCodec = tsCodec;
|
|
701
755
|
verifyCanonical = structValidation.value;
|
|
702
756
|
verifyTimestamp = tsCodec.extractTimestamp(verifyCanonical);
|
|
703
757
|
verifyNowMs = (opts.now ?? Date.now)();
|
|
@@ -705,10 +759,12 @@ async function runInspect(args, opts) {
|
|
|
705
759
|
const codecOrError = await buildCodec(variant, brand, values, opts);
|
|
706
760
|
if (isCodecError(codecOrError)) {
|
|
707
761
|
if (cap.mode === "verify") {
|
|
762
|
+
const uuid = verifyTsCodec.toUUID(verifyCanonical);
|
|
708
763
|
opts.stdout(formatSignedInspectOutput({
|
|
709
764
|
brand,
|
|
710
765
|
timestamp: verifyTimestamp,
|
|
711
766
|
canonical: verifyCanonical,
|
|
767
|
+
uuid,
|
|
712
768
|
input,
|
|
713
769
|
nowMs: verifyNowMs,
|
|
714
770
|
verification: "unavailable"
|
|
@@ -728,15 +784,20 @@ async function runInspect(args, opts) {
|
|
|
728
784
|
}
|
|
729
785
|
canonical = parsed.value;
|
|
730
786
|
}
|
|
787
|
+
function codecToUUID(id) {
|
|
788
|
+
return codecOrError.toUUID(id);
|
|
789
|
+
}
|
|
731
790
|
switch (cap.mode) {
|
|
732
791
|
case "readable": {
|
|
733
792
|
const timestamp = cap.extractTimestamp(codecOrError, canonical);
|
|
734
793
|
const nowMs = (opts.now ?? Date.now)();
|
|
794
|
+
const uuid = codecToUUID(canonical);
|
|
735
795
|
opts.stderr(cap.note + "\n");
|
|
736
796
|
opts.stdout(formatInspectOutput({
|
|
737
797
|
brand,
|
|
738
798
|
timestamp,
|
|
739
799
|
canonical,
|
|
800
|
+
uuid,
|
|
740
801
|
input,
|
|
741
802
|
nowMs
|
|
742
803
|
}));
|
|
@@ -745,11 +806,13 @@ async function runInspect(args, opts) {
|
|
|
745
806
|
case "keyed-readable": {
|
|
746
807
|
const timestamp = await cap.extractTimestamp(codecOrError, canonical);
|
|
747
808
|
const nowMs = (opts.now ?? Date.now)();
|
|
809
|
+
const uuid = codecToUUID(canonical);
|
|
748
810
|
opts.stderr(cap.note + "\n");
|
|
749
811
|
opts.stdout(formatInspectOutput({
|
|
750
812
|
brand,
|
|
751
813
|
timestamp,
|
|
752
814
|
canonical,
|
|
815
|
+
uuid,
|
|
753
816
|
input,
|
|
754
817
|
nowMs
|
|
755
818
|
}));
|
|
@@ -763,15 +826,18 @@ async function runInspect(args, opts) {
|
|
|
763
826
|
opts.stderr(formatCliError(err) + "\n");
|
|
764
827
|
return 1;
|
|
765
828
|
}
|
|
829
|
+
const uuid = codecToUUID(canonical);
|
|
766
830
|
opts.stdout(formatWrappedInspectOutput({
|
|
767
831
|
brand,
|
|
768
832
|
lookupKey,
|
|
769
833
|
canonical,
|
|
834
|
+
uuid,
|
|
770
835
|
input
|
|
771
836
|
}));
|
|
772
837
|
return 0;
|
|
773
838
|
}
|
|
774
839
|
case "verify": {
|
|
840
|
+
const uuid = verifyTsCodec.toUUID(verifyCanonical);
|
|
775
841
|
const verifyResult = await cap.safeVerify(codecOrError, input);
|
|
776
842
|
if (!verifyResult.ok) {
|
|
777
843
|
/* v8 ignore next 4 -- defensive: both codecs share the same wire parse so ParseError
|
|
@@ -784,6 +850,7 @@ async function runInspect(args, opts) {
|
|
|
784
850
|
brand,
|
|
785
851
|
timestamp: verifyTimestamp,
|
|
786
852
|
canonical: verifyCanonical,
|
|
853
|
+
uuid,
|
|
787
854
|
input,
|
|
788
855
|
nowMs: verifyNowMs,
|
|
789
856
|
verification: "failed"
|
|
@@ -795,6 +862,7 @@ async function runInspect(args, opts) {
|
|
|
795
862
|
brand,
|
|
796
863
|
timestamp: verifyTimestamp,
|
|
797
864
|
canonical: verifyResult.id,
|
|
865
|
+
uuid,
|
|
798
866
|
input,
|
|
799
867
|
nowMs: verifyNowMs,
|
|
800
868
|
verification: "ok"
|
|
@@ -876,15 +944,20 @@ const commands = [
|
|
|
876
944
|
}
|
|
877
945
|
];
|
|
878
946
|
async function run(opts) {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
947
|
+
try {
|
|
948
|
+
const [subcommand, ...rest] = opts.argv;
|
|
949
|
+
const command = commands.find((candidate) => candidate.names.includes(subcommand ?? ""));
|
|
950
|
+
if (command !== void 0) return await command.run(rest, opts);
|
|
951
|
+
if (subcommand === void 0 || subcommand === "--help" || subcommand === "-h") {
|
|
952
|
+
opts.stdout(usage());
|
|
953
|
+
return 0;
|
|
954
|
+
}
|
|
955
|
+
opts.stderr(usage());
|
|
956
|
+
return 2;
|
|
957
|
+
} catch (err) {
|
|
958
|
+
opts.stderr(formatCliError(err) + "\n");
|
|
959
|
+
return 1;
|
|
885
960
|
}
|
|
886
|
-
opts.stderr(usage());
|
|
887
|
-
return 2;
|
|
888
961
|
}
|
|
889
962
|
//#endregion
|
|
890
963
|
//#region bin/cli.ts
|