@smonn/ids 0.5.0 → 0.7.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 +555 -18
- package/dist/adapter-types-oHCCSgOO.d.mts +12 -0
- package/dist/adapter-types-oHCCSgOO.d.mts.map +1 -0
- package/dist/cli.mjs +246 -63
- package/dist/cli.mjs.map +1 -1
- package/dist/{codec-shell-dWpxoFmy.mjs → codec-shell-DH-UO4UR.mjs} +8 -8
- package/dist/codec-shell-DH-UO4UR.mjs.map +1 -0
- package/dist/drizzle-CeSni5PB.d.mts +44 -0
- package/dist/drizzle-CeSni5PB.d.mts.map +1 -0
- package/dist/drizzle.d.mts +3 -0
- package/dist/drizzle.mjs +43 -0
- package/dist/drizzle.mjs.map +1 -0
- package/dist/error-Cp5qYZcv.mjs +52 -0
- package/dist/error-Cp5qYZcv.mjs.map +1 -0
- package/dist/error-DTr4i6Ic.d.mts +44 -0
- package/dist/error-DTr4i6Ic.d.mts.map +1 -0
- package/dist/express.d.mts +85 -0
- package/dist/express.d.mts.map +1 -0
- package/dist/express.mjs +90 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +88 -0
- package/dist/fastify.d.mts.map +1 -0
- package/dist/fastify.mjs +91 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +68 -0
- package/dist/hono.d.mts.map +1 -0
- package/dist/hono.mjs +63 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -2
- package/dist/kysely.d.mts +56 -0
- package/dist/kysely.d.mts.map +1 -0
- package/dist/kysely.mjs +43 -0
- package/dist/kysely.mjs.map +1 -0
- package/dist/{opaque-B4ps7Pqk.mjs → opaque-uvjOFY_0.mjs} +37 -20
- package/dist/opaque-uvjOFY_0.mjs.map +1 -0
- package/dist/opaque.d.mts +34 -9
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +3 -2
- package/dist/prisma.d.mts +85 -0
- package/dist/prisma.d.mts.map +1 -0
- package/dist/prisma.mjs +54 -0
- package/dist/prisma.mjs.map +1 -0
- package/dist/reverse-BgFU6JHw.mjs +87 -0
- package/dist/reverse-BgFU6JHw.mjs.map +1 -0
- package/dist/reverse.d.mts +77 -0
- package/dist/reverse.d.mts.map +1 -0
- package/dist/reverse.mjs +3 -0
- package/dist/signed.d.mts +56 -0
- package/dist/signed.d.mts.map +1 -0
- package/dist/signed.mjs +100 -0
- package/dist/signed.mjs.map +1 -0
- package/dist/{timestamp-Bgzxx8bE.mjs → timestamp-B5_UCzc6.mjs} +3 -3
- package/dist/{timestamp-Bgzxx8bE.mjs.map → timestamp-B5_UCzc6.mjs.map} +1 -1
- package/dist/{timestamp-bytes-B57RM7Ho.mjs → timestamp-bytes-BBY7JI33.mjs} +2 -2
- package/dist/{timestamp-bytes-B57RM7Ho.mjs.map → timestamp-bytes-BBY7JI33.mjs.map} +1 -1
- package/dist/wrapped-0vL72Nje.mjs +361 -0
- package/dist/wrapped-0vL72Nje.mjs.map +1 -0
- package/dist/wrapped.d.mts +89 -9
- package/dist/wrapped.d.mts.map +1 -1
- package/dist/wrapped.mjs +3 -336
- package/package.json +45 -3
- package/dist/codec-shell-dWpxoFmy.mjs.map +0 -1
- package/dist/opaque-B4ps7Pqk.mjs.map +0 -1
- package/dist/wrapped.mjs.map +0 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
|
|
2
|
+
import { i as ParseResult, t as Id } from "./types-g7CiQDyE.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/prisma.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Minimum codec interface required by the Prisma adapter.
|
|
7
|
+
*
|
|
8
|
+
* Any codec variant satisfies this type — TimestampCodec, OpaqueTimestampCodec,
|
|
9
|
+
* ReverseTimestampCodec, and WrappedKeyCodec all expose `safeParse`. The adapter
|
|
10
|
+
* never calls key-dependent methods.
|
|
11
|
+
*
|
|
12
|
+
* Intentionally the same structural shape as the Drizzle adapter's IdColumnCodec.
|
|
13
|
+
* Do NOT import IdColumnCodec from `@smonn/ids/drizzle` — that would create
|
|
14
|
+
* cross-adapter coupling.
|
|
15
|
+
*/
|
|
16
|
+
type IdColumnCodec<Brand extends string> = {
|
|
17
|
+
safeParse(value: unknown): ParseResult<Brand>;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Read/write transform pair for integrating `Id<Brand>` with Prisma extensions.
|
|
21
|
+
*
|
|
22
|
+
* **Prisma casting caveat:** Prisma cannot fully brand a generated model field
|
|
23
|
+
* type at the schema level. The `read` function asserts `Id<Brand>` at the
|
|
24
|
+
* TypeScript level, but Prisma's generated types for the model field will not
|
|
25
|
+
* reflect this branding. Callers consuming the validated value from a Prisma
|
|
26
|
+
* result component may need an explicit `as Id<Brand>` cast at the call site.
|
|
27
|
+
*/
|
|
28
|
+
type IdTransform<Brand extends string> = {
|
|
29
|
+
/**
|
|
30
|
+
* Read transform: validates the raw database value via `safeParse` and returns
|
|
31
|
+
* `Id<Brand>`. Throws if the value is missing, malformed, or belongs to a
|
|
32
|
+
* different brand.
|
|
33
|
+
*
|
|
34
|
+
* Use in a Prisma `$extends` result component's `compute` function.
|
|
35
|
+
*/
|
|
36
|
+
read(value: unknown): Id<Brand>;
|
|
37
|
+
/**
|
|
38
|
+
* Write transform: passes `Id<Brand>` through as its canonical string form.
|
|
39
|
+
* `Id<Brand>` is already the canonical string, so this is an identity function
|
|
40
|
+
* at runtime.
|
|
41
|
+
*
|
|
42
|
+
* Use in a Prisma `$extends` query component or explicit `data` mapping.
|
|
43
|
+
*/
|
|
44
|
+
write(value: Id<Brand>): string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Creates a read/write transform pair for use with Prisma's `$extends` extension model.
|
|
48
|
+
*
|
|
49
|
+
* Works with any codec variant exposing `safeParse` (TimestampCodec,
|
|
50
|
+
* OpaqueTimestampCodec, ReverseTimestampCodec, WrappedKeyCodec).
|
|
51
|
+
*
|
|
52
|
+
* **Prisma casting caveat:** Prisma's `$extends` result component can add
|
|
53
|
+
* typed computed accessors to model instances, but cannot retroactively
|
|
54
|
+
* re-type an existing schema field at the Prisma Client level. The `read`
|
|
55
|
+
* function asserts `Id<Brand>`, but callers will need an explicit
|
|
56
|
+
* `as Id<Brand>` cast at consumption sites where Prisma's generated types
|
|
57
|
+
* are expected.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { idField } from "@smonn/ids/prisma";
|
|
62
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
63
|
+
*
|
|
64
|
+
* const usr = createTimestampId("usr");
|
|
65
|
+
* const userIdField = idField(usr);
|
|
66
|
+
*
|
|
67
|
+
* const xprisma = prisma.$extends({
|
|
68
|
+
* result: {
|
|
69
|
+
* user: {
|
|
70
|
+
* id: {
|
|
71
|
+
* needs: { id: true },
|
|
72
|
+
* compute(user) {
|
|
73
|
+
* // Cast required: Prisma cannot brand the generated type at schema level
|
|
74
|
+
* return userIdField.read(user.id) as Id<"usr">;
|
|
75
|
+
* },
|
|
76
|
+
* },
|
|
77
|
+
* },
|
|
78
|
+
* },
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
declare function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand>;
|
|
83
|
+
//#endregion
|
|
84
|
+
export { IdColumnCodec, IdTransform, IdsError, type IdsErrorCode, idField, isIdsError };
|
|
85
|
+
//# sourceMappingURL=prisma.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/prisma.ts"],"mappings":";;;;AAiBA;;;;;;;;;;AACyC;AADzC,KAAY,aAAA;EACV,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;;;;;;KAY7B,WAAA;;;;;;;AAgBM;EARhB,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;EA+CX;;;;;;;EAvCd,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;AAAA;;;;;;;AAuCsE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAxE,OAAA,uBAA8B,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,WAAA,CAAY,KAAA"}
|
package/dist/prisma.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
+
//#region src/prisma.ts
|
|
3
|
+
/**
|
|
4
|
+
* Creates a read/write transform pair for use with Prisma's `$extends` extension model.
|
|
5
|
+
*
|
|
6
|
+
* Works with any codec variant exposing `safeParse` (TimestampCodec,
|
|
7
|
+
* OpaqueTimestampCodec, ReverseTimestampCodec, WrappedKeyCodec).
|
|
8
|
+
*
|
|
9
|
+
* **Prisma casting caveat:** Prisma's `$extends` result component can add
|
|
10
|
+
* typed computed accessors to model instances, but cannot retroactively
|
|
11
|
+
* re-type an existing schema field at the Prisma Client level. The `read`
|
|
12
|
+
* function asserts `Id<Brand>`, but callers will need an explicit
|
|
13
|
+
* `as Id<Brand>` cast at consumption sites where Prisma's generated types
|
|
14
|
+
* are expected.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { idField } from "@smonn/ids/prisma";
|
|
19
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
20
|
+
*
|
|
21
|
+
* const usr = createTimestampId("usr");
|
|
22
|
+
* const userIdField = idField(usr);
|
|
23
|
+
*
|
|
24
|
+
* const xprisma = prisma.$extends({
|
|
25
|
+
* result: {
|
|
26
|
+
* user: {
|
|
27
|
+
* id: {
|
|
28
|
+
* needs: { id: true },
|
|
29
|
+
* compute(user) {
|
|
30
|
+
* // Cast required: Prisma cannot brand the generated type at schema level
|
|
31
|
+
* return userIdField.read(user.id) as Id<"usr">;
|
|
32
|
+
* },
|
|
33
|
+
* },
|
|
34
|
+
* },
|
|
35
|
+
* },
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
function idField(codec) {
|
|
40
|
+
return {
|
|
41
|
+
read(value) {
|
|
42
|
+
const result = codec.safeParse(value);
|
|
43
|
+
if (!result.ok) throw new IdsError("invalid_id", `invalid ID from database: ${result.error}`, { cause: result.error });
|
|
44
|
+
return result.id;
|
|
45
|
+
},
|
|
46
|
+
write(value) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { IdsError, idField, isIdsError };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=prisma.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma.mjs","names":[],"sources":["../src/prisma.ts"],"sourcesContent":["import { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport type { Id, ParseResult } from \"./types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\n\n/**\n * Minimum codec interface required by the Prisma adapter.\n *\n * Any codec variant satisfies this type — TimestampCodec, OpaqueTimestampCodec,\n * ReverseTimestampCodec, and WrappedKeyCodec all expose `safeParse`. The adapter\n * never calls key-dependent methods.\n *\n * Intentionally the same structural shape as the Drizzle adapter's IdColumnCodec.\n * Do NOT import IdColumnCodec from `@smonn/ids/drizzle` — that would create\n * cross-adapter coupling.\n */\nexport type IdColumnCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\n\n/**\n * Read/write transform pair for integrating `Id<Brand>` with Prisma extensions.\n *\n * **Prisma casting caveat:** Prisma cannot fully brand a generated model field\n * type at the schema level. The `read` function asserts `Id<Brand>` at the\n * TypeScript level, but Prisma's generated types for the model field will not\n * reflect this branding. Callers consuming the validated value from a Prisma\n * result component may need an explicit `as Id<Brand>` cast at the call site.\n */\nexport type IdTransform<Brand extends string> = {\n /**\n * Read transform: validates the raw database value via `safeParse` and returns\n * `Id<Brand>`. Throws if the value is missing, malformed, or belongs to a\n * different brand.\n *\n * Use in a Prisma `$extends` result component's `compute` function.\n */\n read(value: unknown): Id<Brand>;\n /**\n * Write transform: passes `Id<Brand>` through as its canonical string form.\n * `Id<Brand>` is already the canonical string, so this is an identity function\n * at runtime.\n *\n * Use in a Prisma `$extends` query component or explicit `data` mapping.\n */\n write(value: Id<Brand>): string;\n};\n\n/**\n * Creates a read/write transform pair for use with Prisma's `$extends` extension model.\n *\n * Works with any codec variant exposing `safeParse` (TimestampCodec,\n * OpaqueTimestampCodec, ReverseTimestampCodec, WrappedKeyCodec).\n *\n * **Prisma casting caveat:** Prisma's `$extends` result component can add\n * typed computed accessors to model instances, but cannot retroactively\n * re-type an existing schema field at the Prisma Client level. The `read`\n * function asserts `Id<Brand>`, but callers will need an explicit\n * `as Id<Brand>` cast at consumption sites where Prisma's generated types\n * are expected.\n *\n * @example\n * ```ts\n * import { idField } from \"@smonn/ids/prisma\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const userIdField = idField(usr);\n *\n * const xprisma = prisma.$extends({\n * result: {\n * user: {\n * id: {\n * needs: { id: true },\n * compute(user) {\n * // Cast required: Prisma cannot brand the generated type at schema level\n * return userIdField.read(user.id) as Id<\"usr\">;\n * },\n * },\n * },\n * },\n * });\n * ```\n */\nexport function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand> {\n return {\n read(value: unknown): Id<Brand> {\n const result = codec.safeParse(value);\n if (!result.ok) {\n throw new IdsError(\"invalid_id\", `invalid ID from database: ${result.error}`, {\n cause: result.error,\n });\n }\n return result.id;\n },\n write(value: Id<Brand>): string {\n return value;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqFA,SAAgB,QAA8B,OAAiD;CAC7F,OAAO;EACL,KAAK,OAA2B;GAC9B,MAAM,SAAS,MAAM,UAAU,KAAK;GACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,SAAS,cAAc,6BAA6B,OAAO,SAAS,EAC5E,OAAO,OAAO,MAChB,CAAC;GAEH,OAAO,OAAO;EAChB;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;CACF;AACF"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { a as toWireId, i as payloadBytesFromId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-DH-UO4UR.mjs";
|
|
2
|
+
import { r as writeTimestamp } from "./timestamp-bytes-BBY7JI33.mjs";
|
|
3
|
+
//#region src/layouts/reverse-timestamp.ts
|
|
4
|
+
const randomByteLength = 10;
|
|
5
|
+
/** Writes inverted timestamp bytes, then fills random portion. */
|
|
6
|
+
function buildReversePayload(ms, rng, buffer, randomView) {
|
|
7
|
+
writeTimestamp(ms, buffer);
|
|
8
|
+
for (let i = 0; i < 6; i++) buffer[i] = ~buffer[i] & 255;
|
|
9
|
+
rng(randomView);
|
|
10
|
+
}
|
|
11
|
+
/** Writes inverted timestamp bytes, then fills random portion with a sentinel. */
|
|
12
|
+
function buildReverseSentinelPayload(ms, fill, buffer, randomView) {
|
|
13
|
+
writeTimestamp(ms, buffer);
|
|
14
|
+
for (let i = 0; i < 6; i++) buffer[i] = ~buffer[i] & 255;
|
|
15
|
+
randomView.fill(fill);
|
|
16
|
+
}
|
|
17
|
+
/** Decodes the original timestamp by inverting the first 6 payload bytes. */
|
|
18
|
+
function extractReverseTimestampFromId(prefix, id) {
|
|
19
|
+
const bytes = payloadBytesFromId(prefix, id);
|
|
20
|
+
let ms = 0;
|
|
21
|
+
for (let i = 0; i < 6; i++) ms = ms * 256 + (~bytes[i] & 255);
|
|
22
|
+
return new Date(ms);
|
|
23
|
+
}
|
|
24
|
+
/** Layout ops binder for the Reverse Timestamp variant. */
|
|
25
|
+
function createReverseTimestampLayoutOps(prefix, rng) {
|
|
26
|
+
const buffer = new Uint8Array(16);
|
|
27
|
+
const randomView = new Uint8Array(buffer.buffer, 6, randomByteLength);
|
|
28
|
+
return {
|
|
29
|
+
generateAt: (ms) => {
|
|
30
|
+
buildReversePayload(ms, rng, buffer, randomView);
|
|
31
|
+
return toWireId(prefix, buffer);
|
|
32
|
+
},
|
|
33
|
+
extractTimestamp: (id) => extractReverseTimestampFromId(prefix, id),
|
|
34
|
+
minIdForTime: (ms) => {
|
|
35
|
+
buildReverseSentinelPayload(ms, 0, buffer, randomView);
|
|
36
|
+
return toWireId(prefix, buffer);
|
|
37
|
+
},
|
|
38
|
+
maxIdForTime: (ms) => {
|
|
39
|
+
buildReverseSentinelPayload(ms, 255, buffer, randomView);
|
|
40
|
+
return toWireId(prefix, buffer);
|
|
41
|
+
},
|
|
42
|
+
exampleWireId: (ms) => {
|
|
43
|
+
buildReversePayload(ms, rng, buffer, randomView);
|
|
44
|
+
return toWireId(prefix, buffer);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/reverse.ts
|
|
50
|
+
function defaultRng(target) {
|
|
51
|
+
crypto.getRandomValues(target);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates a Reverse Timestamp codec for `brand` (three lowercase a–z characters).
|
|
55
|
+
*
|
|
56
|
+
* IDs sort newest-first: the 48-bit timestamp field is bitwise-inverted before encoding,
|
|
57
|
+
* so lexicographic ID order equals descending creation-time order. `extractTimestamp`
|
|
58
|
+
* inverts back to recover the original millisecond.
|
|
59
|
+
*
|
|
60
|
+
* @param brand - Entity type brand validated once at construction.
|
|
61
|
+
* @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.
|
|
62
|
+
*/
|
|
63
|
+
function createReverseTimestampId(brand, opts = {}) {
|
|
64
|
+
validateBrand(brand);
|
|
65
|
+
registerBrand(brand, opts.allowDuplicateBrand);
|
|
66
|
+
const now = opts.now ?? Date.now;
|
|
67
|
+
const rng = opts.rng ?? defaultRng;
|
|
68
|
+
const prefix = `${brand}_`;
|
|
69
|
+
const wire = wireMethods(prefix);
|
|
70
|
+
const layout = createReverseTimestampLayoutOps(prefix, rng);
|
|
71
|
+
return {
|
|
72
|
+
generate: () => layout.generateAt(now()),
|
|
73
|
+
generateAt: (date) => layout.generateAt(date.getTime()),
|
|
74
|
+
is: wire.is,
|
|
75
|
+
parse: wire.parse,
|
|
76
|
+
safeParse: wire.safeParse,
|
|
77
|
+
extractTimestamp: layout.extractTimestamp,
|
|
78
|
+
minIdForTime: (date) => layout.minIdForTime(date.getTime()),
|
|
79
|
+
maxIdForTime: (date) => layout.maxIdForTime(date.getTime()),
|
|
80
|
+
toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(now())),
|
|
81
|
+
"~standard": wire["~standard"]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { createReverseTimestampId as t };
|
|
86
|
+
|
|
87
|
+
//# sourceMappingURL=reverse-BgFU6JHw.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reverse-BgFU6JHw.mjs","names":[],"sources":["../src/layouts/reverse-timestamp.ts","../src/reverse.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { payloadBytesFromId, toWireId } from \"../wire/envelope.js\";\nimport { payloadByteLength } from \"../wire/invariants.js\";\nimport { timestampByteLength, writeTimestamp } from \"../wire/timestamp-bytes.js\";\n\nconst randomByteLength: number = payloadByteLength - timestampByteLength;\n\n/** Writes inverted timestamp bytes, then fills random portion. */\nfunction buildReversePayload(\n ms: number,\n rng: (target: Uint8Array) => void,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n for (let i = 0; i < timestampByteLength; i++) {\n buffer[i] = ~buffer[i]! & 0xff;\n }\n rng(randomView);\n}\n\n/** Writes inverted timestamp bytes, then fills random portion with a sentinel. */\nfunction buildReverseSentinelPayload(\n ms: number,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n for (let i = 0; i < timestampByteLength; i++) {\n buffer[i] = ~buffer[i]! & 0xff;\n }\n randomView.fill(fill);\n}\n\n/** Decodes the original timestamp by inverting the first 6 payload bytes. */\nfunction extractReverseTimestampFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n id: Id<Brand>,\n): Date {\n const bytes = payloadBytesFromId(prefix, id);\n let ms = 0;\n for (let i = 0; i < timestampByteLength; i++) {\n ms = ms * 256 + (~bytes[i]! & 0xff);\n }\n return new Date(ms);\n}\n\n/** Layout ops binder for the Reverse Timestamp variant. */\nexport function createReverseTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n rng: (target: Uint8Array) => void,\n) {\n const buffer = new Uint8Array(payloadByteLength);\n const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);\n\n return {\n generateAt: (ms: number): Id<Brand> => {\n buildReversePayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n extractTimestamp: (id: Id<Brand>): Date => extractReverseTimestampFromId(prefix, id),\n minIdForTime: (ms: number): Id<Brand> => {\n buildReverseSentinelPayload(ms, 0x00, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n maxIdForTime: (ms: number): Id<Brand> => {\n buildReverseSentinelPayload(ms, 0xff, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n exampleWireId: (ms: number): Id<Brand> => {\n buildReversePayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n };\n}\n","import { validateBrand } from \"./brand.js\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport { createReverseTimestampLayoutOps } from \"./layouts/reverse-timestamp.js\";\nimport { registerBrand } from \"./registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\n\n/**\n * Configuration options for a Reverse Timestamp codec instance.\n */\nexport type ReverseTimestampOptions = {\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * A brand-scoped codec for generating and validating Reverse Timestamp IDs.\n *\n * Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a\n * 16-byte payload (6-byte bitwise-inverted ms timestamp + 10 random bytes). IDs sort\n * by creation time in **descending** (newest-first) order.\n *\n * Range queries across a time interval [t_old, t_new] should scan from\n * `minIdForTime(t_new)` to `maxIdForTime(t_old)` — the reversed sort order means\n * newer timestamps produce lexicographically smaller IDs.\n *\n * Constructed via `createReverseTimestampId(brand)` from `@smonn/ids/reverse`.\n */\nexport type ReverseTimestampCodec<Brand extends string> = {\n /** Produces a new canonical ID using the codec's `now` and `rng`. */\n generate(): Id<Brand>;\n /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */\n generateAt(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>` by inverting the timestamp bytes.\n * Trusts the type — use `safeParse()` at boundaries first.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /**\n * Lexicographically smallest ID for any ID generated at `date` (random portion `0x00`).\n * Because timestamps are inverted, a newer `date` yields a lexicographically smaller result —\n * use `minIdForTime(t_new)` as the lower bound when scanning [t_old, t_new].\n * Throws on invalid dates.\n */\n minIdForTime(date: Date): Id<Brand>;\n /**\n * Lexicographically largest ID for any ID generated at `date` (random portion `0xff`).\n * Because timestamps are inverted, an older `date` yields a lexicographically larger result —\n * use `maxIdForTime(t_old)` as the upper bound when scanning [t_old, t_new].\n * Throws on invalid dates.\n */\n maxIdForTime(date: Date): Id<Brand>;\n /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\nfunction defaultRng(target: Uint8Array): void {\n crypto.getRandomValues(target as Uint8Array<ArrayBuffer>);\n}\n\n/**\n * Creates a Reverse Timestamp codec for `brand` (three lowercase a–z characters).\n *\n * IDs sort newest-first: the 48-bit timestamp field is bitwise-inverted before encoding,\n * so lexicographic ID order equals descending creation-time order. `extractTimestamp`\n * inverts back to recover the original millisecond.\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createReverseTimestampId<Brand extends string>(\n brand: Brand,\n opts: ReverseTimestampOptions = {},\n): ReverseTimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const now = opts.now ?? Date.now;\n const rng = opts.rng ?? defaultRng;\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createReverseTimestampLayoutOps(prefix, rng);\n\n return {\n generate: () => layout.generateAt(now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n minIdForTime: (date: Date) => layout.minIdForTime(date.getTime()),\n maxIdForTime: (date: Date) => layout.maxIdForTime(date.getTime()),\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(now())),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":";;;AAKA,MAAM,mBAAA;;AAGN,SAAS,oBACP,IACA,KACA,QACA,YACM;CACN,eAAe,IAAI,MAAM;CACzB,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KACvC,OAAO,KAAK,CAAC,OAAO,KAAM;CAE5B,IAAI,UAAU;AAChB;;AAGA,SAAS,4BACP,IACA,MACA,QACA,YACM;CACN,eAAe,IAAI,MAAM;CACzB,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KACvC,OAAO,KAAK,CAAC,OAAO,KAAM;CAE5B,WAAW,KAAK,IAAI;AACtB;;AAGA,SAAS,8BACP,QACA,IACM;CACN,MAAM,QAAQ,mBAAmB,QAAQ,EAAE;CAC3C,IAAI,KAAK;CACT,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KACvC,KAAK,KAAK,OAAO,CAAC,MAAM,KAAM;CAEhC,OAAO,IAAI,KAAK,EAAE;AACpB;;AAGA,SAAgB,gCACd,QACA,KACA;CACA,MAAM,SAAS,IAAI,WAAA,EAA4B;CAC/C,MAAM,aAAa,IAAI,WAAW,OAAO,QAAA,GAA6B,gBAAgB;CAEtF,OAAO;EACL,aAAa,OAA0B;GACrC,oBAAoB,IAAI,KAAK,QAAQ,UAAU;GAC/C,OAAO,SAAS,QAAQ,MAAM;EAChC;EACA,mBAAmB,OAAwB,8BAA8B,QAAQ,EAAE;EACnF,eAAe,OAA0B;GACvC,4BAA4B,IAAI,GAAM,QAAQ,UAAU;GACxD,OAAO,SAAS,QAAQ,MAAM;EAChC;EACA,eAAe,OAA0B;GACvC,4BAA4B,IAAI,KAAM,QAAQ,UAAU;GACxD,OAAO,SAAS,QAAQ,MAAM;EAChC;EACA,gBAAgB,OAA0B;GACxC,oBAAoB,IAAI,KAAK,QAAQ,UAAU;GAC/C,OAAO,SAAS,QAAQ,MAAM;EAChC;CACF;AACF;;;ACGA,SAAS,WAAW,QAA0B;CAC5C,OAAO,gBAAgB,MAAiC;AAC1D;;;;;;;;;;;AAYA,SAAgB,yBACd,OACA,OAAgC,CAAC,GACH;CAC9B,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAE7C,MAAM,MAAM,KAAK,OAAO,KAAK;CAC7B,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,SAAwB,GAAG,MAAM;CACvC,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,SAAS,gCAAgC,QAAQ,GAAG;CAE1D,OAAO;EACL,gBAAgB,OAAO,WAAW,IAAI,CAAC;EACvC,aAAa,SAAe,OAAO,WAAW,KAAK,QAAQ,CAAC;EAC5D,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,WAAW,KAAK;EAChB,kBAAkB,OAAO;EACzB,eAAe,SAAe,OAAO,aAAa,KAAK,QAAQ,CAAC;EAChE,eAAe,SAAe,OAAO,aAAa,KAAK,QAAQ,CAAC;EAChE,oBAAoB,KAAK,aAAa,OAAO,OAAO,cAAc,IAAI,CAAC,CAAC;EACxE,aAAa,KAAK;CACpB;AACF"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
|
|
2
|
+
import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, t as Id } from "./types-g7CiQDyE.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/reverse.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Configuration options for a Reverse Timestamp codec instance.
|
|
7
|
+
*/
|
|
8
|
+
type ReverseTimestampOptions = {
|
|
9
|
+
/** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */now?: () => number; /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */
|
|
10
|
+
rng?: (target: Uint8Array) => void; /** If true, silences the duplicate-brand warning in non-production environments. */
|
|
11
|
+
allowDuplicateBrand?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A brand-scoped codec for generating and validating Reverse Timestamp IDs.
|
|
15
|
+
*
|
|
16
|
+
* Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a
|
|
17
|
+
* 16-byte payload (6-byte bitwise-inverted ms timestamp + 10 random bytes). IDs sort
|
|
18
|
+
* by creation time in **descending** (newest-first) order.
|
|
19
|
+
*
|
|
20
|
+
* Range queries across a time interval [t_old, t_new] should scan from
|
|
21
|
+
* `minIdForTime(t_new)` to `maxIdForTime(t_old)` — the reversed sort order means
|
|
22
|
+
* newer timestamps produce lexicographically smaller IDs.
|
|
23
|
+
*
|
|
24
|
+
* Constructed via `createReverseTimestampId(brand)` from `@smonn/ids/reverse`.
|
|
25
|
+
*/
|
|
26
|
+
type ReverseTimestampCodec<Brand extends string> = {
|
|
27
|
+
/** Produces a new canonical ID using the codec's `now` and `rng`. */generate(): Id<Brand>; /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */
|
|
28
|
+
generateAt(date: Date): Id<Brand>;
|
|
29
|
+
/**
|
|
30
|
+
* Strict type guard: `true` only for already-canonical strings for this brand.
|
|
31
|
+
* For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.
|
|
32
|
+
*/
|
|
33
|
+
is(value: unknown): value is Id<Brand>;
|
|
34
|
+
/**
|
|
35
|
+
* Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.
|
|
36
|
+
*/
|
|
37
|
+
parse(value: unknown): Id<Brand>;
|
|
38
|
+
/**
|
|
39
|
+
* Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.
|
|
40
|
+
*/
|
|
41
|
+
safeParse(value: unknown): ParseResult<Brand>;
|
|
42
|
+
/**
|
|
43
|
+
* Decodes the creation `Date` from an `Id<Brand>` by inverting the timestamp bytes.
|
|
44
|
+
* Trusts the type — use `safeParse()` at boundaries first.
|
|
45
|
+
*/
|
|
46
|
+
extractTimestamp(id: Id<Brand>): Date;
|
|
47
|
+
/**
|
|
48
|
+
* Lexicographically smallest ID for any ID generated at `date` (random portion `0x00`).
|
|
49
|
+
* Because timestamps are inverted, a newer `date` yields a lexicographically smaller result —
|
|
50
|
+
* use `minIdForTime(t_new)` as the lower bound when scanning [t_old, t_new].
|
|
51
|
+
* Throws on invalid dates.
|
|
52
|
+
*/
|
|
53
|
+
minIdForTime(date: Date): Id<Brand>;
|
|
54
|
+
/**
|
|
55
|
+
* Lexicographically largest ID for any ID generated at `date` (random portion `0xff`).
|
|
56
|
+
* Because timestamps are inverted, an older `date` yields a lexicographically larger result —
|
|
57
|
+
* use `maxIdForTime(t_old)` as the upper bound when scanning [t_old, t_new].
|
|
58
|
+
* Throws on invalid dates.
|
|
59
|
+
*/
|
|
60
|
+
maxIdForTime(date: Date): Id<Brand>; /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */
|
|
61
|
+
toJsonSchema(): JsonSchema; /** Standard Schema validate entry point. */
|
|
62
|
+
readonly "~standard": StandardSchemaProps<Brand>;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Creates a Reverse Timestamp codec for `brand` (three lowercase a–z characters).
|
|
66
|
+
*
|
|
67
|
+
* IDs sort newest-first: the 48-bit timestamp field is bitwise-inverted before encoding,
|
|
68
|
+
* so lexicographic ID order equals descending creation-time order. `extractTimestamp`
|
|
69
|
+
* inverts back to recover the original millisecond.
|
|
70
|
+
*
|
|
71
|
+
* @param brand - Entity type brand validated once at construction.
|
|
72
|
+
* @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.
|
|
73
|
+
*/
|
|
74
|
+
declare function createReverseTimestampId<Brand extends string>(brand: Brand, opts?: ReverseTimestampOptions): ReverseTimestampCodec<Brand>;
|
|
75
|
+
//#endregion
|
|
76
|
+
export { IdsError, type IdsErrorCode, ReverseTimestampCodec, ReverseTimestampOptions, createReverseTimestampId, isIdsError };
|
|
77
|
+
//# sourceMappingURL=reverse.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reverse.d.mts","names":[],"sources":["../src/reverse.ts"],"mappings":";;;;AAaA;;;AAAA,KAAY,uBAAA;+EAEV,GAAA;EAEA,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;AAgBF;;;;;;;;;;;;;AAAA,KAAY,qBAAA;uEAEV,QAAA,IAAY,EAAA,CAAG,KAAA;EAEf,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;EAK3B,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;;;EAIhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EA2BJ;;;EAvBtB,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;;EAKvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;;;;;;;EAOjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;;;EAO7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;;;;;iBAiB5B,wBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,GAAM,uBAAA,GACL,qBAAA,CAAsB,KAAA"}
|
package/dist/reverse.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/signing-key.d.ts
|
|
4
|
+
/** Wire encoding for signing key raw key bytes (not Crockford base32). */
|
|
5
|
+
type SigningKeyFormat = "hex" | "base64url";
|
|
6
|
+
declare const signingKeyBrand: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* Opaque imported handle for one operator signing key.
|
|
9
|
+
*
|
|
10
|
+
* Holds a single HMAC-SHA-256 key derived via HKDF under the domain-separation
|
|
11
|
+
* label `ids/signed-timestamp/hmac`. The underlying `CryptoKey` is held
|
|
12
|
+
* internally and never exposed to callers. Obtain handles via
|
|
13
|
+
* {@link importSigningKey} and pass them to `createSignedTimestampId` as the
|
|
14
|
+
* `keys` signing keyring.
|
|
15
|
+
*
|
|
16
|
+
* Distinct from both the **Opaque key** and the **Wrapping key** — the same
|
|
17
|
+
* raw key material must not silently serve multiple codecs without an explicit import.
|
|
18
|
+
*/
|
|
19
|
+
type SigningKey = {
|
|
20
|
+
readonly [signingKeyBrand]: "SigningKey";
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Import raw operator key material into a {@link SigningKey} handle.
|
|
24
|
+
*
|
|
25
|
+
* Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label
|
|
26
|
+
* `ids/signed-timestamp/hmac`. Accepts 16, 24, or 32 bytes. To store or
|
|
27
|
+
* transport key material, use {@link encodeSigningKey} / {@link decodeSigningKey}
|
|
28
|
+
* (`"hex"` or `"base64url"` — not Crockford base32).
|
|
29
|
+
*
|
|
30
|
+
* @param bytes - 16, 24, or 32 raw key bytes.
|
|
31
|
+
* @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
|
|
32
|
+
*/
|
|
33
|
+
declare function importSigningKey(bytes: Uint8Array): Promise<SigningKey>;
|
|
34
|
+
/**
|
|
35
|
+
* Encode raw signing operator key material for storage in env vars or secret managers.
|
|
36
|
+
*
|
|
37
|
+
* Supports `"hex"` (lowercase) and `"base64url"`. Output round-trips through
|
|
38
|
+
* {@link decodeSigningKey} back to the original bytes.
|
|
39
|
+
*
|
|
40
|
+
* @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
|
|
41
|
+
* @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
|
|
42
|
+
*/
|
|
43
|
+
declare function encodeSigningKey(bytes: Uint8Array, format: SigningKeyFormat): string;
|
|
44
|
+
/**
|
|
45
|
+
* Decode key material emitted by {@link encodeSigningKey} back to raw bytes.
|
|
46
|
+
*
|
|
47
|
+
* The result can be passed directly to {@link importSigningKey}.
|
|
48
|
+
*
|
|
49
|
+
* @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
|
|
50
|
+
* @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.
|
|
51
|
+
* @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.
|
|
52
|
+
*/
|
|
53
|
+
declare function decodeSigningKey(encoded: string, format: SigningKeyFormat): Uint8Array;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { IdsError, type IdsErrorCode, type SigningKey, type SigningKeyFormat, decodeSigningKey, encodeSigningKey, importSigningKey, isIdsError };
|
|
56
|
+
//# sourceMappingURL=signed.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signed.d.mts","names":[],"sources":["../src/signing-key.ts"],"mappings":";;;;KAIY,gBAAA;AAAA,cAME,eAAA;AANd;;;;AAAY;AAA2B;;;;AAMzB;AAcd;;AApBA,KAoBY,UAAA;EAAA,UACA,eAAA;AAAA;AAqBZ;;;;;;;;;;;AAAA,iBAAsB,gBAAA,CAAiB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,UAAA;;AAAA;AAiBnE;;;;;;;iBAAgB,gBAAA,CAAiB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,gBAAA;;AAAA;AAgB5D;;;;;;;iBAAgB,gBAAA,CAAiB,OAAA,UAAiB,MAAA,EAAQ,gBAAA,GAAmB,UAAA"}
|
package/dist/signed.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
|
|
2
|
+
import { i as encodeHex, n as decodeHex, r as encodeBase64Url, t as decodeBase64Url } from "./bytes-lhzKVaBV.mjs";
|
|
3
|
+
//#region src/signing-key.ts
|
|
4
|
+
const validKeyByteLengths = new Set([
|
|
5
|
+
16,
|
|
6
|
+
24,
|
|
7
|
+
32
|
|
8
|
+
]);
|
|
9
|
+
const hmacInfo = new TextEncoder().encode("ids/signed-timestamp/hmac");
|
|
10
|
+
const internals = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
/**
|
|
12
|
+
* Import raw operator key material into a {@link SigningKey} handle.
|
|
13
|
+
*
|
|
14
|
+
* Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label
|
|
15
|
+
* `ids/signed-timestamp/hmac`. Accepts 16, 24, or 32 bytes. To store or
|
|
16
|
+
* transport key material, use {@link encodeSigningKey} / {@link decodeSigningKey}
|
|
17
|
+
* (`"hex"` or `"base64url"` — not Crockford base32).
|
|
18
|
+
*
|
|
19
|
+
* @param bytes - 16, 24, or 32 raw key bytes.
|
|
20
|
+
* @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
|
|
21
|
+
*/
|
|
22
|
+
async function importSigningKey(bytes) {
|
|
23
|
+
assertValidKeyByteLength(bytes.length);
|
|
24
|
+
const hmacKey = await deriveHmacKey(bytes);
|
|
25
|
+
const key = Object.freeze({});
|
|
26
|
+
internals.set(key, {
|
|
27
|
+
rawBytes: bytes.slice(),
|
|
28
|
+
hmacKey
|
|
29
|
+
});
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Encode raw signing operator key material for storage in env vars or secret managers.
|
|
34
|
+
*
|
|
35
|
+
* Supports `"hex"` (lowercase) and `"base64url"`. Output round-trips through
|
|
36
|
+
* {@link decodeSigningKey} back to the original bytes.
|
|
37
|
+
*
|
|
38
|
+
* @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
|
|
39
|
+
* @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
|
|
40
|
+
*/
|
|
41
|
+
function encodeSigningKey(bytes, format) {
|
|
42
|
+
assertSigningKeyFormat(format);
|
|
43
|
+
assertValidKeyByteLength(bytes.length);
|
|
44
|
+
if (format === "hex") return encodeHex(bytes);
|
|
45
|
+
return encodeBase64Url(bytes);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decode key material emitted by {@link encodeSigningKey} back to raw bytes.
|
|
49
|
+
*
|
|
50
|
+
* The result can be passed directly to {@link importSigningKey}.
|
|
51
|
+
*
|
|
52
|
+
* @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
|
|
53
|
+
* @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.
|
|
54
|
+
* @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.
|
|
55
|
+
*/
|
|
56
|
+
function decodeSigningKey(encoded, format) {
|
|
57
|
+
assertSigningKeyFormat(format);
|
|
58
|
+
let bytes;
|
|
59
|
+
if (format === "hex") {
|
|
60
|
+
if (encoded.length === 0 || encoded.length % 2 !== 0) throw new IdsError("invalid_key_encoding", "invalid hex key: length must be a positive even number of characters");
|
|
61
|
+
if (!/^[0-9a-fA-F]+$/.test(encoded)) throw new IdsError("invalid_key_encoding", "invalid hex key: expected [0-9a-fA-F] only");
|
|
62
|
+
bytes = decodeHex(encoded);
|
|
63
|
+
} else try {
|
|
64
|
+
bytes = decodeBase64Url(encoded);
|
|
65
|
+
} catch {
|
|
66
|
+
throw new IdsError("invalid_key_encoding", "invalid base64url key");
|
|
67
|
+
}
|
|
68
|
+
assertValidKeyByteLength(bytes.length);
|
|
69
|
+
return bytes;
|
|
70
|
+
}
|
|
71
|
+
async function deriveHmacKey(bytes) {
|
|
72
|
+
const base = await crypto.subtle.importKey("raw", bytes, "HKDF", false, ["deriveKey"]);
|
|
73
|
+
return crypto.subtle.deriveKey({
|
|
74
|
+
name: "HKDF",
|
|
75
|
+
hash: "SHA-256",
|
|
76
|
+
salt: new Uint8Array(),
|
|
77
|
+
info: hmacInfo
|
|
78
|
+
}, base, {
|
|
79
|
+
name: "HMAC",
|
|
80
|
+
hash: "SHA-256",
|
|
81
|
+
length: 256
|
|
82
|
+
}, false, ["sign", "verify"]);
|
|
83
|
+
}
|
|
84
|
+
function assertValidKeyByteLength(byteLength) {
|
|
85
|
+
if (!validKeyByteLengths.has(byteLength)) throw new IdsError("invalid_key_length", `invalid signing key length: expected 16, 24, or 32 bytes, got ${byteLength}`);
|
|
86
|
+
}
|
|
87
|
+
function assertSigningKeyFormat(format) {
|
|
88
|
+
if (format !== "hex" && format !== "base64url") throw new IdsError("invalid_key_format", `invalid signing key format: expected hex or base64url, got '${formatForError(format)}'`);
|
|
89
|
+
}
|
|
90
|
+
function formatForError(value) {
|
|
91
|
+
try {
|
|
92
|
+
return String(value);
|
|
93
|
+
} catch {
|
|
94
|
+
return "[unprintable]";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { IdsError, decodeSigningKey, encodeSigningKey, importSigningKey, isIdsError };
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=signed.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signed.mjs","names":[],"sources":["../src/signing-key.ts"],"sourcesContent":["import { decodeBase64Url, decodeHex, encodeBase64Url, encodeHex } from \"./bytes.js\";\nimport { IdsError } from \"./error.js\";\n\n/** Wire encoding for signing key raw key bytes (not Crockford base32). */\nexport type SigningKeyFormat = \"hex\" | \"base64url\";\n\nconst validKeyByteLengths = new Set([16, 24, 32]);\n\nconst hmacInfo = new TextEncoder().encode(\"ids/signed-timestamp/hmac\");\n\ndeclare const signingKeyBrand: unique symbol;\n\n/**\n * Opaque imported handle for one operator signing key.\n *\n * Holds a single HMAC-SHA-256 key derived via HKDF under the domain-separation\n * label `ids/signed-timestamp/hmac`. The underlying `CryptoKey` is held\n * internally and never exposed to callers. Obtain handles via\n * {@link importSigningKey} and pass them to `createSignedTimestampId` as the\n * `keys` signing keyring.\n *\n * Distinct from both the **Opaque key** and the **Wrapping key** — the same\n * raw key material must not silently serve multiple codecs without an explicit import.\n */\nexport type SigningKey = {\n readonly [signingKeyBrand]: \"SigningKey\";\n};\n\ntype SigningKeyInternals = {\n rawBytes: Uint8Array;\n hmacKey: CryptoKey;\n};\n\nconst internals = new WeakMap<SigningKey, SigningKeyInternals>();\n\n/**\n * Import raw operator key material into a {@link SigningKey} handle.\n *\n * Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label\n * `ids/signed-timestamp/hmac`. Accepts 16, 24, or 32 bytes. To store or\n * transport key material, use {@link encodeSigningKey} / {@link decodeSigningKey}\n * (`\"hex\"` or `\"base64url\"` — not Crockford base32).\n *\n * @param bytes - 16, 24, or 32 raw key bytes.\n * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.\n */\nexport async function importSigningKey(bytes: Uint8Array): Promise<SigningKey> {\n assertValidKeyByteLength(bytes.length);\n const hmacKey = await deriveHmacKey(bytes);\n const key = Object.freeze({}) as SigningKey;\n internals.set(key, { rawBytes: bytes.slice(), hmacKey });\n return key;\n}\n\n/**\n * Encode raw signing operator key material for storage in env vars or secret managers.\n *\n * Supports `\"hex\"` (lowercase) and `\"base64url\"`. Output round-trips through\n * {@link decodeSigningKey} back to the original bytes.\n *\n * @throws {IdsError} `invalid_key_format` if `format` is not `\"hex\"` or `\"base64url\"`.\n * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.\n */\nexport function encodeSigningKey(bytes: Uint8Array, format: SigningKeyFormat): string {\n assertSigningKeyFormat(format);\n assertValidKeyByteLength(bytes.length);\n if (format === \"hex\") return encodeHex(bytes);\n return encodeBase64Url(bytes);\n}\n\n/**\n * Decode key material emitted by {@link encodeSigningKey} back to raw bytes.\n *\n * The result can be passed directly to {@link importSigningKey}.\n *\n * @throws {IdsError} `invalid_key_format` if `format` is not `\"hex\"` or `\"base64url\"`.\n * @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.\n * @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.\n */\nexport function decodeSigningKey(encoded: string, format: SigningKeyFormat): Uint8Array {\n assertSigningKeyFormat(format);\n let bytes: Uint8Array;\n if (format === \"hex\") {\n if (encoded.length === 0 || encoded.length % 2 !== 0) {\n throw new IdsError(\n \"invalid_key_encoding\",\n \"invalid hex key: length must be a positive even number of characters\",\n );\n }\n if (!/^[0-9a-fA-F]+$/.test(encoded)) {\n throw new IdsError(\"invalid_key_encoding\", \"invalid hex key: expected [0-9a-fA-F] only\");\n }\n bytes = decodeHex(encoded);\n } else {\n try {\n bytes = decodeBase64Url(encoded);\n } catch {\n throw new IdsError(\"invalid_key_encoding\", \"invalid base64url key\");\n }\n }\n assertValidKeyByteLength(bytes.length);\n return bytes;\n}\n\n/**\n * Returns true when two handles were imported from the same raw key material.\n *\n * Uses a constant-time comparison so duplicate detection over key material does\n * not leak the position of the first differing byte through a timing side channel.\n */\nexport function signingKeysEqual(a: SigningKey, b: SigningKey): boolean {\n const aBytes = getSigningKeyInternals(a).rawBytes;\n const bBytes = getSigningKeyInternals(b).rawBytes;\n if (aBytes.length !== bBytes.length) return false;\n let diff = 0;\n for (let i = 0; i < aBytes.length; i++) {\n diff |= aBytes[i]! ^ bBytes[i]!;\n }\n return diff === 0;\n}\n\n/**\n * Returns the derived HMAC CryptoKey held inside the handle.\n *\n * Intentional module-internal escape hatch for codec implementations (e.g. `createSignedTimestampId`).\n * Not re-exported from `@smonn/ids/signed`; external callers cannot reach this.\n */\nexport function getSigningKeyHmacKey(key: SigningKey): CryptoKey {\n return getSigningKeyInternals(key).hmacKey;\n}\n\n/**\n * Asserts that a signing keyring is non-empty.\n * @throws {IdsError} `empty_keyring` if the array is empty.\n */\nexport function assertNonEmptySigningKeyring(keys: readonly SigningKey[]): void {\n if (keys.length === 0) {\n throw new IdsError(\"empty_keyring\", \"signing keyring must contain at least one key\");\n }\n}\n\n/**\n * Asserts that no two entries in the signing keyring share the same raw bytes.\n * @throws {IdsError} `duplicate_keyring_entry` if a duplicate is found.\n */\nexport function assertNonDuplicateSigningKeys(keys: readonly SigningKey[]): void {\n for (let i = 0; i < keys.length; i++) {\n for (let j = i + 1; j < keys.length; j++) {\n if (signingKeysEqual(keys[i]!, keys[j]!)) {\n throw new IdsError(\"duplicate_keyring_entry\", \"duplicate signing key in keyring\");\n }\n }\n }\n}\n\nfunction getSigningKeyInternals(key: SigningKey): SigningKeyInternals {\n const keyInternals = internals.get(key);\n if (keyInternals === undefined) {\n throw new Error(\"invalid signing key\");\n }\n return keyInternals;\n}\n\nasync function deriveHmacKey(bytes: Uint8Array): Promise<CryptoKey> {\n const base = await crypto.subtle.importKey(\n \"raw\",\n bytes as Uint8Array<ArrayBuffer>,\n \"HKDF\",\n false,\n [\"deriveKey\"],\n );\n return crypto.subtle.deriveKey(\n { name: \"HKDF\", hash: \"SHA-256\", salt: new Uint8Array(), info: hmacInfo },\n base,\n { name: \"HMAC\", hash: \"SHA-256\", length: 256 },\n false,\n [\"sign\", \"verify\"],\n );\n}\n\nfunction assertValidKeyByteLength(byteLength: number): void {\n if (!validKeyByteLengths.has(byteLength)) {\n throw new IdsError(\n \"invalid_key_length\",\n `invalid signing key length: expected 16, 24, or 32 bytes, got ${byteLength}`,\n );\n }\n}\n\nfunction assertSigningKeyFormat(format: unknown): asserts format is SigningKeyFormat {\n if (format !== \"hex\" && format !== \"base64url\") {\n throw new IdsError(\n \"invalid_key_format\",\n `invalid signing key format: expected hex or base64url, got '${formatForError(format)}'`,\n );\n }\n}\n\nfunction formatForError(value: unknown): string {\n try {\n return String(value);\n } catch {\n return \"[unprintable]\";\n }\n}\n"],"mappings":";;;AAMA,MAAM,sBAAsB,IAAI,IAAI;CAAC;CAAI;CAAI;AAAE,CAAC;AAEhD,MAAM,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,2BAA2B;AAyBrE,MAAM,4BAAY,IAAI,QAAyC;;;;;;;;;;;;AAa/D,eAAsB,iBAAiB,OAAwC;CAC7E,yBAAyB,MAAM,MAAM;CACrC,MAAM,UAAU,MAAM,cAAc,KAAK;CACzC,MAAM,MAAM,OAAO,OAAO,CAAC,CAAC;CAC5B,UAAU,IAAI,KAAK;EAAE,UAAU,MAAM,MAAM;EAAG;CAAQ,CAAC;CACvD,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,iBAAiB,OAAmB,QAAkC;CACpF,uBAAuB,MAAM;CAC7B,yBAAyB,MAAM,MAAM;CACrC,IAAI,WAAW,OAAO,OAAO,UAAU,KAAK;CAC5C,OAAO,gBAAgB,KAAK;AAC9B;;;;;;;;;;AAWA,SAAgB,iBAAiB,SAAiB,QAAsC;CACtF,uBAAuB,MAAM;CAC7B,IAAI;CACJ,IAAI,WAAW,OAAO;EACpB,IAAI,QAAQ,WAAW,KAAK,QAAQ,SAAS,MAAM,GACjD,MAAM,IAAI,SACR,wBACA,sEACF;EAEF,IAAI,CAAC,iBAAiB,KAAK,OAAO,GAChC,MAAM,IAAI,SAAS,wBAAwB,4CAA4C;EAEzF,QAAQ,UAAU,OAAO;CAC3B,OACE,IAAI;EACF,QAAQ,gBAAgB,OAAO;CACjC,QAAQ;EACN,MAAM,IAAI,SAAS,wBAAwB,uBAAuB;CACpE;CAEF,yBAAyB,MAAM,MAAM;CACrC,OAAO;AACT;AA6DA,eAAe,cAAc,OAAuC;CAClE,MAAM,OAAO,MAAM,OAAO,OAAO,UAC/B,OACA,OACA,QACA,OACA,CAAC,WAAW,CACd;CACA,OAAO,OAAO,OAAO,UACnB;EAAE,MAAM;EAAQ,MAAM;EAAW,MAAM,IAAI,WAAW;EAAG,MAAM;CAAS,GACxE,MACA;EAAE,MAAM;EAAQ,MAAM;EAAW,QAAQ;CAAI,GAC7C,OACA,CAAC,QAAQ,QAAQ,CACnB;AACF;AAEA,SAAS,yBAAyB,YAA0B;CAC1D,IAAI,CAAC,oBAAoB,IAAI,UAAU,GACrC,MAAM,IAAI,SACR,sBACA,iEAAiE,YACnE;AAEJ;AAEA,SAAS,uBAAuB,QAAqD;CACnF,IAAI,WAAW,SAAS,WAAW,aACjC,MAAM,IAAI,SACR,sBACA,+DAA+D,eAAe,MAAM,EAAE,EACxF;AAEJ;AAEA,SAAS,eAAe,OAAwB;CAC9C,IAAI;EACF,OAAO,OAAO,KAAK;CACrB,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as toWireId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-
|
|
2
|
-
import { n as readTimestampMsFromBase32Suffix, r as writeTimestamp } from "./timestamp-bytes-
|
|
1
|
+
import { a as toWireId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-DH-UO4UR.mjs";
|
|
2
|
+
import { n as readTimestampMsFromBase32Suffix, r as writeTimestamp } from "./timestamp-bytes-BBY7JI33.mjs";
|
|
3
3
|
//#region src/layouts/timestamp.ts
|
|
4
4
|
const randomByteLength = 10;
|
|
5
5
|
/** Writes a 16-byte timestamp-layout payload into codec-owned scratch. */
|
|
@@ -93,4 +93,4 @@ function createTimestampId(brand, opts = {}) {
|
|
|
93
93
|
//#endregion
|
|
94
94
|
export { createTimestampId as t };
|
|
95
95
|
|
|
96
|
-
//# sourceMappingURL=timestamp-
|
|
96
|
+
//# sourceMappingURL=timestamp-B5_UCzc6.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timestamp-Bgzxx8bE.mjs","names":[],"sources":["../src/layouts/timestamp.ts","../src/timestamp.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { toWireId } from \"../wire/envelope.js\";\nimport { payloadByteLength } from \"../wire/invariants.js\";\nimport {\n readTimestampMsFromBase32Suffix,\n timestampByteLength,\n writeTimestamp,\n} from \"../wire/timestamp-bytes.js\";\n\nconst randomByteLength: number = payloadByteLength - timestampByteLength;\n\n/** Writes a 16-byte timestamp-layout payload into codec-owned scratch. */\nfunction buildPayload(\n ms: number,\n rng: (target: Uint8Array) => void,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n rng(randomView);\n}\n\n/** Writes sentinel min/max random bytes into codec-owned scratch. */\nfunction buildSentinelPayload(\n ms: number,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n randomView.fill(fill);\n}\n\n/** Decodes the creation timestamp from a trusted wire ID. */\nfunction extractTimestampFromId<Brand extends string>(prefix: Prefix<Brand>, id: Id<Brand>): Date {\n return new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length)));\n}\n\n/** Layout ops binder for the Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */\nexport function createTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n rng: (target: Uint8Array) => void,\n) {\n // Per-codec scratch buffer. Shared across generateAt(), minIdForTime(),\n // maxIdForTime(), and exampleWireId() — all are synchronous and overwrite both\n // the timestamp and random slices before encoding, so successive callers see\n // their own freshly-written bytes. toWireId reads the buffer and returns an\n // 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 generateAt: (ms: number): Id<Brand> => {\n buildPayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n extractTimestamp: (id: Id<Brand>): Date => extractTimestampFromId(prefix, id),\n minIdForTime: (ms: number): Id<Brand> => {\n buildSentinelPayload(ms, 0x00, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n maxIdForTime: (ms: number): Id<Brand> => {\n buildSentinelPayload(ms, 0xff, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n exampleWireId: (ms: number): Id<Brand> => {\n buildPayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n };\n}\n","import { validateBrand } from \"./brand.js\";\nimport { createTimestampLayoutOps } from \"./layouts/timestamp.js\";\nimport { registerBrand } from \"./registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\n\n/**\n * Configuration options for a codec instance.\n */\nexport type TimestampOptions = {\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to a `crypto.randomUUID` fast path. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\ntype ResolvedTimestampOptions = Required<Pick<TimestampOptions, \"now\" | \"rng\">> &\n Pick<TimestampOptions, \"allowDuplicateBrand\">;\n\n/**\n * A brand-scoped codec for generating and validating public-facing IDs.\n *\n * Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a\n * 16-byte payload (6-byte ms timestamp + 10 random bytes). IDs sort by creation\n * time in ascending order.\n *\n * For encrypted IDs, use `createOpaqueTimestampId` from `@smonn/ids/opaque`.\n */\nexport type TimestampCodec<Brand extends string> = {\n /** Produces a new canonical ID using the codec's `now` and `rng`. */\n generate(): Id<Brand>;\n /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */\n generateAt(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /** Tight lower bound for any ID generated at `date` (random portion `0x00`). Throws on invalid dates. */\n minIdForTime(date: Date): Id<Brand>;\n /** Tight upper bound for any ID generated at `date` (random portion `0xff`). Throws on invalid dates. */\n maxIdForTime(date: Date): Id<Brand>;\n /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\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 defaultTimestampOptions: ResolvedTimestampOptions = {\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\n/**\n * Creates a codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createTimestampId<Brand extends string>(\n brand: Brand,\n opts: TimestampOptions = {},\n): TimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const options = {\n now: opts.now ?? defaultTimestampOptions.now,\n rng: opts.rng ?? defaultTimestampOptions.rng,\n } satisfies ResolvedTimestampOptions;\n\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createTimestampLayoutOps(prefix, options.rng);\n\n return {\n generate: () => layout.generateAt(options.now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n minIdForTime: (date: Date) => layout.minIdForTime(date.getTime()),\n maxIdForTime: (date: Date) => layout.maxIdForTime(date.getTime()),\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(options.now())),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"timestamp-B5_UCzc6.mjs","names":[],"sources":["../src/layouts/timestamp.ts","../src/timestamp.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { toWireId } from \"../wire/envelope.js\";\nimport { payloadByteLength } from \"../wire/invariants.js\";\nimport {\n readTimestampMsFromBase32Suffix,\n timestampByteLength,\n writeTimestamp,\n} from \"../wire/timestamp-bytes.js\";\n\nconst randomByteLength: number = payloadByteLength - timestampByteLength;\n\n/** Writes a 16-byte timestamp-layout payload into codec-owned scratch. */\nfunction buildPayload(\n ms: number,\n rng: (target: Uint8Array) => void,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n rng(randomView);\n}\n\n/** Writes sentinel min/max random bytes into codec-owned scratch. */\nfunction buildSentinelPayload(\n ms: number,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n randomView.fill(fill);\n}\n\n/** Decodes the creation timestamp from a trusted wire ID. */\nfunction extractTimestampFromId<Brand extends string>(prefix: Prefix<Brand>, id: Id<Brand>): Date {\n return new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length)));\n}\n\n/** Layout ops binder for the Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */\nexport function createTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n rng: (target: Uint8Array) => void,\n) {\n // Per-codec scratch buffer. Shared across generateAt(), minIdForTime(),\n // maxIdForTime(), and exampleWireId() — all are synchronous and overwrite both\n // the timestamp and random slices before encoding, so successive callers see\n // their own freshly-written bytes. toWireId reads the buffer and returns an\n // 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 generateAt: (ms: number): Id<Brand> => {\n buildPayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n extractTimestamp: (id: Id<Brand>): Date => extractTimestampFromId(prefix, id),\n minIdForTime: (ms: number): Id<Brand> => {\n buildSentinelPayload(ms, 0x00, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n maxIdForTime: (ms: number): Id<Brand> => {\n buildSentinelPayload(ms, 0xff, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n exampleWireId: (ms: number): Id<Brand> => {\n buildPayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n };\n}\n","import { validateBrand } from \"./brand.js\";\nimport { createTimestampLayoutOps } from \"./layouts/timestamp.js\";\nimport { registerBrand } from \"./registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\n\n/**\n * Configuration options for a codec instance.\n */\nexport type TimestampOptions = {\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to a `crypto.randomUUID` fast path. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\ntype ResolvedTimestampOptions = Required<Pick<TimestampOptions, \"now\" | \"rng\">> &\n Pick<TimestampOptions, \"allowDuplicateBrand\">;\n\n/**\n * A brand-scoped codec for generating and validating public-facing IDs.\n *\n * Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a\n * 16-byte payload (6-byte ms timestamp + 10 random bytes). IDs sort by creation\n * time in ascending order.\n *\n * For encrypted IDs, use `createOpaqueTimestampId` from `@smonn/ids/opaque`.\n */\nexport type TimestampCodec<Brand extends string> = {\n /** Produces a new canonical ID using the codec's `now` and `rng`. */\n generate(): Id<Brand>;\n /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */\n generateAt(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /** Tight lower bound for any ID generated at `date` (random portion `0x00`). Throws on invalid dates. */\n minIdForTime(date: Date): Id<Brand>;\n /** Tight upper bound for any ID generated at `date` (random portion `0xff`). Throws on invalid dates. */\n maxIdForTime(date: Date): Id<Brand>;\n /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\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 defaultTimestampOptions: ResolvedTimestampOptions = {\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\n/**\n * Creates a codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createTimestampId<Brand extends string>(\n brand: Brand,\n opts: TimestampOptions = {},\n): TimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const options = {\n now: opts.now ?? defaultTimestampOptions.now,\n rng: opts.rng ?? defaultTimestampOptions.rng,\n } satisfies ResolvedTimestampOptions;\n\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createTimestampLayoutOps(prefix, options.rng);\n\n return {\n generate: () => layout.generateAt(options.now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n minIdForTime: (date: Date) => layout.minIdForTime(date.getTime()),\n maxIdForTime: (date: Date) => layout.maxIdForTime(date.getTime()),\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(options.now())),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as decodeBase32 } from "./codec-shell-
|
|
1
|
+
import { o as decodeBase32 } from "./codec-shell-DH-UO4UR.mjs";
|
|
2
2
|
const timestampBase32Length = Math.ceil(48 / 5);
|
|
3
3
|
/** Write the timestamp in big-endian; encoded via mod-256 to avoid 32-bit bitwise coercion. */
|
|
4
4
|
function writeTimestamp(ms, buffer) {
|
|
@@ -23,4 +23,4 @@ function readTimestampMsFromBase32Suffix(base32Suffix) {
|
|
|
23
23
|
//#endregion
|
|
24
24
|
export { readTimestampMsFromBase32Suffix as n, writeTimestamp as r, readTimestampMs as t };
|
|
25
25
|
|
|
26
|
-
//# sourceMappingURL=timestamp-bytes-
|
|
26
|
+
//# sourceMappingURL=timestamp-bytes-BBY7JI33.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timestamp-bytes-
|
|
1
|
+
{"version":3,"file":"timestamp-bytes-BBY7JI33.mjs","names":[],"sources":["../src/wire/timestamp-bytes.ts"],"sourcesContent":["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-family 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"],"mappings":";AAMA,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"}
|