@smonn/ids 0.3.1 → 0.3.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 +57 -8
- package/dist/cli.mjs +212 -17
- package/dist/cli.mjs.map +1 -1
- package/dist/{shared-CmbAeUdM.mjs → codec-shell-C0arqqX3.mjs} +73 -22
- package/dist/codec-shell-C0arqqX3.mjs.map +1 -0
- package/dist/id-CcoPE2EX.mjs +95 -0
- package/dist/id-CcoPE2EX.mjs.map +1 -0
- package/dist/index.d.mts +39 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/opaque-B-ttBfHO.mjs +186 -0
- package/dist/opaque-B-ttBfHO.mjs.map +1 -0
- package/dist/opaque.d.mts +61 -8
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +2 -70
- package/dist/{types-Dg2j_zlO.d.mts → types-Bv-63YK4.d.mts} +7 -1
- package/dist/types-Bv-63YK4.d.mts.map +1 -0
- package/package.json +3 -1
- package/dist/id-B4GiFKYV.mjs +0 -76
- package/dist/id-B4GiFKYV.mjs.map +0 -1
- package/dist/opaque.mjs.map +0 -1
- package/dist/shared-CmbAeUdM.mjs.map +0 -1
- package/dist/types-Dg2j_zlO.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Public-facing branded IDs for TypeScript apps.
|
|
|
6
6
|
pnpm add @smonn/ids
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Each ID looks like `usr_01h7b3k9rqxn4cw3p9r8t2sgkz`: a three-letter brand, an underscore, then 26 Crockford base32 characters of payload. The
|
|
9
|
+
Each ID looks like `usr_01h7b3k9rqxn4cw3p9r8t2sgkz`: a three-letter brand, an underscore, then 26 Crockford base32 characters of payload. The Timestamp codec encodes a 48-bit millisecond Unix timestamp followed by 80 random bits — same byte layout as a [ULID](https://github.com/ulid/spec); see [ADR-0002](./docs/adr/0002-payload-layout.md) for the deliberate divergences. The Opaque codec (`@smonn/ids/opaque`) keeps the same wire shape but encrypts the payload under a key, so the timestamp isn't readable from the ID.
|
|
10
10
|
|
|
11
11
|
## What this is for
|
|
12
12
|
|
|
@@ -166,7 +166,7 @@ The `pattern` describes the **canonical form only** — it matches `generate()`
|
|
|
166
166
|
|
|
167
167
|
### "Don't leak creation time in IDs that customers can see"
|
|
168
168
|
|
|
169
|
-
The
|
|
169
|
+
The Timestamp codec exposes the creation timestamp by design — that's what makes `ORDER BY id` work. If that's a leak you can't accept (invoice IDs revealing billing cadence, signup IDs revealing acquisition velocity), use the Opaque codec at `@smonn/ids/opaque`. Same `<brand>_<26 chars>` wire shape, but the payload is AES-encrypted under a key you supply.
|
|
170
170
|
|
|
171
171
|
```ts
|
|
172
172
|
import { createOpaqueId, importOpaqueKey } from "@smonn/ids/opaque";
|
|
@@ -178,20 +178,22 @@ const id = await invoices.generate(); // "inv_…", timestamp not extractable wi
|
|
|
178
178
|
await invoices.extractTimestamp(id); // Date — same codec, same key required
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
Three differences from the
|
|
181
|
+
Three differences from the Timestamp codec:
|
|
182
182
|
|
|
183
183
|
- **Async key-dependent methods.** WebCrypto is async-only, so `generate`, `generateAt`, and `extractTimestamp` return `Promise`s. `is`, `parse`, `safeParse`, `toJsonSchema`, and the Standard Schema adapter stay sync — they work on the wire form only ([ADR-0006](./docs/adr/0006-async-keyed-codec-contract.md)).
|
|
184
|
-
- **No `minIdForTime` / `maxIdForTime`.** Encrypted payloads don't sort by time. If you need time-range scans on
|
|
185
|
-
- **Wire-indistinguishable from the
|
|
184
|
+
- **No `minIdForTime` / `maxIdForTime`.** Encrypted payloads don't sort by time. If you need time-range scans on Opaque-coded entities, store the timestamp in a separate column.
|
|
185
|
+
- **Wire-indistinguishable from the Timestamp codec.** Codec choice is a per-brand commitment; the brand registry warns if you register the same brand against both in dev ([ADR-0007](./docs/adr/0007-wire-indistinguishable-codec-variants.md)).
|
|
186
186
|
|
|
187
187
|
Encryption is AES-CBC with a zero IV. That's deliberately safe here because the plaintext already carries 80 bits of entropy per ID; see [ADR-0004](./docs/adr/0004-aes-cbc-strip-trick.md) for the full rationale.
|
|
188
188
|
|
|
189
|
+
To store or transport key material outside the library, `encodeOpaqueKey` / `decodeOpaqueKey` round-trip raw bytes in `hex` or `base64url` — distinct from the Crockford base32 used in ID payloads. The CLI's `keygen` subcommand emits keys in this format (see [CLI](#cli)).
|
|
190
|
+
|
|
189
191
|
## What this is **not** for
|
|
190
192
|
|
|
191
193
|
- **Internal surrogate primary keys.** If nobody outside your service ever sees the ID, the brand prefix and lenient parsing are dead weight. Use a `bigint` sequence.
|
|
192
194
|
- **Wire-compatible ULIDs.** The byte layout is ULID-shaped but the encoding is lowercase and wrapped in a brand envelope. Stock ULID parsers will reject these.
|
|
193
195
|
- **Distributed-trace / request-correlation IDs.** Use OpenTelemetry-format IDs.
|
|
194
|
-
- **Hiding creation time with the
|
|
196
|
+
- **Hiding creation time with the Timestamp codec.** Anyone with one ID at a known creation time can compute the epoch offset. A custom epoch wouldn't help and isn't supported. To hide creation time per-ID, use the Opaque codec (above).
|
|
195
197
|
|
|
196
198
|
## API surface
|
|
197
199
|
|
|
@@ -209,8 +211,11 @@ import {
|
|
|
209
211
|
import {
|
|
210
212
|
createOpaqueId, // (brand: string, opts: { key, now?, rng?, allowDuplicateBrand? }) => OpaqueCodec<Brand>
|
|
211
213
|
importOpaqueKey, // (bytes: Uint8Array) => Promise<CryptoKey>
|
|
214
|
+
encodeOpaqueKey, // (bytes: Uint8Array, format: OpaqueKeyFormat) => string
|
|
215
|
+
decodeOpaqueKey, // (encoded: string, format: OpaqueKeyFormat) => Uint8Array
|
|
212
216
|
type OpaqueCodec, // returned by createOpaqueId
|
|
213
217
|
type OpaqueOptions, // { key, now, rng, allowDuplicateBrand } injection points
|
|
218
|
+
type OpaqueKeyFormat, // "hex" | "base64url"
|
|
214
219
|
} from "@smonn/ids/opaque";
|
|
215
220
|
```
|
|
216
221
|
|
|
@@ -230,7 +235,11 @@ import {
|
|
|
230
235
|
|
|
231
236
|
## CLI
|
|
232
237
|
|
|
233
|
-
|
|
238
|
+
Brand-agnostic subcommands, no install required. Run `npx @smonn/ids --help` for the full flag list.
|
|
239
|
+
|
|
240
|
+
### `inspect` (`i`)
|
|
241
|
+
|
|
242
|
+
Decode an ID and print brand, timestamp, canonical form, and whether the input was already canonical.
|
|
234
243
|
|
|
235
244
|
```bash
|
|
236
245
|
$ npx @smonn/ids inspect usr_01h7b3k9rqxn1cw3p9r8t2sgkz
|
|
@@ -238,14 +247,54 @@ brand: usr
|
|
|
238
247
|
timestamp: 1983-05-27T10:24:22.469Z (43 years ago)
|
|
239
248
|
canonical: usr_01h7b3k9rqxn1cw3p9r8t2sgkz
|
|
240
249
|
input: canonical
|
|
250
|
+
```
|
|
241
251
|
|
|
252
|
+
Accepts non-canonical input (uppercase, Crockford aliases). Assumes the **Timestamp codec** — if the brand uses the **Opaque codec**, pass `--opaque` and set `IDS_KEY` (below); otherwise the timestamp line is meaningless garbage.
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
IDS_KEY=<hex-or-base64url-key> npx @smonn/ids inspect inv_… --opaque
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Prints the decrypted timestamp **assuming `IDS_KEY` matches the key used at generation** — a well-formed but wrong key yields a plausible but incorrect timestamp, not an error (see [CONTEXT.md](./CONTEXT.md)).
|
|
259
|
+
|
|
260
|
+
### `generate` (`g`)
|
|
261
|
+
|
|
262
|
+
Mint one or more canonical IDs for a brand. Output is one ID per line (pipeable).
|
|
263
|
+
|
|
264
|
+
```bash
|
|
242
265
|
$ npx @smonn/ids generate usr --count 3
|
|
243
266
|
usr_…
|
|
244
267
|
usr_…
|
|
245
268
|
usr_…
|
|
246
269
|
```
|
|
247
270
|
|
|
248
|
-
`
|
|
271
|
+
Flags: `--count` / `-c N` (default 1). Uses the Timestamp codec unless `--opaque` is set.
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
IDS_KEY=<hex-or-base64url-key> npx @smonn/ids generate inv --opaque --count 2
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### `keygen` (`k`)
|
|
278
|
+
|
|
279
|
+
Emit a random Opaque key to stdout (a secret — do not log or commit). Default: 256-bit hex.
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
$ npx @smonn/ids keygen
|
|
283
|
+
a1b2c3…
|
|
284
|
+
|
|
285
|
+
$ npx @smonn/ids keygen --bits 128 --key-format base64url
|
|
286
|
+
AbCdEf…
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Flags: `--bits 128|192|256` (default 256), `--key-format hex|base64url` (default `hex`). `IDS_KEY_FORMAT` does not affect `keygen` — only `--key-format` on the command line. Output round-trips through `decodeOpaqueKey` / `importOpaqueKey`.
|
|
290
|
+
|
|
291
|
+
### Opaque mode (`--opaque`)
|
|
292
|
+
|
|
293
|
+
`generate --opaque` and `inspect --opaque` read the AES key from the `IDS_KEY` environment variable — not from argv (argv leaks via `ps` and shell history). Missing or malformed `IDS_KEY` prints a clear stderr message and exits non-zero.
|
|
294
|
+
|
|
295
|
+
Key format defaults to `hex`; override per-invocation with `--key-format` or set `IDS_KEY_FORMAT=hex|base64url` for a session default. `--key-format` on the command line wins over `IDS_KEY_FORMAT`.
|
|
296
|
+
|
|
297
|
+
Invalid input prints the parse error to stderr and exits non-zero.
|
|
249
298
|
|
|
250
299
|
## Design
|
|
251
300
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as createId } from "./id-
|
|
2
|
+
import { t as createId } from "./id-CcoPE2EX.mjs";
|
|
3
|
+
import { i as encodeOpaqueKey, n as importOpaqueKey, r as decodeOpaqueKey, t as createOpaqueId } from "./opaque-B-ttBfHO.mjs";
|
|
3
4
|
//#region src/cli.ts
|
|
4
|
-
function run(opts) {
|
|
5
|
+
async function run(opts) {
|
|
5
6
|
const [subcommand, ...rest] = opts.argv;
|
|
6
7
|
if (subcommand === "generate" || subcommand === "g") return runGenerate(rest, opts);
|
|
7
8
|
if (subcommand === "inspect" || subcommand === "i") return runInspect(rest, opts);
|
|
9
|
+
if (subcommand === "keygen" || subcommand === "k") return runKeygen(rest, opts);
|
|
8
10
|
if (subcommand === void 0 || subcommand === "--help" || subcommand === "-h") {
|
|
9
11
|
opts.stdout(usage());
|
|
10
12
|
return 0;
|
|
@@ -17,21 +19,72 @@ function usage() {
|
|
|
17
19
|
"Usage: ids <subcommand> [args]",
|
|
18
20
|
"",
|
|
19
21
|
"Subcommands:",
|
|
20
|
-
" inspect, i <id>
|
|
21
|
-
"
|
|
22
|
+
" inspect, i <id> [--opaque] [--key-format hex|base64url]",
|
|
23
|
+
" Decode an ID and print brand, timestamp, and canonical form.",
|
|
24
|
+
" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
|
|
25
|
+
" generate, g <brand> [--count, -c N] [--opaque] [--key-format hex|base64url]",
|
|
26
|
+
" Mint one or more canonical IDs for the given brand.",
|
|
27
|
+
" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).",
|
|
28
|
+
" keygen, k [--bits 128|192|256] [--key-format hex|base64url]",
|
|
29
|
+
" Emit a random AES key for importOpaqueKey (stdout only).",
|
|
22
30
|
""
|
|
23
31
|
].join("\n");
|
|
24
32
|
}
|
|
25
33
|
function runInspect(args, opts) {
|
|
26
|
-
const
|
|
34
|
+
const { flags, values, positionals } = splitFlags(args);
|
|
35
|
+
const [input] = positionals;
|
|
27
36
|
if (input === void 0) {
|
|
28
37
|
opts.stderr(usage());
|
|
29
|
-
return 1;
|
|
38
|
+
return Promise.resolve(1);
|
|
30
39
|
}
|
|
40
|
+
const opaque = flags.has("--opaque");
|
|
31
41
|
const brand = input.slice(0, 3).toLowerCase();
|
|
42
|
+
if (opaque) {
|
|
43
|
+
const format = parseOpaqueKeyFormat(values, opts);
|
|
44
|
+
if (isKeyFormatError(format)) {
|
|
45
|
+
opts.stderr(format + "\n");
|
|
46
|
+
return Promise.resolve(1);
|
|
47
|
+
}
|
|
48
|
+
return runOpaqueInspect(brand, input, format, opts);
|
|
49
|
+
}
|
|
32
50
|
let codec;
|
|
33
51
|
try {
|
|
34
52
|
codec = createId(brand, codecOpts(opts));
|
|
53
|
+
} catch (err) {
|
|
54
|
+
opts.stderr(err.message + "\n");
|
|
55
|
+
return Promise.resolve(1);
|
|
56
|
+
}
|
|
57
|
+
const validation = codec["~standard"].validate(input);
|
|
58
|
+
if (validation.issues) {
|
|
59
|
+
opts.stderr(validation.issues[0].message + "\n");
|
|
60
|
+
return Promise.resolve(1);
|
|
61
|
+
}
|
|
62
|
+
const canonical = validation.value;
|
|
63
|
+
const timestamp = codec.extractTimestamp(canonical);
|
|
64
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
65
|
+
const relative = formatRelative(timestamp.getTime(), nowMs);
|
|
66
|
+
const inputLine = describeInputForm(input, canonical);
|
|
67
|
+
opts.stdout([
|
|
68
|
+
`brand: ${brand}`,
|
|
69
|
+
`timestamp: ${timestamp.toISOString()} (${relative})`,
|
|
70
|
+
`canonical: ${canonical}`,
|
|
71
|
+
`input: ${inputLine}`,
|
|
72
|
+
""
|
|
73
|
+
].join("\n"));
|
|
74
|
+
return Promise.resolve(0);
|
|
75
|
+
}
|
|
76
|
+
async function runOpaqueInspect(brand, input, format, opts) {
|
|
77
|
+
const keyResult = await loadOpaqueKey(opts, format);
|
|
78
|
+
if (typeof keyResult === "string") {
|
|
79
|
+
opts.stderr(keyResult + "\n");
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
let codec;
|
|
83
|
+
try {
|
|
84
|
+
codec = createOpaqueId(brand, {
|
|
85
|
+
key: keyResult,
|
|
86
|
+
...codecOpts(opts)
|
|
87
|
+
});
|
|
35
88
|
} catch (err) {
|
|
36
89
|
opts.stderr(err.message + "\n");
|
|
37
90
|
return 1;
|
|
@@ -42,10 +95,11 @@ function runInspect(args, opts) {
|
|
|
42
95
|
return 1;
|
|
43
96
|
}
|
|
44
97
|
const canonical = validation.value;
|
|
45
|
-
const timestamp = codec.extractTimestamp(canonical);
|
|
98
|
+
const timestamp = await codec.extractTimestamp(canonical);
|
|
46
99
|
const nowMs = (opts.now ?? Date.now)();
|
|
47
100
|
const relative = formatRelative(timestamp.getTime(), nowMs);
|
|
48
101
|
const inputLine = describeInputForm(input, canonical);
|
|
102
|
+
opts.stderr("note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp\n");
|
|
49
103
|
opts.stdout([
|
|
50
104
|
`brand: ${brand}`,
|
|
51
105
|
`timestamp: ${timestamp.toISOString()} (${relative})`,
|
|
@@ -89,30 +143,171 @@ function unit(n, name) {
|
|
|
89
143
|
return `${n} ${n === 1 ? name : `${name}s`}`;
|
|
90
144
|
}
|
|
91
145
|
function runGenerate(args, opts) {
|
|
92
|
-
const
|
|
93
|
-
const
|
|
146
|
+
const { flags, values, positionals } = splitFlags(args);
|
|
147
|
+
const [brand] = positionals;
|
|
148
|
+
const count = parseCount(values);
|
|
94
149
|
if (typeof count === "string") {
|
|
95
150
|
opts.stderr(count + "\n");
|
|
96
|
-
return 1;
|
|
151
|
+
return Promise.resolve(1);
|
|
152
|
+
}
|
|
153
|
+
if (flags.has("--opaque")) {
|
|
154
|
+
const format = parseOpaqueKeyFormat(values, opts);
|
|
155
|
+
if (isKeyFormatError(format)) {
|
|
156
|
+
opts.stderr(format + "\n");
|
|
157
|
+
return Promise.resolve(1);
|
|
158
|
+
}
|
|
159
|
+
return runOpaqueGenerate(brand ?? "", count, format, opts);
|
|
97
160
|
}
|
|
98
161
|
let codec;
|
|
99
162
|
try {
|
|
100
163
|
codec = createId(brand ?? "", codecOpts(opts));
|
|
101
164
|
} catch (err) {
|
|
102
165
|
opts.stderr(err.message + "\n");
|
|
103
|
-
return 1;
|
|
166
|
+
return Promise.resolve(1);
|
|
104
167
|
}
|
|
105
168
|
for (let i = 0; i < count; i++) opts.stdout(codec.generate() + "\n");
|
|
169
|
+
return Promise.resolve(0);
|
|
170
|
+
}
|
|
171
|
+
async function runOpaqueGenerate(brand, count, format, opts) {
|
|
172
|
+
const keyResult = await loadOpaqueKey(opts, format);
|
|
173
|
+
if (typeof keyResult === "string") {
|
|
174
|
+
opts.stderr(keyResult + "\n");
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
let codec;
|
|
178
|
+
try {
|
|
179
|
+
codec = createOpaqueId(brand, {
|
|
180
|
+
key: keyResult,
|
|
181
|
+
...codecOpts(opts)
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
opts.stderr(err.message + "\n");
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
for (let i = 0; i < count; i++) opts.stdout(await codec.generate() + "\n");
|
|
106
188
|
return 0;
|
|
107
189
|
}
|
|
108
|
-
function
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
190
|
+
function runKeygen(args, opts) {
|
|
191
|
+
const { values } = splitFlags(args);
|
|
192
|
+
const bits = parseBits(values);
|
|
193
|
+
if (typeof bits === "string") {
|
|
194
|
+
opts.stderr(bits + "\n");
|
|
195
|
+
return Promise.resolve(1);
|
|
196
|
+
}
|
|
197
|
+
const format = parseKeygenFormat(values);
|
|
198
|
+
if (isKeyFormatError(format)) {
|
|
199
|
+
opts.stderr(format + "\n");
|
|
200
|
+
return Promise.resolve(1);
|
|
201
|
+
}
|
|
202
|
+
const bytes = new Uint8Array(bits / 8);
|
|
203
|
+
crypto.getRandomValues(bytes);
|
|
204
|
+
opts.stdout(encodeOpaqueKey(bytes, format) + "\n");
|
|
205
|
+
return Promise.resolve(0);
|
|
206
|
+
}
|
|
207
|
+
async function loadOpaqueKey(opts, format) {
|
|
208
|
+
const raw = (opts.env ?? process.env).IDS_KEY;
|
|
209
|
+
if (raw === void 0 || raw === "") return "missing IDS_KEY environment variable";
|
|
210
|
+
try {
|
|
211
|
+
return importOpaqueKey(decodeOpaqueKey(raw, format));
|
|
212
|
+
} catch (err) {
|
|
213
|
+
return err.message;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function parseCount(values) {
|
|
217
|
+
const raw = values.get("--count") ?? values.get("-c");
|
|
218
|
+
if (raw === void 0) return 1;
|
|
219
|
+
if (raw === "") return "--count requires a value";
|
|
113
220
|
if (!/^[1-9][0-9]*$/.test(raw)) return `--count must be a positive integer, got '${raw}'`;
|
|
114
221
|
return Number(raw);
|
|
115
222
|
}
|
|
223
|
+
function parseBits(values) {
|
|
224
|
+
const raw = values.get("--bits");
|
|
225
|
+
if (raw === void 0) return 256;
|
|
226
|
+
if (raw === "") return "--bits requires a value";
|
|
227
|
+
if (raw === "128") return 128;
|
|
228
|
+
if (raw === "192") return 192;
|
|
229
|
+
if (raw === "256") return 256;
|
|
230
|
+
return `--bits must be 128, 192, or 256, got '${raw}'`;
|
|
231
|
+
}
|
|
232
|
+
function isKeyFormatError(result) {
|
|
233
|
+
return result !== "hex" && result !== "base64url";
|
|
234
|
+
}
|
|
235
|
+
function parseKeyFormatFlag(values) {
|
|
236
|
+
const fromFlag = values.get("--key-format");
|
|
237
|
+
if (fromFlag === void 0) return void 0;
|
|
238
|
+
if (fromFlag === "") return "--key-format requires a value";
|
|
239
|
+
if (fromFlag === "hex" || fromFlag === "base64url") return fromFlag;
|
|
240
|
+
return `--key-format must be hex or base64url, got '${fromFlag}'`;
|
|
241
|
+
}
|
|
242
|
+
function parseKeygenFormat(values) {
|
|
243
|
+
const fromFlag = parseKeyFormatFlag(values);
|
|
244
|
+
if (fromFlag === void 0) return "hex";
|
|
245
|
+
return fromFlag;
|
|
246
|
+
}
|
|
247
|
+
function parseOpaqueKeyFormat(values, opts) {
|
|
248
|
+
const fromFlag = parseKeyFormatFlag(values);
|
|
249
|
+
if (fromFlag !== void 0) return fromFlag;
|
|
250
|
+
const fromEnv = (opts.env ?? process.env).IDS_KEY_FORMAT;
|
|
251
|
+
if (fromEnv === void 0 || fromEnv === "") return "hex";
|
|
252
|
+
if (fromEnv === "hex" || fromEnv === "base64url") return fromEnv;
|
|
253
|
+
return `IDS_KEY_FORMAT must be hex or base64url, got '${fromEnv}'`;
|
|
254
|
+
}
|
|
255
|
+
function splitFlagToken(arg) {
|
|
256
|
+
const eq = arg.indexOf("=");
|
|
257
|
+
if (eq <= 0) return {
|
|
258
|
+
flag: arg,
|
|
259
|
+
inlineValue: void 0
|
|
260
|
+
};
|
|
261
|
+
return {
|
|
262
|
+
flag: arg.slice(0, eq),
|
|
263
|
+
inlineValue: arg.slice(eq + 1)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function splitFlags(args) {
|
|
267
|
+
const flags = /* @__PURE__ */ new Set();
|
|
268
|
+
const values = /* @__PURE__ */ new Map();
|
|
269
|
+
const positionals = [];
|
|
270
|
+
const valueFlags = new Set([
|
|
271
|
+
"--count",
|
|
272
|
+
"-c",
|
|
273
|
+
"--bits",
|
|
274
|
+
"--key-format"
|
|
275
|
+
]);
|
|
276
|
+
for (let i = 0; i < args.length; i++) {
|
|
277
|
+
const raw = args[i];
|
|
278
|
+
const { flag, inlineValue } = splitFlagToken(raw);
|
|
279
|
+
if (flag === "--opaque") {
|
|
280
|
+
flags.add(flag);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (valueFlags.has(flag)) {
|
|
284
|
+
if (inlineValue !== void 0) {
|
|
285
|
+
flags.add(flag);
|
|
286
|
+
values.set(flag, inlineValue);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const value = args[i + 1];
|
|
290
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
291
|
+
values.set(flag, "");
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
flags.add(flag);
|
|
295
|
+
values.set(flag, value);
|
|
296
|
+
i++;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (flag.startsWith("-")) {
|
|
300
|
+
flags.add(flag);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
positionals.push(raw);
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
flags,
|
|
307
|
+
values,
|
|
308
|
+
positionals
|
|
309
|
+
};
|
|
310
|
+
}
|
|
116
311
|
function codecOpts(opts) {
|
|
117
312
|
const o = { allowDuplicateBrand: true };
|
|
118
313
|
if (opts.now !== void 0) o.now = opts.now;
|
|
@@ -121,7 +316,7 @@ function codecOpts(opts) {
|
|
|
121
316
|
}
|
|
122
317
|
//#endregion
|
|
123
318
|
//#region bin/cli.ts
|
|
124
|
-
process.exitCode = run({
|
|
319
|
+
process.exitCode = await run({
|
|
125
320
|
argv: process.argv.slice(2),
|
|
126
321
|
stdout: (s) => process.stdout.write(s),
|
|
127
322
|
stderr: (s) => process.stderr.write(s)
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts","../bin/cli.ts"],"sourcesContent":["import { createId, type Options } from \"./id.js\";\nimport type { Id } from \"./types.js\";\n\nexport type RunOpts = {\n argv: ReadonlyArray<string>;\n stdout: (chunk: string) => void;\n stderr: (chunk: string) => void;\n now?: Options[\"now\"];\n rng?: Options[\"rng\"];\n};\n\nexport function run(opts: RunOpts): number {\n const [subcommand, ...rest] = opts.argv;\n if (subcommand === \"generate\" || subcommand === \"g\") return runGenerate(rest, opts);\n if (subcommand === \"inspect\" || subcommand === \"i\") return runInspect(rest, opts);\n if (subcommand === undefined || subcommand === \"--help\" || subcommand === \"-h\") {\n opts.stdout(usage());\n return 0;\n }\n opts.stderr(usage());\n return 1;\n}\n\nfunction usage(): string {\n return [\n \"Usage: ids <subcommand> [args]\",\n \"\",\n \"Subcommands:\",\n \" inspect, i <id> Decode an ID and print brand, timestamp, and canonical form.\",\n \" generate, g <brand> [--count, -c N] Mint one or more canonical IDs for the given brand.\",\n \"\",\n ].join(\"\\n\");\n}\n\nfunction runInspect(args: ReadonlyArray<string>, opts: RunOpts): number {\n const [input] = args;\n if (input === undefined) {\n opts.stderr(usage());\n return 1;\n }\n const brand = input.slice(0, 3).toLowerCase();\n let codec;\n try {\n codec = createId(brand, codecOpts(opts));\n } catch (err) {\n opts.stderr((err as Error).message + \"\\n\");\n return 1;\n }\n const validation = codec[\"~standard\"].validate(input);\n if (validation.issues) {\n opts.stderr(validation.issues[0]!.message + \"\\n\");\n return 1;\n }\n const canonical = validation.value;\n const timestamp = codec.extractTimestamp(canonical);\n const nowMs = (opts.now ?? Date.now)();\n const relative = formatRelative(timestamp.getTime(), nowMs);\n const inputLine = describeInputForm(input, canonical);\n opts.stdout(\n [\n `brand: ${brand}`,\n `timestamp: ${timestamp.toISOString()} (${relative})`,\n `canonical: ${canonical}`,\n `input: ${inputLine}`,\n \"\",\n ].join(\"\\n\"),\n );\n return 0;\n}\n\nfunction describeInputForm(input: string, canonical: Id<string>): string {\n if (input === canonical) return \"canonical\";\n const notes: string[] = [];\n if (input !== input.toLowerCase()) notes.push(\"was uppercase\");\n if (/[ilo]/i.test(input.slice(4))) notes.push(\"used Crockford aliases\");\n return `not canonical (${notes.join(\" + \")})`;\n}\n\nconst msPerSecond = 1000;\nconst msPerMinute = 60 * msPerSecond;\nconst msPerHour = 60 * msPerMinute;\nconst msPerDay = 24 * msPerHour;\nconst daysPerMonth = 30.44;\nconst monthsPerYear = 12;\n\nfunction formatRelative(thenMs: number, nowMs: number): string {\n const diff = nowMs - thenMs;\n const abs = Math.abs(diff);\n const suffix = diff < 0 ? \"from now\" : \"ago\";\n\n const head = headUnits(abs);\n return head === \"\" ? \"just now\" : `${head} ${suffix}`;\n}\n\nfunction headUnits(abs: number): string {\n if (abs < msPerMinute) return \"\";\n if (abs < msPerHour) return unit(Math.round(abs / msPerMinute), \"minute\");\n if (abs < msPerDay) return unit(Math.round(abs / msPerHour), \"hour\");\n if (abs < msPerDay * daysPerMonth) return unit(Math.round(abs / msPerDay), \"day\");\n\n const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));\n if (totalMonths < monthsPerYear) return unit(totalMonths, \"month\");\n\n const years = Math.floor(totalMonths / monthsPerYear);\n const months = totalMonths % monthsPerYear;\n return months === 0 ? unit(years, \"year\") : `${unit(years, \"year\")} ${unit(months, \"month\")}`;\n}\n\nfunction unit(n: number, name: string): string {\n return `${n} ${n === 1 ? name : `${name}s`}`;\n}\n\nfunction runGenerate(args: ReadonlyArray<string>, opts: RunOpts): number {\n const [brand, ...flags] = args;\n const count = parseCount(flags);\n if (typeof count === \"string\") {\n opts.stderr(count + \"\\n\");\n return 1;\n }\n let codec;\n try {\n codec = createId(brand ?? \"\", codecOpts(opts));\n } catch (err) {\n opts.stderr((err as Error).message + \"\\n\");\n return 1;\n }\n for (let i = 0; i < count; i++) opts.stdout(codec.generate() + \"\\n\");\n return 0;\n}\n\nfunction parseCount(flags: ReadonlyArray<string>): number | string {\n const idx = flags.findIndex((f) => f === \"--count\" || f === \"-c\");\n if (idx === -1) return 1;\n const raw = flags[idx + 1];\n if (raw === undefined) return \"--count requires a value\";\n if (!/^[1-9][0-9]*$/.test(raw)) return `--count must be a positive integer, got '${raw}'`;\n return Number(raw);\n}\n\nfunction codecOpts(opts: RunOpts): Partial<Options> {\n // CLI invocations are intentionally ephemeral — one codec per run, never\n // retained — so a repeated `createId(brand)` here is not the bundling/import\n // bug that the duplicate-brand warning is designed to catch.\n const o: Partial<Options> = { allowDuplicateBrand: true };\n if (opts.now !== undefined) o.now = opts.now;\n if (opts.rng !== undefined) o.rng = opts.rng;\n return o;\n}\n","#!/usr/bin/env node\nimport { run } from \"../src/cli.js\";\n\nprocess.exitCode = run({\n argv: process.argv.slice(2),\n stdout: (s) => process.stdout.write(s),\n stderr: (s) => process.stderr.write(s),\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts","../bin/cli.ts"],"sourcesContent":["import { createId, type Options } from \"./id.js\";\nimport {\n createOpaqueId,\n decodeOpaqueKey,\n encodeOpaqueKey,\n importOpaqueKey,\n type OpaqueKeyFormat,\n} from \"./opaque.js\";\nimport type { Id } from \"./types.js\";\n\nexport type RunOpts = {\n argv: ReadonlyArray<string>;\n stdout: (chunk: string) => void;\n stderr: (chunk: string) => void;\n now?: Options[\"now\"];\n rng?: Options[\"rng\"];\n /** Defaults to `process.env`. Injected in tests for `IDS_KEY`. */\n env?: Readonly<Record<string, string | undefined>>;\n};\n\nexport async function run(opts: RunOpts): Promise<number> {\n const [subcommand, ...rest] = opts.argv;\n if (subcommand === \"generate\" || subcommand === \"g\") return runGenerate(rest, opts);\n if (subcommand === \"inspect\" || subcommand === \"i\") return runInspect(rest, opts);\n if (subcommand === \"keygen\" || subcommand === \"k\") return runKeygen(rest, opts);\n if (subcommand === undefined || subcommand === \"--help\" || subcommand === \"-h\") {\n opts.stdout(usage());\n return 0;\n }\n opts.stderr(usage());\n return 1;\n}\n\nfunction usage(): string {\n return [\n \"Usage: ids <subcommand> [args]\",\n \"\",\n \"Subcommands:\",\n \" inspect, i <id> [--opaque] [--key-format hex|base64url]\",\n \" Decode an ID and print brand, timestamp, and canonical form.\",\n \" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).\",\n \" generate, g <brand> [--count, -c N] [--opaque] [--key-format hex|base64url]\",\n \" Mint one or more canonical IDs for the given brand.\",\n \" --opaque reads the AES key from IDS_KEY (hex by default; IDS_KEY_FORMAT or --key-format).\",\n \" keygen, k [--bits 128|192|256] [--key-format hex|base64url]\",\n \" Emit a random AES key for importOpaqueKey (stdout only).\",\n \"\",\n ].join(\"\\n\");\n}\n\nfunction runInspect(args: ReadonlyArray<string>, opts: RunOpts): Promise<number> {\n const { flags, values, positionals } = splitFlags(args);\n const [input] = positionals;\n if (input === undefined) {\n opts.stderr(usage());\n return Promise.resolve(1);\n }\n const opaque = flags.has(\"--opaque\");\n const brand = input.slice(0, 3).toLowerCase();\n if (opaque) {\n const format = parseOpaqueKeyFormat(values, opts);\n if (isKeyFormatError(format)) {\n opts.stderr(format + \"\\n\");\n return Promise.resolve(1);\n }\n return runOpaqueInspect(brand, input, format, opts);\n }\n let codec;\n try {\n codec = createId(brand, codecOpts(opts));\n } catch (err) {\n opts.stderr((err as Error).message + \"\\n\");\n return Promise.resolve(1);\n }\n const validation = codec[\"~standard\"].validate(input);\n if (validation.issues) {\n opts.stderr(validation.issues[0]!.message + \"\\n\");\n return Promise.resolve(1);\n }\n const canonical = validation.value;\n const timestamp = codec.extractTimestamp(canonical);\n const nowMs = (opts.now ?? Date.now)();\n const relative = formatRelative(timestamp.getTime(), nowMs);\n const inputLine = describeInputForm(input, canonical);\n opts.stdout(\n [\n `brand: ${brand}`,\n `timestamp: ${timestamp.toISOString()} (${relative})`,\n `canonical: ${canonical}`,\n `input: ${inputLine}`,\n \"\",\n ].join(\"\\n\"),\n );\n return Promise.resolve(0);\n}\n\nasync function runOpaqueInspect(\n brand: string,\n input: string,\n format: OpaqueKeyFormat,\n opts: RunOpts,\n): Promise<number> {\n const keyResult = await loadOpaqueKey(opts, format);\n if (typeof keyResult === \"string\") {\n opts.stderr(keyResult + \"\\n\");\n return 1;\n }\n let codec;\n try {\n codec = createOpaqueId(brand, { key: keyResult, ...codecOpts(opts) });\n } catch (err) {\n opts.stderr((err as Error).message + \"\\n\");\n return 1;\n }\n const validation = codec[\"~standard\"].validate(input);\n if (validation.issues) {\n opts.stderr(validation.issues[0]!.message + \"\\n\");\n return 1;\n }\n const canonical = validation.value;\n const timestamp = await codec.extractTimestamp(canonical);\n const nowMs = (opts.now ?? Date.now)();\n const relative = formatRelative(timestamp.getTime(), nowMs);\n const inputLine = describeInputForm(input, canonical);\n opts.stderr(\n \"note: timestamp assumes IDS_KEY matches the key used at generation; a wrong key yields a plausible but incorrect timestamp\\n\",\n );\n opts.stdout(\n [\n `brand: ${brand}`,\n `timestamp: ${timestamp.toISOString()} (${relative})`,\n `canonical: ${canonical}`,\n `input: ${inputLine}`,\n \"\",\n ].join(\"\\n\"),\n );\n return 0;\n}\n\nfunction describeInputForm(input: string, canonical: Id<string>): string {\n if (input === canonical) return \"canonical\";\n const notes: string[] = [];\n if (input !== input.toLowerCase()) notes.push(\"was uppercase\");\n if (/[ilo]/i.test(input.slice(4))) notes.push(\"used Crockford aliases\");\n return `not canonical (${notes.join(\" + \")})`;\n}\n\nconst msPerSecond = 1000;\nconst msPerMinute = 60 * msPerSecond;\nconst msPerHour = 60 * msPerMinute;\nconst msPerDay = 24 * msPerHour;\nconst daysPerMonth = 30.44;\nconst monthsPerYear = 12;\n\nfunction formatRelative(thenMs: number, nowMs: number): string {\n const diff = nowMs - thenMs;\n const abs = Math.abs(diff);\n const suffix = diff < 0 ? \"from now\" : \"ago\";\n\n const head = headUnits(abs);\n return head === \"\" ? \"just now\" : `${head} ${suffix}`;\n}\n\nfunction headUnits(abs: number): string {\n if (abs < msPerMinute) return \"\";\n if (abs < msPerHour) return unit(Math.round(abs / msPerMinute), \"minute\");\n if (abs < msPerDay) return unit(Math.round(abs / msPerHour), \"hour\");\n if (abs < msPerDay * daysPerMonth) return unit(Math.round(abs / msPerDay), \"day\");\n\n const totalMonths = Math.round(abs / (msPerDay * daysPerMonth));\n if (totalMonths < monthsPerYear) return unit(totalMonths, \"month\");\n\n const years = Math.floor(totalMonths / monthsPerYear);\n const months = totalMonths % monthsPerYear;\n return months === 0 ? unit(years, \"year\") : `${unit(years, \"year\")} ${unit(months, \"month\")}`;\n}\n\nfunction unit(n: number, name: string): string {\n return `${n} ${n === 1 ? name : `${name}s`}`;\n}\n\nfunction runGenerate(args: ReadonlyArray<string>, opts: RunOpts): Promise<number> {\n const { flags, values, positionals } = splitFlags(args);\n const [brand] = positionals;\n const count = parseCount(values);\n if (typeof count === \"string\") {\n opts.stderr(count + \"\\n\");\n return Promise.resolve(1);\n }\n const opaque = flags.has(\"--opaque\");\n if (opaque) {\n const format = parseOpaqueKeyFormat(values, opts);\n if (isKeyFormatError(format)) {\n opts.stderr(format + \"\\n\");\n return Promise.resolve(1);\n }\n return runOpaqueGenerate(brand ?? \"\", count, format, opts);\n }\n let codec;\n try {\n codec = createId(brand ?? \"\", codecOpts(opts));\n } catch (err) {\n opts.stderr((err as Error).message + \"\\n\");\n return Promise.resolve(1);\n }\n for (let i = 0; i < count; i++) opts.stdout(codec.generate() + \"\\n\");\n return Promise.resolve(0);\n}\n\nasync function runOpaqueGenerate(\n brand: string,\n count: number,\n format: OpaqueKeyFormat,\n opts: RunOpts,\n): Promise<number> {\n const keyResult = await loadOpaqueKey(opts, format);\n if (typeof keyResult === \"string\") {\n opts.stderr(keyResult + \"\\n\");\n return 1;\n }\n let codec;\n try {\n codec = createOpaqueId(brand, { key: keyResult, ...codecOpts(opts) });\n } catch (err) {\n opts.stderr((err as Error).message + \"\\n\");\n return 1;\n }\n for (let i = 0; i < count; i++) opts.stdout((await codec.generate()) + \"\\n\");\n return 0;\n}\n\nfunction runKeygen(args: ReadonlyArray<string>, opts: RunOpts): Promise<number> {\n const { values } = splitFlags(args);\n const bits = parseBits(values);\n if (typeof bits === \"string\") {\n opts.stderr(bits + \"\\n\");\n return Promise.resolve(1);\n }\n const format = parseKeygenFormat(values);\n if (isKeyFormatError(format)) {\n opts.stderr(format + \"\\n\");\n return Promise.resolve(1);\n }\n const bytes = new Uint8Array(bits / 8);\n crypto.getRandomValues(bytes);\n opts.stdout(encodeOpaqueKey(bytes, format) + \"\\n\");\n return Promise.resolve(0);\n}\n\nasync function loadOpaqueKey(opts: RunOpts, format: OpaqueKeyFormat): Promise<CryptoKey | string> {\n const env = opts.env ?? process.env;\n const raw = env.IDS_KEY;\n if (raw === undefined || raw === \"\") return \"missing IDS_KEY environment variable\";\n try {\n return importOpaqueKey(decodeOpaqueKey(raw, format));\n } catch (err) {\n return (err as Error).message;\n }\n}\n\nfunction parseCount(values: Map<string, string>): number | string {\n const raw = values.get(\"--count\") ?? values.get(\"-c\");\n if (raw === undefined) return 1;\n if (raw === \"\") return \"--count requires a value\";\n if (!/^[1-9][0-9]*$/.test(raw)) return `--count must be a positive integer, got '${raw}'`;\n return Number(raw);\n}\n\nfunction parseBits(values: Map<string, string>): number | string {\n const raw = values.get(\"--bits\");\n if (raw === undefined) return 256;\n if (raw === \"\") return \"--bits requires a value\";\n if (raw === \"128\") return 128;\n if (raw === \"192\") return 192;\n if (raw === \"256\") return 256;\n return `--bits must be 128, 192, or 256, got '${raw}'`;\n}\n\nfunction isKeyFormatError(result: OpaqueKeyFormat | string): result is string {\n return result !== \"hex\" && result !== \"base64url\";\n}\n\nfunction parseKeyFormatFlag(values: Map<string, string>): OpaqueKeyFormat | string | undefined {\n const fromFlag = values.get(\"--key-format\");\n if (fromFlag === undefined) return undefined;\n if (fromFlag === \"\") return \"--key-format requires a value\";\n if (fromFlag === \"hex\" || fromFlag === \"base64url\") return fromFlag;\n return `--key-format must be hex or base64url, got '${fromFlag}'`;\n}\n\nfunction parseKeygenFormat(values: Map<string, string>): OpaqueKeyFormat | string {\n const fromFlag = parseKeyFormatFlag(values);\n if (fromFlag === undefined) return \"hex\";\n return fromFlag;\n}\n\nfunction parseOpaqueKeyFormat(\n values: Map<string, string>,\n opts: RunOpts,\n): OpaqueKeyFormat | string {\n const fromFlag = parseKeyFormatFlag(values);\n if (fromFlag !== undefined) return fromFlag;\n const env = opts.env ?? process.env;\n const fromEnv = env.IDS_KEY_FORMAT;\n if (fromEnv === undefined || fromEnv === \"\") return \"hex\";\n if (fromEnv === \"hex\" || fromEnv === \"base64url\") return fromEnv;\n return `IDS_KEY_FORMAT must be hex or base64url, got '${fromEnv}'`;\n}\n\ntype ParsedFlags = {\n flags: Set<string>;\n values: Map<string, string>;\n positionals: string[];\n};\n\nfunction splitFlagToken(arg: string): { flag: string; inlineValue: string | undefined } {\n const eq = arg.indexOf(\"=\");\n if (eq <= 0) return { flag: arg, inlineValue: undefined };\n return { flag: arg.slice(0, eq), inlineValue: arg.slice(eq + 1) };\n}\n\nfunction splitFlags(args: ReadonlyArray<string>): ParsedFlags {\n const flags = new Set<string>();\n const values = new Map<string, string>();\n const positionals: string[] = [];\n const valueFlags = new Set([\"--count\", \"-c\", \"--bits\", \"--key-format\"]);\n for (let i = 0; i < args.length; i++) {\n const raw = args[i]!;\n const { flag, inlineValue } = splitFlagToken(raw);\n if (flag === \"--opaque\") {\n flags.add(flag);\n continue;\n }\n if (valueFlags.has(flag)) {\n if (inlineValue !== undefined) {\n flags.add(flag);\n values.set(flag, inlineValue);\n continue;\n }\n const value = args[i + 1];\n if (value === undefined || value.startsWith(\"-\")) {\n values.set(flag, \"\");\n continue;\n }\n flags.add(flag);\n values.set(flag, value);\n i++;\n continue;\n }\n if (flag.startsWith(\"-\")) {\n flags.add(flag);\n continue;\n }\n positionals.push(raw);\n }\n return { flags, values, positionals };\n}\n\nfunction codecOpts(opts: RunOpts): Partial<Options> {\n // CLI invocations are intentionally ephemeral — one codec per run, never\n // retained — so a repeated `createId(brand)` here is not the bundling/import\n // bug that the duplicate-brand warning is designed to catch.\n const o: Partial<Options> = { allowDuplicateBrand: true };\n if (opts.now !== undefined) o.now = opts.now;\n if (opts.rng !== undefined) o.rng = opts.rng;\n return o;\n}\n","#!/usr/bin/env node\nimport { run } from \"../src/cli.js\";\n\nprocess.exitCode = await run({\n argv: process.argv.slice(2),\n stdout: (s) => process.stdout.write(s),\n stderr: (s) => process.stderr.write(s),\n});\n"],"mappings":"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"}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
//#region src/brand.ts
|
|
2
|
+
const brandPattern = /^[a-z]{3}$/;
|
|
3
|
+
/** Validates a three-character lowercase brand. Throws on invalid input. */
|
|
4
|
+
function validateBrand(brand) {
|
|
5
|
+
if (!brandPattern.test(brand)) throw new Error("invalid brand, expected three lowercase a-z characters");
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
1
8
|
//#region src/base32.ts
|
|
2
9
|
const alphabet = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
3
10
|
const valueToCharCode = new Uint8Array(32);
|
|
@@ -37,13 +44,50 @@ function decodeBase32(str) {
|
|
|
37
44
|
return result;
|
|
38
45
|
}
|
|
39
46
|
//#endregion
|
|
47
|
+
//#region src/wire/envelope.ts
|
|
48
|
+
/** Encodes a 16-byte payload as lowercase Crockford base32 (26 chars). */
|
|
49
|
+
function encodePayload(bytes) {
|
|
50
|
+
return encodeBase32(bytes);
|
|
51
|
+
}
|
|
52
|
+
/** Decodes a 26-char base32 payload suffix to 16 bytes. Trust-the-type. */
|
|
53
|
+
function decodePayload(base32) {
|
|
54
|
+
return decodeBase32(base32);
|
|
55
|
+
}
|
|
56
|
+
/** Composes a canonical wire ID from a prefix and 16-byte payload. */
|
|
57
|
+
function toWireId(prefix, payload) {
|
|
58
|
+
return prefix + encodePayload(payload);
|
|
59
|
+
}
|
|
60
|
+
/** Decodes the full 16-byte payload from a trusted wire ID. */
|
|
61
|
+
function payloadBytesFromId(prefix, id) {
|
|
62
|
+
return decodePayload(id.slice(prefix.length));
|
|
63
|
+
}
|
|
64
|
+
const payloadBase32Length = Math.ceil(128 / 5);
|
|
65
|
+
const base32CharClass = "[0-9a-hjkmnp-tv-z]";
|
|
66
|
+
const timestampBase32Length = Math.ceil(48 / 5);
|
|
67
|
+
/** Write the timestamp in big-endian; encoded via mod-256 to avoid 32-bit bitwise coercion. */
|
|
68
|
+
function writeTimestamp(ms, buffer) {
|
|
69
|
+
if (Number.isNaN(ms)) throw new Error("timestamp is not a number");
|
|
70
|
+
if (ms < 0) throw new Error("timestamp is negative");
|
|
71
|
+
if (ms >= 2 ** 48) throw new Error("timestamp exceeds 48-bit range");
|
|
72
|
+
for (let i = 5; i >= 0; i--) {
|
|
73
|
+
buffer[i] = ms % 256;
|
|
74
|
+
ms = Math.floor(ms / 256);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Decode the first `timestampByteLength` bytes of a buffer as a big-endian unsigned millisecond timestamp. */
|
|
78
|
+
function readTimestampMs(buffer) {
|
|
79
|
+
let ms = 0;
|
|
80
|
+
for (let i = 0; i < 6; i++) ms = ms * 256 + buffer[i];
|
|
81
|
+
return ms;
|
|
82
|
+
}
|
|
83
|
+
/** Decodes ms from the first 10 base32 chars of a payload suffix (partial decode). */
|
|
84
|
+
function readTimestampMsFromBase32Suffix(base32Suffix) {
|
|
85
|
+
return readTimestampMs(decodeBase32(base32Suffix.slice(0, timestampBase32Length)));
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
40
88
|
//#region src/registry.ts
|
|
41
|
-
const brandPattern = /^[a-z]{3}$/;
|
|
42
89
|
const registeredBrands = /* @__PURE__ */ new Set();
|
|
43
90
|
const warnedBrands = /* @__PURE__ */ new Set();
|
|
44
|
-
function validateBrand(brand) {
|
|
45
|
-
if (!brandPattern.test(brand)) throw new Error("invalid brand, expected three lowercase a-z characters");
|
|
46
|
-
}
|
|
47
91
|
function registerBrand(brand, allowDuplicateBrand) {
|
|
48
92
|
if (typeof process === "undefined" || process.env.NODE_ENV === "production" || allowDuplicateBrand) return;
|
|
49
93
|
if (registeredBrands.has(brand)) {
|
|
@@ -53,8 +97,8 @@ function registerBrand(brand, allowDuplicateBrand) {
|
|
|
53
97
|
}
|
|
54
98
|
} else registeredBrands.add(brand);
|
|
55
99
|
}
|
|
56
|
-
|
|
57
|
-
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/wire/parse.ts
|
|
58
102
|
const replacePattern = /[ilo]/g;
|
|
59
103
|
const aliasTestPattern = /[ilo]/;
|
|
60
104
|
const replacer = (match) => match === "o" ? "0" : "1";
|
|
@@ -97,26 +141,33 @@ function errorMessage(prefix, error) {
|
|
|
97
141
|
case "invalid_base32": return "invalid base32 payload";
|
|
98
142
|
}
|
|
99
143
|
}
|
|
100
|
-
function writeTimestamp(ms, buffer) {
|
|
101
|
-
if (Number.isNaN(ms)) throw new Error("timestamp is not a number");
|
|
102
|
-
if (ms < 0) throw new Error("timestamp is negative");
|
|
103
|
-
if (ms >= 2 ** 48) throw new Error("timestamp exceeds 48-bit range");
|
|
104
|
-
for (let i = 5; i >= 0; i--) {
|
|
105
|
-
buffer[i] = ms % 256;
|
|
106
|
-
ms = Math.floor(ms / 256);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function readTimestampMs(buffer) {
|
|
110
|
-
let ms = 0;
|
|
111
|
-
for (let i = 0; i < 6; i++) ms = ms * 256 + buffer[i];
|
|
112
|
-
return ms;
|
|
113
|
-
}
|
|
114
144
|
function standardValidate(prefix, value) {
|
|
115
145
|
const result = safeParse(prefix, value);
|
|
116
146
|
if (result.ok) return { value: result.id };
|
|
117
147
|
return { issues: [{ message: errorMessage(prefix, result.error) }] };
|
|
118
148
|
}
|
|
119
149
|
//#endregion
|
|
120
|
-
|
|
150
|
+
//#region src/wire/codec-shell.ts
|
|
151
|
+
/** Wire-only methods shared by every codec variant for a fixed prefix. */
|
|
152
|
+
function wireMethods(prefix) {
|
|
153
|
+
return {
|
|
154
|
+
is: (value) => is(prefix, value),
|
|
155
|
+
parse: (value) => parse(prefix, value),
|
|
156
|
+
safeParse: (value) => safeParse(prefix, value),
|
|
157
|
+
toJsonSchema: (brand, example) => ({
|
|
158
|
+
type: "string",
|
|
159
|
+
pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,
|
|
160
|
+
description: `Branded ID for '${brand}'`,
|
|
161
|
+
example
|
|
162
|
+
}),
|
|
163
|
+
"~standard": {
|
|
164
|
+
version: 1,
|
|
165
|
+
vendor: "@smonn/ids",
|
|
166
|
+
validate: (value) => standardValidate(prefix, value)
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { writeTimestamp as a, toWireId as c, readTimestampMsFromBase32Suffix as i, validateBrand as l, registerBrand as n, payloadBase32Length as o, readTimestampMs as r, payloadBytesFromId as s, wireMethods as t };
|
|
121
172
|
|
|
122
|
-
//# sourceMappingURL=
|
|
173
|
+
//# sourceMappingURL=codec-shell-C0arqqX3.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec-shell-C0arqqX3.mjs","names":[],"sources":["../src/brand.ts","../src/base32.ts","../src/wire/envelope.ts","../src/wire/invariants.ts","../src/wire/timestamp-bytes.ts","../src/registry.ts","../src/wire/parse.ts","../src/wire/codec-shell.ts"],"sourcesContent":["const brandPattern = /^[a-z]{3}$/;\n\n/** Validates a three-character lowercase brand. Throws on invalid input. */\nexport function validateBrand(brand: string): void {\n if (!brandPattern.test(brand)) {\n throw new Error(\"invalid brand, expected three lowercase a-z characters\");\n }\n}\n","/*\n This is based on Crockford's Base32 spec: https://www.crockford.com/base32.html\n One difference is that it uses lowercase instead of uppercase when encoding.\n\n These functions are internal: callers (id.ts) guarantee that input is a\n 16-byte buffer for encode, or a string of characters drawn from the alphabet\n for decode. Invalid input produces silent garbage rather than a thrown error,\n consistent with the trust-the-type rule in ADR-0003.\n*/\n\nexport const alphabet = \"0123456789abcdefghjkmnpqrstvwxyz\";\n\n// 0–31 → ASCII char code, for write-into-codes-then-fromCharCode encoding.\nconst valueToCharCode = new Uint8Array(32);\nfor (let i = 0; i < 32; i++) valueToCharCode[i] = alphabet.charCodeAt(i);\n\n// charCode → 0–31 value. Canonical lowercase only; upstream resolves case and\n// o/i/l aliases before any string reaches decodeBase32.\nconst INVALID = 0xff;\nconst charCodeToValue = new Uint8Array(256).fill(INVALID);\nfor (let i = 0; i < alphabet.length; i++) charCodeToValue[alphabet.charCodeAt(i)] = i;\n\nexport function encodeBase32(bytes: Uint8Array): string {\n // Build an Array<number> of char codes and pass it to fromCharCode.apply.\n // Faster than `result += char` (avoids cons-string overhead) and than\n // Uint8Array variants (apply has a fast path for plain Arrays).\n // oxlint-disable-next-line no-new-array\n const codes = new Array<number>(Math.floor((bytes.length * 8) / 5) + 1);\n let chi = 0;\n let bits = 0;\n let value = 0;\n\n for (let i = 0; i < bytes.length; i++) {\n value = (value << 8) | bytes[i]!;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n codes[chi++] = valueToCharCode[(value >>> bits) & 0x1f]!;\n }\n }\n codes[chi] = valueToCharCode[(value << (5 - bits)) & 0x1f]!;\n return String.fromCharCode.apply(null, codes);\n}\n\nexport function decodeBase32(str: string): Uint8Array {\n const result = new Uint8Array(Math.floor((str.length * 5) / 8));\n let bits = 0;\n let value = 0;\n let index = 0;\n\n for (let i = 0; i < str.length; i++) {\n const v = charCodeToValue[str.charCodeAt(i)]!;\n value = (value << 5) | v;\n bits += 5;\n if (bits >= 8) {\n bits -= 8;\n result[index++] = (value >>> bits) & 0xff;\n }\n }\n return result;\n}\n","import { decodeBase32, encodeBase32 } from \"../base32.js\";\nimport type { Id, Prefix } from \"../types.js\";\n\n/** Encodes a 16-byte payload as lowercase Crockford base32 (26 chars). */\nfunction encodePayload(bytes: Uint8Array): string {\n return encodeBase32(bytes);\n}\n\n/** Decodes a 26-char base32 payload suffix to 16 bytes. Trust-the-type. */\nfunction decodePayload(base32: string): Uint8Array {\n return decodeBase32(base32);\n}\n\n/** Composes a canonical wire ID from a prefix and 16-byte payload. */\nexport function toWireId<Brand extends string>(\n prefix: Prefix<Brand>,\n payload: Uint8Array,\n): Id<Brand> {\n return (prefix + encodePayload(payload)) as Id<Brand>;\n}\n\n/** Decodes the full 16-byte payload from a trusted wire ID. */\nexport function payloadBytesFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n id: Id<Brand>,\n): Uint8Array {\n return decodePayload(id.slice(prefix.length));\n}\n","// Payload is always 16 bytes on the wire (every codec). 16 bytes → 26 Crockford\n// base32 chars. ADR-0002 codifies this as the shared wire-format invariant.\nexport const payloadByteLength: number = 16;\nexport const payloadBase32Length: number = Math.ceil((payloadByteLength * 8) / 5);\n\n// Compact regex character class for the canonical lowercase Crockford alphabet\n// (`0123456789abcdefghjkmnpqrstvwxyz` — excludes i, l, o, u). Used in the JSON\n// Schema `pattern`, which describes the canonical wire form only (ADR-0003).\nexport const base32CharClass: string = \"[0-9a-hjkmnp-tv-z]\";\n","import { decodeBase32 } from \"../base32.js\";\n\n// Timestamp byte layout: first N bytes of the plaintext payload encode a\n// big-endian Unix-ms timestamp. Shared by Timestamp and Opaque layouts.\nexport const timestampByteLength: number = 6;\n\nconst timestampBase32Length: number = Math.ceil((timestampByteLength * 8) / 5);\n\n/** Write the timestamp in big-endian; encoded via mod-256 to avoid 32-bit bitwise coercion. */\nexport function writeTimestamp(ms: number, buffer: Uint8Array): void {\n if (Number.isNaN(ms)) throw new Error(\"timestamp is not a number\");\n if (ms < 0) throw new Error(\"timestamp is negative\");\n if (ms >= 2 ** (timestampByteLength * 8)) {\n throw new Error(\"timestamp exceeds 48-bit range\");\n }\n for (let i = timestampByteLength - 1; i >= 0; i--) {\n buffer[i] = ms % 256;\n ms = Math.floor(ms / 256);\n }\n}\n\n/** Decode the first `timestampByteLength` bytes of a buffer as a big-endian unsigned millisecond timestamp. */\nexport function readTimestampMs(buffer: Uint8Array): number {\n let ms = 0;\n for (let i = 0; i < timestampByteLength; i++) ms = ms * 256 + buffer[i]!;\n return ms;\n}\n\n/** Decodes ms from the first 10 base32 chars of a payload suffix (partial decode). */\nexport function readTimestampMsFromBase32Suffix(base32Suffix: string): number {\n return readTimestampMs(decodeBase32(base32Suffix.slice(0, timestampBase32Length)));\n}\n","const registeredBrands = new Set<string>();\nconst warnedBrands = new Set<string>();\n\nexport function registerBrand(brand: string, allowDuplicateBrand: boolean | undefined): void {\n if (\n typeof process === \"undefined\" ||\n process.env.NODE_ENV === \"production\" ||\n allowDuplicateBrand\n ) {\n return;\n }\n\n if (registeredBrands.has(brand)) {\n if (!warnedBrands.has(brand)) {\n console.warn(\n `[@smonn/ids] brand \"${brand}\" was registered more than once — this usually indicates a bundling or import bug, or that more than one codec variant is using the same brand. Pass { allowDuplicateBrand: true } to silence.`,\n );\n warnedBrands.add(brand);\n }\n } else {\n registeredBrands.add(brand);\n }\n}\n","import { alphabet } from \"../base32.js\";\nimport type { Id, ParseError, ParseResult, Prefix } from \"../types.js\";\nimport { payloadBase32Length } from \"./invariants.js\";\n\nconst replacePattern = /[ilo]/g;\nconst aliasTestPattern = /[ilo]/;\nconst replacer = (match: string): string => (match === \"o\" ? \"0\" : \"1\");\nconst base32Pattern = new RegExp(`^[${alphabet}]{${payloadBase32Length}}$`);\n\nexport function safeParse<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n): ParseResult<Brand> {\n if (typeof value !== \"string\") return { ok: false, error: \"not_string\" };\n const lowercase = value.toLowerCase();\n if (!lowercase.startsWith(prefix)) return { ok: false, error: \"invalid_prefix\" };\n\n const sliced = lowercase.slice(prefix.length);\n const base32 = aliasTestPattern.test(sliced)\n ? sliced.replaceAll(replacePattern, replacer)\n : sliced;\n\n if (!base32Pattern.test(base32)) return { ok: false, error: \"invalid_base32\" };\n\n const id = (prefix + base32) as Id<Brand>;\n return { ok: true, id };\n}\n\nexport function parse<Brand extends string>(prefix: Prefix<Brand>, value: unknown): Id<Brand> {\n const result = safeParse(prefix, value);\n if (result.ok) return result.id;\n throw new Error(`Invalid ID: ${result.error}`);\n}\n\nexport function is<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n): value is Id<Brand> {\n if (typeof value !== \"string\") return false;\n if (!value.startsWith(prefix)) return false;\n return base32Pattern.test(value.slice(prefix.length));\n}\n\nfunction errorMessage<Brand extends string>(prefix: Prefix<Brand>, error: ParseError): string {\n switch (error) {\n case \"not_string\":\n return \"expected string\";\n case \"invalid_prefix\":\n return `expected prefix '${prefix}'`;\n case \"invalid_base32\":\n return \"invalid base32 payload\";\n }\n}\n\nexport function standardValidate<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n):\n | { readonly value: Id<Brand>; readonly issues?: undefined }\n | { readonly issues: ReadonlyArray<{ readonly message: string }> } {\n const result = safeParse(prefix, value);\n if (result.ok) return { value: result.id };\n return { issues: [{ message: errorMessage(prefix, result.error) }] };\n}\n","import type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"../types.js\";\nimport { base32CharClass, payloadBase32Length } from \"./invariants.js\";\nimport { is, parse, safeParse, standardValidate } from \"./parse.js\";\n\ntype WireMethods<Brand extends string> = {\n is: (value: unknown) => value is Id<Brand>;\n parse: (value: unknown) => Id<Brand>;\n safeParse: (value: unknown) => ParseResult<Brand>;\n toJsonSchema: (brand: Brand, example: string) => JsonSchema;\n \"~standard\": StandardSchemaProps<Brand>;\n};\n\n/** Wire-only methods shared by every codec variant for a fixed prefix. */\nexport function wireMethods<Brand extends string>(prefix: Prefix<Brand>): WireMethods<Brand> {\n const standard: StandardSchemaProps<Brand> = {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n };\n return {\n is: (value: unknown): value is Id<Brand> => is(prefix, value),\n parse: (value: unknown): Id<Brand> => parse(prefix, value),\n safeParse: (value: unknown): ParseResult<Brand> => safeParse(prefix, value),\n toJsonSchema: (brand: Brand, example: string): JsonSchema => ({\n type: \"string\",\n pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,\n description: `Branded ID for '${brand}'`,\n example,\n }),\n \"~standard\": standard,\n };\n}\n"],"mappings":";AAAA,MAAM,eAAe;;AAGrB,SAAgB,cAAc,OAAqB;CACjD,IAAI,CAAC,aAAa,KAAK,KAAK,GAC1B,MAAM,IAAI,MAAM,wDAAwD;AAE5E;;;ACGA,MAAa,WAAW;AAGxB,MAAM,kBAAkB,IAAI,WAAW,EAAE;AACzC,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,gBAAgB,KAAK,SAAS,WAAW,CAAC;AAKvE,MAAM,kBAAkB,IAAI,WAAW,GAAG,EAAE,KAAK,GAAO;AACxD,KAAK,IAAI,IAAI,GAAG,IAAI,IAAiB,KAAK,gBAAgB,SAAS,WAAW,CAAC,KAAK;AAEpF,SAAgB,aAAa,OAA2B;CAKtD,MAAM,QAAQ,IAAI,MAAc,KAAK,MAAO,MAAM,SAAS,IAAK,CAAC,IAAI,CAAC;CACtE,IAAI,MAAM;CACV,IAAI,OAAO;CACX,IAAI,QAAQ;CAEZ,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,QAAS,SAAS,IAAK,MAAM;EAC7B,QAAQ;EACR,OAAO,QAAQ,GAAG;GAChB,QAAQ;GACR,MAAM,SAAS,gBAAiB,UAAU,OAAQ;EACpD;CACF;CACA,MAAM,OAAO,gBAAiB,SAAU,IAAI,OAAS;CACrD,OAAO,OAAO,aAAa,MAAM,MAAM,KAAK;AAC9C;AAEA,SAAgB,aAAa,KAAyB;CACpD,MAAM,SAAS,IAAI,WAAW,KAAK,MAAO,IAAI,SAAS,IAAK,CAAC,CAAC;CAC9D,IAAI,OAAO;CACX,IAAI,QAAQ;CACZ,IAAI,QAAQ;CAEZ,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;EACnC,MAAM,IAAI,gBAAgB,IAAI,WAAW,CAAC;EAC1C,QAAS,SAAS,IAAK;EACvB,QAAQ;EACR,IAAI,QAAQ,GAAG;GACb,QAAQ;GACR,OAAO,WAAY,UAAU,OAAQ;EACvC;CACF;CACA,OAAO;AACT;;;;ACxDA,SAAS,cAAc,OAA2B;CAChD,OAAO,aAAa,KAAK;AAC3B;;AAGA,SAAS,cAAc,QAA4B;CACjD,OAAO,aAAa,MAAM;AAC5B;;AAGA,SAAgB,SACd,QACA,SACW;CACX,OAAQ,SAAS,cAAc,OAAO;AACxC;;AAGA,SAAgB,mBACd,QACA,IACY;CACZ,OAAO,cAAc,GAAG,MAAM,OAAO,MAAM,CAAC;AAC9C;ACxBA,MAAa,sBAA8B,KAAK,KAAA,MAA+B,CAAC;AAKhF,MAAa,kBAA0B;ACFvC,MAAM,wBAAgC,KAAK,KAAA,KAAiC,CAAC;;AAG7E,SAAgB,eAAe,IAAY,QAA0B;CACnE,IAAI,OAAO,MAAM,EAAE,GAAG,MAAM,IAAI,MAAM,2BAA2B;CACjE,IAAI,KAAK,GAAG,MAAM,IAAI,MAAM,uBAAuB;CACnD,IAAI,MAAM,KAAA,IACR,MAAM,IAAI,MAAM,gCAAgC;CAElD,KAAK,IAAI,IAAA,GAA6B,KAAK,GAAG,KAAK;EACjD,OAAO,KAAK,KAAK;EACjB,KAAK,KAAK,MAAM,KAAK,GAAG;CAC1B;AACF;;AAGA,SAAgB,gBAAgB,QAA4B;CAC1D,IAAI,KAAK;CACT,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KAAK,KAAK,KAAK,MAAM,OAAO;CACrE,OAAO;AACT;;AAGA,SAAgB,gCAAgC,cAA8B;CAC5E,OAAO,gBAAgB,aAAa,aAAa,MAAM,GAAG,qBAAqB,CAAC,CAAC;AACnF;;;AC/BA,MAAM,mCAAmB,IAAI,IAAY;AACzC,MAAM,+BAAe,IAAI,IAAY;AAErC,SAAgB,cAAc,OAAe,qBAAgD;CAC3F,IACE,OAAO,YAAY,eACnB,QAAQ,IAAI,aAAa,gBACzB,qBAEA;CAGF,IAAI,iBAAiB,IAAI,KAAK;MACxB,CAAC,aAAa,IAAI,KAAK,GAAG;GAC5B,QAAQ,KACN,uBAAuB,MAAM,+LAC/B;GACA,aAAa,IAAI,KAAK;EACxB;QAEA,iBAAiB,IAAI,KAAK;AAE9B;;;AClBA,MAAM,iBAAiB;AACvB,MAAM,mBAAmB;AACzB,MAAM,YAAY,UAA2B,UAAU,MAAM,MAAM;AACnE,MAAM,gBAAgB,IAAI,OAAO,KAAK,SAAS,IAAI,oBAAoB,GAAG;AAE1E,SAAgB,UACd,QACA,OACoB;CACpB,IAAI,OAAO,UAAU,UAAU,OAAO;EAAE,IAAI;EAAO,OAAO;CAAa;CACvE,MAAM,YAAY,MAAM,YAAY;CACpC,IAAI,CAAC,UAAU,WAAW,MAAM,GAAG,OAAO;EAAE,IAAI;EAAO,OAAO;CAAiB;CAE/E,MAAM,SAAS,UAAU,MAAM,OAAO,MAAM;CAC5C,MAAM,SAAS,iBAAiB,KAAK,MAAM,IACvC,OAAO,WAAW,gBAAgB,QAAQ,IAC1C;CAEJ,IAAI,CAAC,cAAc,KAAK,MAAM,GAAG,OAAO;EAAE,IAAI;EAAO,OAAO;CAAiB;CAG7E,OAAO;EAAE,IAAI;EAAM,IADP,SAAS;CACC;AACxB;AAEA,SAAgB,MAA4B,QAAuB,OAA2B;CAC5F,MAAM,SAAS,UAAU,QAAQ,KAAK;CACtC,IAAI,OAAO,IAAI,OAAO,OAAO;CAC7B,MAAM,IAAI,MAAM,eAAe,OAAO,OAAO;AAC/C;AAEA,SAAgB,GACd,QACA,OACoB;CACpB,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,CAAC,MAAM,WAAW,MAAM,GAAG,OAAO;CACtC,OAAO,cAAc,KAAK,MAAM,MAAM,OAAO,MAAM,CAAC;AACtD;AAEA,SAAS,aAAmC,QAAuB,OAA2B;CAC5F,QAAQ,OAAR;EACE,KAAK,cACH,OAAO;EACT,KAAK,kBACH,OAAO,oBAAoB,OAAO;EACpC,KAAK,kBACH,OAAO;CACX;AACF;AAEA,SAAgB,iBACd,QACA,OAGmE;CACnE,MAAM,SAAS,UAAU,QAAQ,KAAK;CACtC,IAAI,OAAO,IAAI,OAAO,EAAE,OAAO,OAAO,GAAG;CACzC,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,aAAa,QAAQ,OAAO,KAAK,EAAE,CAAC,EAAE;AACrE;;;;AClDA,SAAgB,YAAkC,QAA2C;CAM3F,OAAO;EACL,KAAK,UAAuC,GAAG,QAAQ,KAAK;EAC5D,QAAQ,UAA8B,MAAM,QAAQ,KAAK;EACzD,YAAY,UAAuC,UAAU,QAAQ,KAAK;EAC1E,eAAe,OAAc,aAAiC;GAC5D,MAAM;GACN,SAAS,IAAI,SAAS,gBAAgB,GAAG,oBAAoB;GAC7D,aAAa,mBAAmB,MAAM;GACtC;EACF;EACA,aAAa;GAdb,SAAS;GACT,QAAQ;GACR,WAAW,UAAmB,iBAAiB,QAAQ,KAAK;EAYxC;CACtB;AACF"}
|