@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smonn/ids",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Simon Ingeson (https://github.com/smonn)",
|
|
6
6
|
"repository": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"@changesets/cli": "2.31.0",
|
|
24
24
|
"@types/node": "25.9.1",
|
|
25
25
|
"@vitest/coverage-v8": "4.1.8",
|
|
26
|
+
"dependency-cruiser": "16.10.4",
|
|
26
27
|
"knip": "6.15.0",
|
|
27
28
|
"mitata": "1.0.34",
|
|
28
29
|
"oxfmt": "0.53.0",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"test": "vitest run",
|
|
45
46
|
"test:watch": "vitest",
|
|
46
47
|
"test:coverage": "vitest run --coverage",
|
|
48
|
+
"depcruise": "depcruise --config .dependency-cruiser.cjs \"src/**/*.ts\" \"bin/**/*.ts\"",
|
|
47
49
|
"knip": "knip",
|
|
48
50
|
"bench:build": "tsdown -c tsdown.bench.config.ts",
|
|
49
51
|
"bench": "pnpm -s bench:build 1>&2 && node bench/dist/index.mjs",
|
package/dist/id-B4GiFKYV.mjs
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { a as readTimestampMs, c as writeTimestamp, d as decodeBase32, f as encodeBase32, i as payloadBase32Length, l as registerBrand, n as is, o as safeParse, r as parse, s as standardValidate, t as base32CharClass, u as validateBrand } from "./shared-CmbAeUdM.mjs";
|
|
2
|
-
//#region src/id.ts
|
|
3
|
-
const hexCharCodeToNibble = new Uint8Array(128);
|
|
4
|
-
for (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;
|
|
5
|
-
for (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;
|
|
6
|
-
const defaultOptions = {
|
|
7
|
-
now: Date.now,
|
|
8
|
-
rng: (target) => {
|
|
9
|
-
const s = crypto.randomUUID();
|
|
10
|
-
target[0] = hexCharCodeToNibble[s.charCodeAt(0)] << 4 | hexCharCodeToNibble[s.charCodeAt(1)];
|
|
11
|
-
target[1] = hexCharCodeToNibble[s.charCodeAt(2)] << 4 | hexCharCodeToNibble[s.charCodeAt(3)];
|
|
12
|
-
target[2] = hexCharCodeToNibble[s.charCodeAt(4)] << 4 | hexCharCodeToNibble[s.charCodeAt(5)];
|
|
13
|
-
target[3] = hexCharCodeToNibble[s.charCodeAt(6)] << 4 | hexCharCodeToNibble[s.charCodeAt(7)];
|
|
14
|
-
target[4] = hexCharCodeToNibble[s.charCodeAt(9)] << 4 | hexCharCodeToNibble[s.charCodeAt(10)];
|
|
15
|
-
target[5] = hexCharCodeToNibble[s.charCodeAt(11)] << 4 | hexCharCodeToNibble[s.charCodeAt(12)];
|
|
16
|
-
target[6] = hexCharCodeToNibble[s.charCodeAt(24)] << 4 | hexCharCodeToNibble[s.charCodeAt(25)];
|
|
17
|
-
target[7] = hexCharCodeToNibble[s.charCodeAt(26)] << 4 | hexCharCodeToNibble[s.charCodeAt(27)];
|
|
18
|
-
target[8] = hexCharCodeToNibble[s.charCodeAt(28)] << 4 | hexCharCodeToNibble[s.charCodeAt(29)];
|
|
19
|
-
target[9] = hexCharCodeToNibble[s.charCodeAt(30)] << 4 | hexCharCodeToNibble[s.charCodeAt(31)];
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
const randomByteLength = 10;
|
|
23
|
-
const timestampBase32Length = Math.ceil(48 / 5);
|
|
24
|
-
function createId(brand, opts = {}) {
|
|
25
|
-
validateBrand(brand);
|
|
26
|
-
registerBrand(brand, opts.allowDuplicateBrand);
|
|
27
|
-
const options = {
|
|
28
|
-
...defaultOptions,
|
|
29
|
-
...opts
|
|
30
|
-
};
|
|
31
|
-
const prefix = `${brand}_`;
|
|
32
|
-
const buffer = new Uint8Array(16);
|
|
33
|
-
const randomView = new Uint8Array(buffer.buffer, 6, randomByteLength);
|
|
34
|
-
return {
|
|
35
|
-
generate: () => generate(prefix, options, buffer, randomView),
|
|
36
|
-
generateAt: (date) => generate(prefix, options, buffer, randomView, date.getTime()),
|
|
37
|
-
is: (value) => is(prefix, value),
|
|
38
|
-
parse: (value) => parse(prefix, value),
|
|
39
|
-
safeParse: (value) => safeParse(prefix, value),
|
|
40
|
-
extractTimestamp: (id) => extractTimestamp(prefix, id),
|
|
41
|
-
minIdForTime: (date) => sentinelIdForTime(prefix, date, 0, buffer, randomView),
|
|
42
|
-
maxIdForTime: (date) => sentinelIdForTime(prefix, date, 255, buffer, randomView),
|
|
43
|
-
toJsonSchema: () => toJsonSchema(brand, prefix, options, buffer, randomView),
|
|
44
|
-
"~standard": {
|
|
45
|
-
version: 1,
|
|
46
|
-
vendor: "@smonn/ids",
|
|
47
|
-
validate: (value) => standardValidate(prefix, value)
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function toJsonSchema(brand, prefix, options, buffer, randomView) {
|
|
52
|
-
return {
|
|
53
|
-
type: "string",
|
|
54
|
-
pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,
|
|
55
|
-
description: `Branded ID for '${brand}'`,
|
|
56
|
-
example: generate(prefix, options, buffer, randomView)
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function generate(prefix, options, buffer, randomView, ms = options.now()) {
|
|
60
|
-
writeTimestamp(ms, buffer);
|
|
61
|
-
options.rng(randomView);
|
|
62
|
-
return prefix + encodeBase32(buffer);
|
|
63
|
-
}
|
|
64
|
-
function sentinelIdForTime(prefix, date, fill, buffer, randomView) {
|
|
65
|
-
writeTimestamp(date.getTime(), buffer);
|
|
66
|
-
randomView.fill(fill);
|
|
67
|
-
return prefix + encodeBase32(buffer);
|
|
68
|
-
}
|
|
69
|
-
function extractTimestamp(prefix, id) {
|
|
70
|
-
const base32 = id.slice(prefix.length, prefix.length + timestampBase32Length);
|
|
71
|
-
return new Date(readTimestampMs(decodeBase32(base32)));
|
|
72
|
-
}
|
|
73
|
-
//#endregion
|
|
74
|
-
export { createId as t };
|
|
75
|
-
|
|
76
|
-
//# sourceMappingURL=id-B4GiFKYV.mjs.map
|
package/dist/id-B4GiFKYV.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"id-B4GiFKYV.mjs","names":[],"sources":["../src/id.ts"],"sourcesContent":["import { decodeBase32, encodeBase32 } from \"./base32.js\";\nimport { registerBrand, validateBrand } from \"./registry.js\";\nimport {\n base32CharClass,\n is,\n parse,\n payloadBase32Length,\n payloadByteLength,\n readTimestampMs,\n safeParse,\n standardValidate,\n timestampByteLength,\n writeTimestamp,\n} from \"./shared.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\n\nexport type Options = {\n now: () => number;\n rng: (target: Uint8Array) => void;\n allowDuplicateBrand?: boolean;\n};\n\nexport type Codec<Brand extends string> = {\n generate(): Id<Brand>;\n generateAt(date: Date): Id<Brand>;\n is(value: unknown): value is Id<Brand>;\n parse(value: unknown): Id<Brand>;\n safeParse(value: unknown): ParseResult<Brand>;\n extractTimestamp(id: Id<Brand>): Date;\n minIdForTime(date: Date): Id<Brand>;\n maxIdForTime(date: Date): Id<Brand>;\n toJsonSchema(): JsonSchema;\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\n// hex charCode → 0–15 nibble, for decoding UUIDv4 strings into bytes.\n// Covers ['0'-'9' = 48–57] and ['a'-'f' = 97–102]; UUIDs are lowercase per spec.\nconst hexCharCodeToNibble = new Uint8Array(128);\nfor (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;\nfor (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;\n\nconst defaultOptions: Options = {\n now: Date.now,\n // crypto.randomUUID is ~7× faster than crypto.getRandomValues in Node 24\n // (~84 ns vs ~610 ns for a 16-byte fill — likely because the UUID path has\n // a tight fixed-format fast path). We use the 122 random bits of a UUIDv4\n // string as our entropy source, harvesting 10 fully-random bytes from\n // positions where no version (hex 12) or variant (hex 16) bits sit.\n // String layout: \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\" — bytes 0–5 are\n // string[0..7]+string[9..12], bytes 6–9 are string[24..31].\n rng: (target) => {\n const s = crypto.randomUUID();\n target[0] =\n (hexCharCodeToNibble[s.charCodeAt(0)]! << 4) | hexCharCodeToNibble[s.charCodeAt(1)]!;\n target[1] =\n (hexCharCodeToNibble[s.charCodeAt(2)]! << 4) | hexCharCodeToNibble[s.charCodeAt(3)]!;\n target[2] =\n (hexCharCodeToNibble[s.charCodeAt(4)]! << 4) | hexCharCodeToNibble[s.charCodeAt(5)]!;\n target[3] =\n (hexCharCodeToNibble[s.charCodeAt(6)]! << 4) | hexCharCodeToNibble[s.charCodeAt(7)]!;\n target[4] =\n (hexCharCodeToNibble[s.charCodeAt(9)]! << 4) | hexCharCodeToNibble[s.charCodeAt(10)]!;\n target[5] =\n (hexCharCodeToNibble[s.charCodeAt(11)]! << 4) | hexCharCodeToNibble[s.charCodeAt(12)]!;\n target[6] =\n (hexCharCodeToNibble[s.charCodeAt(24)]! << 4) | hexCharCodeToNibble[s.charCodeAt(25)]!;\n target[7] =\n (hexCharCodeToNibble[s.charCodeAt(26)]! << 4) | hexCharCodeToNibble[s.charCodeAt(27)]!;\n target[8] =\n (hexCharCodeToNibble[s.charCodeAt(28)]! << 4) | hexCharCodeToNibble[s.charCodeAt(29)]!;\n target[9] =\n (hexCharCodeToNibble[s.charCodeAt(30)]! << 4) | hexCharCodeToNibble[s.charCodeAt(31)]!;\n },\n};\n\nconst randomByteLength = payloadByteLength - timestampByteLength;\nconst timestampBase32Length = Math.ceil((timestampByteLength * 8) / 5);\n\nexport function createId<Brand extends string>(\n brand: Brand,\n opts: Partial<Options> = {},\n): Codec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const options = {\n ...defaultOptions,\n ...opts,\n } satisfies Options;\n\n const prefix: Prefix<Brand> = `${brand}_`;\n // Per-codec scratch buffer. Shared across generate(), generateAt(),\n // minIdForTime(), and maxIdForTime() — all are synchronous and overwrite both\n // the timestamp and random slices before encoding, so successive callers see\n // their own freshly-written bytes. encodeBase32 reads the buffer and\n // returns an independent string, so the caller never sees the buffer itself.\n const buffer = new Uint8Array(payloadByteLength);\n const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);\n\n return {\n generate: () => generate(prefix, options, buffer, randomView),\n generateAt: (date: Date) => generate(prefix, options, buffer, randomView, date.getTime()),\n is: (value: unknown) => is(prefix, value),\n parse: (value: unknown) => parse(prefix, value),\n safeParse: (value: unknown) => safeParse(prefix, value),\n extractTimestamp: (id: Id<Brand>) => extractTimestamp(prefix, id),\n minIdForTime: (date: Date) => sentinelIdForTime(prefix, date, 0x00, buffer, randomView),\n maxIdForTime: (date: Date) => sentinelIdForTime(prefix, date, 0xff, buffer, randomView),\n toJsonSchema: () => toJsonSchema(brand, prefix, options, buffer, randomView),\n \"~standard\": {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n },\n };\n}\n\nfunction toJsonSchema<Brand extends string>(\n brand: Brand,\n prefix: Prefix<Brand>,\n options: Options,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): JsonSchema {\n return {\n type: \"string\",\n pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,\n description: `Branded ID for '${brand}'`,\n example: generate(prefix, options, buffer, randomView),\n };\n}\n\nfunction generate<Brand extends string>(\n prefix: Prefix<Brand>,\n options: Options,\n buffer: Uint8Array,\n randomView: Uint8Array,\n ms: number = options.now(),\n): Id<Brand> {\n writeTimestamp(ms, buffer);\n options.rng(randomView);\n return (prefix + encodeBase32(buffer)) as Id<Brand>;\n}\n\nfunction sentinelIdForTime<Brand extends string>(\n prefix: Prefix<Brand>,\n date: Date,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): Id<Brand> {\n writeTimestamp(date.getTime(), buffer);\n randomView.fill(fill);\n return (prefix + encodeBase32(buffer)) as Id<Brand>;\n}\n\nfunction extractTimestamp<Brand extends string>(prefix: Prefix<Brand>, id: Id<Brand>): Date {\n const base32 = id.slice(prefix.length, prefix.length + timestampBase32Length);\n return new Date(readTimestampMs(decodeBase32(base32)));\n}\n"],"mappings":"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"}
|
package/dist/opaque.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"opaque.mjs","names":[],"sources":["../src/opaque.ts"],"sourcesContent":["import { decodeBase32, encodeBase32 } from \"./base32.js\";\nimport { registerBrand, validateBrand } from \"./registry.js\";\nimport {\n base32CharClass,\n is,\n parse,\n payloadBase32Length,\n payloadByteLength,\n readTimestampMs,\n safeParse,\n standardValidate,\n timestampByteLength,\n writeTimestamp,\n} from \"./shared.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\n\nexport type OpaqueOptions = {\n key: CryptoKey;\n now: () => number;\n rng: (target: Uint8Array) => void;\n allowDuplicateBrand?: boolean;\n};\n\nexport type OpaqueCodec<Brand extends string> = {\n generate(): Promise<Id<Brand>>;\n generateAt(date: Date): Promise<Id<Brand>>;\n is(value: unknown): value is Id<Brand>;\n parse(value: unknown): Id<Brand>;\n safeParse(value: unknown): ParseResult<Brand>;\n extractTimestamp(id: Id<Brand>): Promise<Date>;\n toJsonSchema(): JsonSchema;\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\nconst zeroIv = new Uint8Array(payloadByteLength);\nconst pkcsPad = 0x10;\n\nfunction defaultRng(target: Uint8Array): void {\n crypto.getRandomValues(target as Uint8Array<ArrayBuffer>);\n}\n\nexport function importOpaqueKey(bytes: Uint8Array): Promise<CryptoKey> {\n return crypto.subtle.importKey(\"raw\", bytes as Uint8Array<ArrayBuffer>, \"AES-CBC\", false, [\n \"encrypt\",\n \"decrypt\",\n ]);\n}\n\nexport function createOpaqueId<Brand extends string>(\n brand: Brand,\n opts: { key: CryptoKey } & Partial<Omit<OpaqueOptions, \"key\">>,\n): OpaqueCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const key = opts.key;\n const now = opts.now ?? Date.now;\n const rng = opts.rng ?? defaultRng;\n const prefix: Prefix<Brand> = `${brand}_`;\n\n return {\n generate: () => generate(prefix, key, rng, now()),\n generateAt: (date: Date) => generate(prefix, key, rng, date.getTime()),\n is: (value: unknown) => is(prefix, value),\n parse: (value: unknown) => parse(prefix, value),\n safeParse: (value: unknown) => safeParse(prefix, value),\n extractTimestamp: (id: Id<Brand>) => extractTimestamp(prefix, key, id),\n toJsonSchema: () => toJsonSchema(brand, prefix),\n \"~standard\": {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n },\n };\n}\n\n// Per-call buffers, unlike id.ts's codec-shared scratch. Reuse would be safe\n// (subtle.encrypt/decrypt snapshot inputs synchronously, per WebCrypto step 2\n// before the Promise returns) but subtle dominates this path — the allocation\n// is <1% of total cost, not worth pinning the design to that spec detail.\nasync function generate<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n rng: (target: Uint8Array) => void,\n ms: number,\n): Promise<Id<Brand>> {\n const plaintext = new Uint8Array(payloadByteLength);\n writeTimestamp(ms, plaintext);\n rng(plaintext.subarray(timestampByteLength, payloadByteLength));\n const encrypted = new Uint8Array(\n await crypto.subtle.encrypt({ name: \"AES-CBC\", iv: zeroIv }, key, plaintext),\n );\n return (prefix + encodeBase32(encrypted.subarray(0, payloadByteLength))) as Id<Brand>;\n}\n\nasync function extractTimestamp<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n id: Id<Brand>,\n): Promise<Date> {\n const c1 = decodeBase32(id.slice(prefix.length));\n // Reconstruct C2 = AES_K(P2 XOR C1) where P2 is the PKCS#7 pad block (0x10×16).\n // CBC encrypt of (P2 XOR C1) with IV=0 yields AES_K(P2 XOR C1) as the first 16 bytes.\n const c2Input = new Uint8Array(payloadByteLength);\n for (let i = 0; i < payloadByteLength; i++) c2Input[i] = pkcsPad ^ c1[i]!;\n const c2Encrypted = new Uint8Array(\n await crypto.subtle.encrypt({ name: \"AES-CBC\", iv: zeroIv }, key, c2Input),\n );\n const ciphertext = new Uint8Array(payloadByteLength * 2);\n ciphertext.set(c1, 0);\n ciphertext.set(c2Encrypted.subarray(0, payloadByteLength), payloadByteLength);\n const plaintext = new Uint8Array(\n await crypto.subtle.decrypt({ name: \"AES-CBC\", iv: zeroIv }, key, ciphertext),\n );\n return new Date(readTimestampMs(plaintext));\n}\n\nfunction toJsonSchema<Brand extends string>(brand: Brand, prefix: Prefix<Brand>): JsonSchema {\n return {\n type: \"string\",\n pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,\n description: `Branded ID for '${brand}'`,\n // The Opaque codec cannot synchronously produce a real example (encrypt is\n // async). A deterministic structurally-valid placeholder satisfies the\n // JSON Schema contract without requiring the key at schema time.\n example: prefix + \"0\".repeat(payloadBase32Length),\n };\n}\n"],"mappings":";;AAkCA,MAAM,SAAS,IAAI,WAAA,EAA4B;AAC/C,MAAM,UAAU;AAEhB,SAAS,WAAW,QAA0B;CAC5C,OAAO,gBAAgB,MAAiC;AAC1D;AAEA,SAAgB,gBAAgB,OAAuC;CACrE,OAAO,OAAO,OAAO,UAAU,OAAO,OAAkC,WAAW,OAAO,CACxF,WACA,SACF,CAAC;AACH;AAEA,SAAgB,eACd,OACA,MACoB;CACpB,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAE7C,MAAM,MAAM,KAAK;CACjB,MAAM,MAAM,KAAK,OAAO,KAAK;CAC7B,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,SAAwB,GAAG,MAAM;CAEvC,OAAO;EACL,gBAAgB,SAAS,QAAQ,KAAK,KAAK,IAAI,CAAC;EAChD,aAAa,SAAe,SAAS,QAAQ,KAAK,KAAK,KAAK,QAAQ,CAAC;EACrE,KAAK,UAAmB,GAAG,QAAQ,KAAK;EACxC,QAAQ,UAAmB,MAAM,QAAQ,KAAK;EAC9C,YAAY,UAAmB,UAAU,QAAQ,KAAK;EACtD,mBAAmB,OAAkB,iBAAiB,QAAQ,KAAK,EAAE;EACrE,oBAAoB,aAAa,OAAO,MAAM;EAC9C,aAAa;GACX,SAAS;GACT,QAAQ;GACR,WAAW,UAAmB,iBAAiB,QAAQ,KAAK;EAC9D;CACF;AACF;AAMA,eAAe,SACb,QACA,KACA,KACA,IACoB;CACpB,MAAM,YAAY,IAAI,WAAA,EAA4B;CAClD,eAAe,IAAI,SAAS;CAC5B,IAAI,UAAU,SAAA,GAAA,EAA+C,CAAC;CAI9D,OAAQ,SAAS,aAAa,IAHR,WACpB,MAAM,OAAO,OAAO,QAAQ;EAAE,MAAM;EAAW,IAAI;CAAO,GAAG,KAAK,SAAS,CAEvC,EAAE,SAAS,GAAA,EAAoB,CAAC;AACxE;AAEA,eAAe,iBACb,QACA,KACA,IACe;CACf,MAAM,KAAK,aAAa,GAAG,MAAM,OAAO,MAAM,CAAC;CAG/C,MAAM,UAAU,IAAI,WAAA,EAA4B;CAChD,KAAK,IAAI,IAAI,GAAG,IAAA,IAAuB,KAAK,QAAQ,KAAK,UAAU,GAAG;CACtE,MAAM,cAAc,IAAI,WACtB,MAAM,OAAO,OAAO,QAAQ;EAAE,MAAM;EAAW,IAAI;CAAO,GAAG,KAAK,OAAO,CAC3E;CACA,MAAM,aAAa,IAAI,WAAA,EAAgC;CACvD,WAAW,IAAI,IAAI,CAAC;CACpB,WAAW,IAAI,YAAY,SAAS,GAAA,EAAoB,GAAA,EAAoB;CAC5E,MAAM,YAAY,IAAI,WACpB,MAAM,OAAO,OAAO,QAAQ;EAAE,MAAM;EAAW,IAAI;CAAO,GAAG,KAAK,UAAU,CAC9E;CACA,OAAO,IAAI,KAAK,gBAAgB,SAAS,CAAC;AAC5C;AAEA,SAAS,aAAmC,OAAc,QAAmC;CAC3F,OAAO;EACL,MAAM;EACN,SAAS,IAAI,SAAS,gBAAgB,GAAG,oBAAoB;EAC7D,aAAa,mBAAmB,MAAM;EAItC,SAAS,SAAS,IAAI,OAAO,mBAAmB;CAClD;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"shared-CmbAeUdM.mjs","names":[],"sources":["../src/base32.ts","../src/registry.ts","../src/shared.ts"],"sourcesContent":["/*\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","const brandPattern = /^[a-z]{3}$/;\nconst registeredBrands = new Set<string>();\nconst warnedBrands = new Set<string>();\n\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\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\";\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\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\n// Timestamp byte layout: first N bytes of the plaintext payload encode a\n// big-endian Unix-ms timestamp. Shared by every codec whose plaintext begins\n// with a timestamp (Timestamp, Opaque, Signed, Reverse). The Derived codec\n// does not use this.\nexport const timestampByteLength: number = 6;\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\n// 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\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"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types-Dg2j_zlO.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";KAAY,MAAA,4BAAkC,KAAA;AAAA,KAElC,EAAA,4BAA8B,MAAA,CAAO,KAAA;EAAA,SACtC,OAAA,EAAS,KAAA;AAAA;AAAA,KAGR,UAAA;AAAA,KAEA,WAAA;EACN,EAAA;EAAU,EAAA,EAAI,EAAA,CAAG,KAAA;AAAA;EACjB,EAAA;EAAW,KAAA,EAAO,UAAA;AAAA;AAAA,KAEZ,UAAA;EAAA,SACD,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;EAAA,SACA,OAAA;AAAA;AAAA,KAGC,mBAAA;EAAA,SACD,OAAA;EAAA,SACA,MAAA;EAAA,SACA,QAAA,GACP,KAAA,WACA,OAAA;IAAA,SAAqB,cAAA,GAAiB,MAAA;EAAA;IAAA,SAEzB,KAAA,EAAO,EAAA,CAAG,KAAA;IAAA,SAAiB,MAAA;EAAA;IAAA,SAC3B,MAAA,EAAQ,aAAA;MAAA,SAAyB,OAAA;IAAA;EAAA;EAAA,SACvC,KAAA;IAAA,SAAmB,KAAA;IAAA,SAAyB,MAAA,EAAQ,EAAA,CAAG,KAAA;EAAA;AAAA"}
|