@smonn/ids 0.12.3 → 0.13.1

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.
Files changed (84) hide show
  1. package/README.md +42 -32
  2. package/dist/{adapter-types-CdYJM6Sf.d.mts → adapter-types-CIc-4O-P.d.mts} +2 -2
  3. package/dist/{adapter-types-CdYJM6Sf.d.mts.map → adapter-types-CIc-4O-P.d.mts.map} +1 -1
  4. package/dist/cli.mjs +316 -178
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{codec-shell-DvrTDa65.mjs → codec-shell-C2NKQEx2.mjs} +5 -1
  7. package/dist/codec-shell-C2NKQEx2.mjs.map +1 -0
  8. package/dist/{digest-CknNw2wa.mjs → digest-DsGeXfk3.mjs} +9 -24
  9. package/dist/digest-DsGeXfk3.mjs.map +1 -0
  10. package/dist/digest.d.mts +3 -3
  11. package/dist/digest.d.mts.map +1 -1
  12. package/dist/digest.mjs +1 -1
  13. package/dist/drizzle.d.mts +3 -3
  14. package/dist/drizzle.d.mts.map +1 -1
  15. package/dist/drizzle.mjs.map +1 -1
  16. package/dist/error-Cp5qYZcv.mjs.map +1 -1
  17. package/dist/{error-JIPylU_E.d.mts → error-Dqyho9vp.d.mts} +7 -2
  18. package/dist/error-Dqyho9vp.d.mts.map +1 -0
  19. package/dist/express.d.mts +2 -2
  20. package/dist/fastify.d.mts +2 -2
  21. package/dist/graphql.d.mts +3 -3
  22. package/dist/graphql.mjs +2 -2
  23. package/dist/graphql.mjs.map +1 -1
  24. package/dist/hono.d.mts +2 -2
  25. package/dist/hono.mjs +1 -2
  26. package/dist/hono.mjs.map +1 -1
  27. package/dist/index.d.mts +21 -5
  28. package/dist/index.d.mts.map +1 -1
  29. package/dist/index.mjs +1 -1
  30. package/dist/{key-material-f29JIyrz.mjs → key-material-DvjACe89.mjs} +53 -2
  31. package/dist/key-material-DvjACe89.mjs.map +1 -0
  32. package/dist/kysely.d.mts +3 -3
  33. package/dist/kysely.d.mts.map +1 -1
  34. package/dist/kysely.mjs.map +1 -1
  35. package/dist/mikro-orm.d.mts +3 -3
  36. package/dist/mikro-orm.d.mts.map +1 -1
  37. package/dist/mikro-orm.mjs.map +1 -1
  38. package/dist/nestjs.d.mts +2 -2
  39. package/dist/nestjs.mjs +1 -1
  40. package/dist/nestjs.mjs.map +1 -1
  41. package/dist/{opaque-BQVNoIIh.mjs → opaque-BW3Uzeeb.mjs} +5 -28
  42. package/dist/opaque-BW3Uzeeb.mjs.map +1 -0
  43. package/dist/opaque.d.mts +22 -4
  44. package/dist/opaque.d.mts.map +1 -1
  45. package/dist/opaque.mjs +1 -1
  46. package/dist/prisma.d.mts +32 -27
  47. package/dist/prisma.d.mts.map +1 -1
  48. package/dist/prisma.mjs +11 -15
  49. package/dist/prisma.mjs.map +1 -1
  50. package/dist/{reverse-DsPd7Lco.mjs → reverse-BW8g_cln.mjs} +12 -5
  51. package/dist/reverse-BW8g_cln.mjs.map +1 -0
  52. package/dist/reverse.d.mts +20 -4
  53. package/dist/reverse.d.mts.map +1 -1
  54. package/dist/reverse.mjs +1 -1
  55. package/dist/{rng-Clos6uC0.mjs → rng-BHFxX1Fc.mjs} +2 -2
  56. package/dist/{rng-Clos6uC0.mjs.map → rng-BHFxX1Fc.mjs.map} +1 -1
  57. package/dist/{signed-4h2BnlWx.mjs → signed-BTz3ZFYE.mjs} +12 -33
  58. package/dist/signed-BTz3ZFYE.mjs.map +1 -0
  59. package/dist/signed.d.mts +13 -4
  60. package/dist/signed.d.mts.map +1 -1
  61. package/dist/signed.mjs +1 -1
  62. package/dist/{timestamp-Cg9nRfnK.mjs → timestamp-CleAIdZI.mjs} +12 -5
  63. package/dist/timestamp-CleAIdZI.mjs.map +1 -0
  64. package/dist/typeorm.d.mts +2 -2
  65. package/dist/typeorm.d.mts.map +1 -1
  66. package/dist/typeorm.mjs.map +1 -1
  67. package/dist/{types-g7CiQDyE.d.mts → types-wplmOgOK.d.mts} +20 -3
  68. package/dist/types-wplmOgOK.d.mts.map +1 -0
  69. package/dist/{wrapped-BQ-lNECo.mjs → wrapped-DPlsv1x-.mjs} +18 -76
  70. package/dist/wrapped-DPlsv1x-.mjs.map +1 -0
  71. package/dist/wrapped.d.mts +31 -5
  72. package/dist/wrapped.d.mts.map +1 -1
  73. package/dist/wrapped.mjs +1 -1
  74. package/package.json +80 -27
  75. package/dist/codec-shell-DvrTDa65.mjs.map +0 -1
  76. package/dist/digest-CknNw2wa.mjs.map +0 -1
  77. package/dist/error-JIPylU_E.d.mts.map +0 -1
  78. package/dist/key-material-f29JIyrz.mjs.map +0 -1
  79. package/dist/opaque-BQVNoIIh.mjs.map +0 -1
  80. package/dist/reverse-DsPd7Lco.mjs.map +0 -1
  81. package/dist/signed-4h2BnlWx.mjs.map +0 -1
  82. package/dist/timestamp-Cg9nRfnK.mjs.map +0 -1
  83. package/dist/types-g7CiQDyE.d.mts.map +0 -1
  84. package/dist/wrapped-BQ-lNECo.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/adapters/prisma.ts"],"mappings":";;;;;;;;;;;;;;KAkBY,WAAA;;;;;;;;EAQV,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;EAQT;AAsClB;;;;;;EAtCE,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;AAAA;;;;;;;;;AAsCsE;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAxE,OAAA,uBAA8B,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,WAAA,CAAY,KAAA"}
1
+ {"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/adapters/prisma.ts"],"mappings":";;;;;AAYA;;;;AAAA,KAAY,WAAA;;;;;;EAMV,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;EA6BsB;;;;;;;EArB/C,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;;;;;;;;;;;;;;AAqBkC;AA6BpD;;;;EA/BE,YAAA,CAAa,SAAA;IACX,KAAA,EAAO,MAAA;IACP,OAAA,GAAU,KAAA,EAAO,MAAA,sBAA4B,EAAA,CAAG,KAAA;EAAA;AAAA;;;;;;;;AA6BoC;;;;;;;;;;;;;;;;;;iBAAxE,OAAA,uBAA8B,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,WAAA,CAAY,KAAA"}
package/dist/prisma.mjs CHANGED
@@ -6,12 +6,9 @@ import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
6
6
  *
7
7
  * Works with any codec variant exposing `safeParse`.
8
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.
9
+ * Use `computeField(fieldName)` to produce a typed `$extends` result-component
10
+ * field definition the brand is carried through Prisma's type machinery
11
+ * automatically and no per-call-site cast is required.
15
12
  *
16
13
  * @example
17
14
  * ```ts
@@ -23,17 +20,10 @@ import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
23
20
  *
24
21
  * const xprisma = prisma.$extends({
25
22
  * 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
- * },
23
+ * user: { id: userIdField.computeField("id") },
35
24
  * },
36
25
  * });
26
+ * // xprisma.user.findUnique(…).id is typed as Id<"usr"> — no cast required
37
27
  * ```
38
28
  */
39
29
  function idField(codec) {
@@ -43,6 +33,12 @@ function idField(codec) {
43
33
  },
44
34
  write(value) {
45
35
  return value;
36
+ },
37
+ computeField(fieldName) {
38
+ return {
39
+ needs: { [fieldName]: true },
40
+ compute: (model) => readIdColumn(codec, model[fieldName])
41
+ };
46
42
  }
47
43
  };
48
44
  }
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.mjs","names":[],"sources":["../src/adapters/prisma.ts"],"sourcesContent":["import { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } 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\nexport type { IdColumnCodec };\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`.\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 return readIdColumn(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,SAAgB,QAA8B,OAAiD;CAC7F,OAAO;EACL,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;CACF;AACF"}
1
+ {"version":3,"file":"prisma.mjs","names":[],"sources":["../src/adapters/prisma.ts"],"sourcesContent":["import { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } 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 } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * Read/write transform pair and `$extends` result-component factory for\n * integrating `Id<Brand>` with Prisma extensions.\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 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 * Creates a typed `$extends` result-component field definition that carries\n * `Id<Brand>` through Prisma's type machinery without a per-call-site cast.\n *\n * @param fieldName - The model field to read from (e.g. `\"id\"`).\n * @returns A `{ needs, compute }` object whose `compute` return type is\n * statically `Id<Brand>`, so the extended-client model field is typed correctly.\n *\n * @example\n * ```ts\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> no cast required\n * ```\n */\n computeField(fieldName: string): {\n needs: Record<string, boolean>;\n compute: (model: Record<string, unknown>) => Id<Brand>;\n };\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`.\n *\n * Use `computeField(fieldName)` to produce a typed `$extends` result-component\n * field definition the brand is carried through Prisma's type machinery\n * automatically and no per-call-site cast is required.\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: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> no cast required\n * ```\n */\nexport function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand> {\n return {\n read(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n computeField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n // Prisma's $extends types `compute` as returning `any` in its constraint\n // type (DynamicResultExtensionArgs). Returning a pre-built object with an\n // explicit Id<Brand> return type on `compute` causes TypeScript to infer\n // the brand through the `& R` intersection in $extends — encapsulating\n // the single necessary cast here rather than pushing it to every call site.\n compute: (model: Record<string, unknown>): Id<Brand> =>\n readIdColumn(codec, model[fieldName]),\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,SAAgB,QAA8B,OAAiD;CAC7F,OAAO;EACL,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;EACA,aAAa,WAAmB;GAC9B,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAM3B,UAAU,UACR,aAAa,OAAO,MAAM,UAAU;GACxC;EACF;CACF;AACF"}
@@ -1,5 +1,5 @@
1
- import { a as toWireId, i as payloadBytesFromId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-DvrTDa65.mjs";
2
- import { a as writeTimestamp, n as fastTenByteRng } from "./rng-Clos6uC0.mjs";
1
+ import { a as toWireId, i as payloadBytesFromId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-C2NKQEx2.mjs";
2
+ import { a as writeTimestamp, n as fastTenByteRng } from "./rng-BHFxX1Fc.mjs";
3
3
  //#region src/codecs/reverse/layout.ts
4
4
  const randomByteLength = 10;
5
5
  /** Writes inverted timestamp bytes, then fills random portion. */
@@ -40,7 +40,7 @@ function createReverseTimestampLayoutOps(prefix, rng) {
40
40
  return toWireId(prefix, buffer);
41
41
  },
42
42
  exampleWireId: (ms) => {
43
- buildReversePayload(ms, rng, buffer, randomView);
43
+ buildReversePayload(ms ?? Date.now(), rng, buffer, randomView);
44
44
  return toWireId(prefix, buffer);
45
45
  }
46
46
  };
@@ -56,6 +56,13 @@ function createReverseTimestampLayoutOps(prefix, rng) {
56
56
  *
57
57
  * @param brand - Entity type brand validated once at construction.
58
58
  * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.
59
+ * @example
60
+ * ```ts
61
+ * const posts = createReverseTimestampId("pst");
62
+ *
63
+ * const id = posts.generate(); // Id<"pst"> (newest-first sort)
64
+ * posts.extractTimestamp(id); // Date
65
+ * ```
59
66
  */
60
67
  function createReverseTimestampId(brand, opts = {}) {
61
68
  validateBrand(brand);
@@ -74,11 +81,11 @@ function createReverseTimestampId(brand, opts = {}) {
74
81
  extractTimestamp: layout.extractTimestamp,
75
82
  minIdForTime: (date) => layout.minIdForTime(date.getTime()),
76
83
  maxIdForTime: (date) => layout.maxIdForTime(date.getTime()),
77
- toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(now())),
84
+ toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),
78
85
  "~standard": wire["~standard"]
79
86
  };
80
87
  }
81
88
  //#endregion
82
89
  export { createReverseTimestampId as t };
83
90
 
84
- //# sourceMappingURL=reverse-DsPd7Lco.mjs.map
91
+ //# sourceMappingURL=reverse-BW8g_cln.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reverse-BW8g_cln.mjs","names":[],"sources":["../src/codecs/reverse/layout.ts","../src/codecs/reverse/index.ts"],"sourcesContent":["import type { Id, LayoutOps, 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): LayoutOps<Brand> & {\n generateAt(ms: number): Id<Brand>;\n extractTimestamp(id: Id<Brand>): Date;\n minIdForTime(ms: number): Id<Brand>;\n maxIdForTime(ms: number): Id<Brand>;\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 ?? Date.now(), rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n };\n}\n","import { validateBrand } from \"../_kernel/brand.js\";\nimport { createReverseTimestampLayoutOps } from \"./layout.js\";\nimport { registerBrand } from \"../_kernel/registry.js\";\nimport { fastTenByteRng } from \"../_kernel/rng.js\";\nimport type {\n Id,\n JsonSchema,\n ParseResult,\n Prefix,\n StandardSchemaProps,\n ValidBrand,\n} 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 } from \"../../error.js\";\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 the 10-byte random tail into `target`. Defaults to a `crypto.randomUUID` harvest fast path (same as the Timestamp codec). */\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 * Best-effort: inverts and decodes the timestamp bytes in the payload without any\n * additional verification. An ID that bypassed `safeParse()` (e.g. via a type\n * assertion) may return a plausible-looking but incorrect `Date`.\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 /**\n * JSON Schema for the canonical wire form. The `pattern` matches the canonical stored\n * form only and is deliberately stricter than `parse()`/`safeParse()`, which accept\n * uppercase letters and Crockford aliases (`o`/`i`/`l`) before normalising. See ADR-0003.\n */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\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 * @example\n * ```ts\n * const posts = createReverseTimestampId(\"pst\");\n *\n * const id = posts.generate(); // Id<\"pst\"> (newest-first sort)\n * posts.extractTimestamp(id); // Date\n * ```\n */\nexport function createReverseTimestampId<Brand extends string>(\n brand: Brand & ValidBrand<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 ?? fastTenByteRng;\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()),\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,KAMA;CACA,MAAM,yBAAS,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,OAA2B;GACzC,oBAAoB,MAAM,KAAK,IAAI,GAAG,KAAK,QAAQ,UAAU;GAC7D,OAAO,SAAS,QAAQ,MAAM;EAChC;CACF;AACF;;;;;;;;;;;;;;;;;;;;AC8BA,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,CAAC;EACnE,aAAa,KAAK;CACpB;AACF"}
@@ -1,5 +1,5 @@
1
- import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
- import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, t as Id } from "./types-g7CiQDyE.mjs";
1
+ import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, o as ValidBrand, t as Id } from "./types-wplmOgOK.mjs";
2
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-Dqyho9vp.mjs";
3
3
 
4
4
  //#region src/codecs/reverse/index.d.ts
5
5
  /**
@@ -42,6 +42,10 @@ type ReverseTimestampCodec<Brand extends string> = {
42
42
  /**
43
43
  * Decodes the creation `Date` from an `Id<Brand>` by inverting the timestamp bytes.
44
44
  * Trusts the type — use `safeParse()` at boundaries first.
45
+ *
46
+ * Best-effort: inverts and decodes the timestamp bytes in the payload without any
47
+ * additional verification. An ID that bypassed `safeParse()` (e.g. via a type
48
+ * assertion) may return a plausible-looking but incorrect `Date`.
45
49
  */
46
50
  extractTimestamp(id: Id<Brand>): Date;
47
51
  /**
@@ -57,7 +61,12 @@ type ReverseTimestampCodec<Brand extends string> = {
57
61
  * use `maxIdForTime(t_old)` as the upper bound when scanning [t_old, t_new].
58
62
  * Throws on invalid dates.
59
63
  */
60
- maxIdForTime(date: Date): Id<Brand>; /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */
64
+ maxIdForTime(date: Date): Id<Brand>;
65
+ /**
66
+ * JSON Schema for the canonical wire form. The `pattern` matches the canonical stored
67
+ * form only and is deliberately stricter than `parse()`/`safeParse()`, which accept
68
+ * uppercase letters and Crockford aliases (`o`/`i`/`l`) before normalising. See ADR-0003.
69
+ */
61
70
  toJsonSchema(): JsonSchema; /** Standard Schema validate entry point. */
62
71
  readonly "~standard": StandardSchemaProps<Brand>;
63
72
  };
@@ -70,8 +79,15 @@ type ReverseTimestampCodec<Brand extends string> = {
70
79
  *
71
80
  * @param brand - Entity type brand validated once at construction.
72
81
  * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.
82
+ * @example
83
+ * ```ts
84
+ * const posts = createReverseTimestampId("pst");
85
+ *
86
+ * const id = posts.generate(); // Id<"pst"> (newest-first sort)
87
+ * posts.extractTimestamp(id); // Date
88
+ * ```
73
89
  */
74
- declare function createReverseTimestampId<Brand extends string>(brand: Brand, opts?: ReverseTimestampOptions): ReverseTimestampCodec<Brand>;
90
+ declare function createReverseTimestampId<Brand extends string>(brand: Brand & ValidBrand<Brand>, opts?: ReverseTimestampOptions): ReverseTimestampCodec<Brand>;
75
91
  //#endregion
76
92
  export { IdsError, type IdsErrorCode, ReverseTimestampCodec, ReverseTimestampOptions, createReverseTimestampId, isIdsError };
77
93
  //# sourceMappingURL=reverse.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reverse.d.mts","names":[],"sources":["../src/codecs/reverse/index.ts"],"mappings":";;;;AAcA;;;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;;;;;;;;;;;iBAa5B,wBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,GAAM,uBAAA,GACL,qBAAA,CAAsB,KAAA"}
1
+ {"version":3,"file":"reverse.d.mts","names":[],"sources":["../src/codecs/reverse/index.ts"],"mappings":";;;;;AAoBA;;KAAY,uBAAA;EAIK,6EAFf,GAAA;EAEA,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;AAAA;AAgBF;;;;;;;;;;;;AAhBE,KAgBU,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;EAmCJ;;;EA/BtB,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;;;;;;EASvC,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;;;;;;EAM7B,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;;;;AAAA;AAoB5C;;;;;;;iBAAgB,wBAAA,uBACd,KAAA,EAAO,KAAA,GAAQ,UAAA,CAAW,KAAA,GAC1B,IAAA,GAAM,uBAAA,GACL,qBAAA,CAAsB,KAAA"}
package/dist/reverse.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { t as createReverseTimestampId } from "./reverse-DsPd7Lco.mjs";
2
+ import { t as createReverseTimestampId } from "./reverse-BW8g_cln.mjs";
3
3
  export { IdsError, createReverseTimestampId, isIdsError };
@@ -1,4 +1,4 @@
1
- import { o as decodeBase32 } from "./codec-shell-DvrTDa65.mjs";
1
+ import { o as decodeBase32 } from "./codec-shell-C2NKQEx2.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) {
@@ -60,4 +60,4 @@ function fastTenByteRng(target) {
60
60
  //#endregion
61
61
  export { writeTimestamp as a, readTimestampMsFromBase32Suffix as i, fastTenByteRng as n, readTimestampMs as r, defaultRng as t };
62
62
 
63
- //# sourceMappingURL=rng-Clos6uC0.mjs.map
63
+ //# sourceMappingURL=rng-BHFxX1Fc.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rng-Clos6uC0.mjs","names":[],"sources":["../src/wire/timestamp-bytes.ts","../src/codecs/_kernel/rng.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 (!Number.isInteger(ms)) throw new Error(\"timestamp is not an integer\");\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","/** Default RNG: writes cryptographically random bytes via `crypto.getRandomValues`. */\nexport function defaultRng(target: Uint8Array): void {\n crypto.getRandomValues(target as Uint8Array<ArrayBuffer>);\n}\n\n// hex charCode → 0–15 nibble, for harvesting bytes out of a UUIDv4 string.\n// Covers ['0'-'9' = 48–57] and ['a'-'f' = 97–102]; randomUUID is 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\n/**\n * Fast RNG for the 10-byte random tail shared by the plaintext timestamp layouts\n * (Timestamp and Reverse Timestamp codecs). Writes exactly `target[0..9]`.\n *\n * `crypto.randomUUID()` is ~7× faster than `crypto.getRandomValues` in Node 24\n * (~84 ns vs ~610 ns for a 16-byte fill — the UUID path has a tight fixed-format\n * fast path). A UUIDv4 string carries 122 cryptographically-random bits; we\n * harvest 10 fully-random bytes from positions where no version (hex 12) or\n * variant (hex 16) bits sit. String layout: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`\n * — bytes 0–5 are string[0..7]+string[9..12], bytes 6–9 are string[24..31].\n *\n * Security-equivalent to `defaultRng` for a 10-byte tail (both are CSPRNG-backed,\n * fully-random bytes); the only difference is throughput.\n */\nexport function fastTenByteRng(target: Uint8Array): void {\n const s = crypto.randomUUID();\n target[0] = (hexCharCodeToNibble[s.charCodeAt(0)]! << 4) | hexCharCodeToNibble[s.charCodeAt(1)]!;\n target[1] = (hexCharCodeToNibble[s.charCodeAt(2)]! << 4) | hexCharCodeToNibble[s.charCodeAt(3)]!;\n target[2] = (hexCharCodeToNibble[s.charCodeAt(4)]! << 4) | hexCharCodeToNibble[s.charCodeAt(5)]!;\n target[3] = (hexCharCodeToNibble[s.charCodeAt(6)]! << 4) | hexCharCodeToNibble[s.charCodeAt(7)]!;\n target[4] = (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"],"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,CAAC,OAAO,UAAU,EAAE,GAAG,MAAM,IAAI,MAAM,6BAA6B;CACxE,IAAI,KAAK,GAAG,MAAM,IAAI,MAAM,uBAAuB;CACnD,IAAI,MAAM,KAAA,IACR,MAAM,IAAI,MAAM,gCAAgC;CAElD,KAAK,IAAI,IAAA,GAA6B,KAAK,GAAG,KAAK;EACjD,OAAO,KAAK,KAAK;EACjB,KAAK,KAAK,MAAM,KAAK,GAAG;CAC1B;AACF;;AAGA,SAAgB,gBAAgB,QAA4B;CAC1D,IAAI,KAAK;CACT,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KAAK,KAAK,KAAK,MAAM,OAAO;CACrE,OAAO;AACT;;AAGA,SAAgB,gCAAgC,cAA8B;CAC5E,OAAO,gBAAgB,aAAa,aAAa,MAAM,GAAG,qBAAqB,CAAC,CAAC;AACnF;;;;AC/BA,SAAgB,WAAW,QAA0B;CACnD,OAAO,gBAAgB,MAAiC;AAC1D;AAIA,MAAM,sCAAsB,IAAI,WAAW,GAAG;AAC9C,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,oBAAoB,KAAK,KAAK;AAC3D,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,oBAAoB,KAAK,KAAK,KAAK;;;;;;;;;;;;;;;AAgB/D,SAAgB,eAAe,QAA0B;CACvD,MAAM,IAAI,OAAO,WAAW;CAC5B,OAAO,KAAM,oBAAoB,EAAE,WAAW,CAAC,MAAO,IAAK,oBAAoB,EAAE,WAAW,CAAC;CAC7F,OAAO,KAAM,oBAAoB,EAAE,WAAW,CAAC,MAAO,IAAK,oBAAoB,EAAE,WAAW,CAAC;CAC7F,OAAO,KAAM,oBAAoB,EAAE,WAAW,CAAC,MAAO,IAAK,oBAAoB,EAAE,WAAW,CAAC;CAC7F,OAAO,KAAM,oBAAoB,EAAE,WAAW,CAAC,MAAO,IAAK,oBAAoB,EAAE,WAAW,CAAC;CAC7F,OAAO,KAAM,oBAAoB,EAAE,WAAW,CAAC,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;CAC9F,OAAO,KACJ,oBAAoB,EAAE,WAAW,EAAE,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;CACrF,OAAO,KACJ,oBAAoB,EAAE,WAAW,EAAE,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;CACrF,OAAO,KACJ,oBAAoB,EAAE,WAAW,EAAE,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;CACrF,OAAO,KACJ,oBAAoB,EAAE,WAAW,EAAE,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;CACrF,OAAO,KACJ,oBAAoB,EAAE,WAAW,EAAE,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;AACvF"}
1
+ {"version":3,"file":"rng-BHFxX1Fc.mjs","names":[],"sources":["../src/wire/timestamp-bytes.ts","../src/codecs/_kernel/rng.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 (!Number.isInteger(ms)) throw new Error(\"timestamp is not an integer\");\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","/** Default RNG: writes cryptographically random bytes via `crypto.getRandomValues`. */\nexport function defaultRng(target: Uint8Array): void {\n crypto.getRandomValues(target as Uint8Array<ArrayBuffer>);\n}\n\n// hex charCode → 0–15 nibble, for harvesting bytes out of a UUIDv4 string.\n// Covers ['0'-'9' = 48–57] and ['a'-'f' = 97–102]; randomUUID is 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\n/**\n * Fast RNG for the 10-byte random tail shared by the plaintext timestamp layouts\n * (Timestamp and Reverse Timestamp codecs). Writes exactly `target[0..9]`.\n *\n * `crypto.randomUUID()` is ~7× faster than `crypto.getRandomValues` in Node 24\n * (~84 ns vs ~610 ns for a 16-byte fill — the UUID path has a tight fixed-format\n * fast path). A UUIDv4 string carries 122 cryptographically-random bits; we\n * harvest 10 fully-random bytes from positions where no version (hex 12) or\n * variant (hex 16) bits sit. String layout: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`\n * — bytes 0–5 are string[0..7]+string[9..12], bytes 6–9 are string[24..31].\n *\n * Security-equivalent to `defaultRng` for a 10-byte tail (both are CSPRNG-backed,\n * fully-random bytes); the only difference is throughput.\n */\nexport function fastTenByteRng(target: Uint8Array): void {\n const s = crypto.randomUUID();\n target[0] = (hexCharCodeToNibble[s.charCodeAt(0)]! << 4) | hexCharCodeToNibble[s.charCodeAt(1)]!;\n target[1] = (hexCharCodeToNibble[s.charCodeAt(2)]! << 4) | hexCharCodeToNibble[s.charCodeAt(3)]!;\n target[2] = (hexCharCodeToNibble[s.charCodeAt(4)]! << 4) | hexCharCodeToNibble[s.charCodeAt(5)]!;\n target[3] = (hexCharCodeToNibble[s.charCodeAt(6)]! << 4) | hexCharCodeToNibble[s.charCodeAt(7)]!;\n target[4] = (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"],"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,CAAC,OAAO,UAAU,EAAE,GAAG,MAAM,IAAI,MAAM,6BAA6B;CACxE,IAAI,KAAK,GAAG,MAAM,IAAI,MAAM,uBAAuB;CACnD,IAAI,MAAM,KAAA,IACR,MAAM,IAAI,MAAM,gCAAgC;CAElD,KAAK,IAAI,IAAA,GAA6B,KAAK,GAAG,KAAK;EACjD,OAAO,KAAK,KAAK;EACjB,KAAK,KAAK,MAAM,KAAK,GAAG;CAC1B;AACF;;AAGA,SAAgB,gBAAgB,QAA4B;CAC1D,IAAI,KAAK;CACT,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KAAK,KAAK,KAAK,MAAM,OAAO;CACrE,OAAO;AACT;;AAGA,SAAgB,gCAAgC,cAA8B;CAC5E,OAAO,gBAAgB,aAAa,aAAa,MAAM,GAAG,qBAAqB,CAAC,CAAC;AACnF;;;;AC/BA,SAAgB,WAAW,QAA0B;CACnD,OAAO,gBAAgB,MAAiC;AAC1D;AAIA,MAAM,sCAAsB,IAAI,WAAW,GAAG;AAC9C,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,oBAAoB,KAAK,KAAK;AAC3D,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,oBAAoB,KAAK,KAAK,KAAK;;;;;;;;;;;;;;;AAgB/D,SAAgB,eAAe,QAA0B;CACvD,MAAM,IAAI,OAAO,WAAW;CAC5B,OAAO,KAAM,oBAAoB,EAAE,WAAW,CAAC,MAAO,IAAK,oBAAoB,EAAE,WAAW,CAAC;CAC7F,OAAO,KAAM,oBAAoB,EAAE,WAAW,CAAC,MAAO,IAAK,oBAAoB,EAAE,WAAW,CAAC;CAC7F,OAAO,KAAM,oBAAoB,EAAE,WAAW,CAAC,MAAO,IAAK,oBAAoB,EAAE,WAAW,CAAC;CAC7F,OAAO,KAAM,oBAAoB,EAAE,WAAW,CAAC,MAAO,IAAK,oBAAoB,EAAE,WAAW,CAAC;CAC7F,OAAO,KAAM,oBAAoB,EAAE,WAAW,CAAC,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;CAC9F,OAAO,KACJ,oBAAoB,EAAE,WAAW,EAAE,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;CACrF,OAAO,KACJ,oBAAoB,EAAE,WAAW,EAAE,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;CACrF,OAAO,KACJ,oBAAoB,EAAE,WAAW,EAAE,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;CACrF,OAAO,KACJ,oBAAoB,EAAE,WAAW,EAAE,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;CACrF,OAAO,KACJ,oBAAoB,EAAE,WAAW,EAAE,MAAO,IAAK,oBAAoB,EAAE,WAAW,EAAE;AACvF"}
@@ -1,7 +1,7 @@
1
1
  import { t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { a as toWireId, i as payloadBytesFromId, n as registerBrand, r as payloadBase32Length, s as validateBrand, t as wireMethods } from "./codec-shell-DvrTDa65.mjs";
3
- import { a as writeTimestamp, i as readTimestampMsFromBase32Suffix, t as defaultRng } from "./rng-Clos6uC0.mjs";
4
- import { i as encodeKeyMaterial, n as assertValidKeyring, r as decodeKeyMaterial, t as assertValidKeyMaterialByteLength } from "./key-material-f29JIyrz.mjs";
2
+ import { a as toWireId, i as payloadBytesFromId, n as registerBrand, r as payloadBase32Length, s as validateBrand, t as wireMethods } from "./codec-shell-C2NKQEx2.mjs";
3
+ import { a as writeTimestamp, i as readTimestampMsFromBase32Suffix, t as defaultRng } from "./rng-BHFxX1Fc.mjs";
4
+ import { c as timingSafeEqual, i as encodeKeyMaterial, n as assertValidKeyring, o as deriveKey, r as decodeKeyMaterial, t as assertValidKeyMaterialByteLength } from "./key-material-DvjACe89.mjs";
5
5
  const tagByteLength = 5;
6
6
  const randomOffset = 6;
7
7
  const tagOffset = 11;
@@ -12,13 +12,6 @@ async function computeTag(hmacKey, brandBytes, signedContent) {
12
12
  message.set(signedContent, brandBytes.length);
13
13
  return new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, message)).subarray(0, tagByteLength);
14
14
  }
15
- function tagsEqual(a, b) {
16
- /* v8 ignore next -- defensive guard; both call sites always pass tagByteLength-byte arrays */
17
- if (a.length !== b.length) return false;
18
- let diff = 0;
19
- for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
20
- return diff === 0;
21
- }
22
15
  function createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys) {
23
16
  const signKey = hmacKeys[0];
24
17
  const brandBytes = new TextEncoder().encode(brand);
@@ -36,7 +29,7 @@ function createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys) {
36
29
  const payload = payloadBytesFromId(prefix, id);
37
30
  const storedTag = payload.subarray(tagOffset, 16);
38
31
  const signedContent = payload.subarray(0, signedContentByteLength);
39
- for (const hmacKey of hmacKeys) if (tagsEqual(storedTag, await computeTag(hmacKey, brandBytes, signedContent))) return true;
32
+ for (const hmacKey of hmacKeys) if (timingSafeEqual(storedTag, await computeTag(hmacKey, brandBytes, signedContent))) return true;
40
33
  return false;
41
34
  },
42
35
  extractTimestamp: (id) => new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length))),
@@ -50,13 +43,12 @@ function createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys) {
50
43
  syncBuffer.fill(255, randomOffset, 16);
51
44
  return toWireId(prefix, syncBuffer);
52
45
  },
53
- exampleWireId: () => prefix + "0".repeat(payloadBase32Length)
46
+ exampleWireId: (_ms) => prefix + "0".repeat(payloadBase32Length)
54
47
  };
55
48
  }
56
49
  //#endregion
57
50
  //#region src/codecs/signed/key.ts
58
51
  const hmacInfo = new TextEncoder().encode("@smonn/ids/signed/hmac");
59
- const SHA256_DIGEST_BYTES = 32;
60
52
  const internals = /* @__PURE__ */ new WeakMap();
61
53
  /**
62
54
  * Import raw operator key material into a {@link SigningKey} handle.
@@ -71,7 +63,11 @@ const internals = /* @__PURE__ */ new WeakMap();
71
63
  */
72
64
  async function importSigningKey(bytes) {
73
65
  assertValidKeyMaterialByteLength(bytes.length, "signing");
74
- const [hmacKey, digestBuffer] = await Promise.all([deriveHmacKey(bytes), crypto.subtle.digest("SHA-256", bytes)]);
66
+ const [hmacKey, digestBuffer] = await Promise.all([deriveKey(bytes, hmacInfo, {
67
+ name: "HMAC",
68
+ hash: "SHA-256",
69
+ length: 256
70
+ }, ["sign", "verify"]), crypto.subtle.digest("SHA-256", bytes)]);
75
71
  const key = Object.freeze({});
76
72
  internals.set(key, {
77
73
  keyDigest: new Uint8Array(digestBuffer),
@@ -110,11 +106,7 @@ function decodeSigningKey(encoded, format) {
110
106
  * not leak the position of the first differing byte through a timing side channel.
111
107
  */
112
108
  function signingKeysEqual(a, b) {
113
- const aDigest = getSigningKeyInternals(a).keyDigest;
114
- const bDigest = getSigningKeyInternals(b).keyDigest;
115
- let diff = 0;
116
- for (let i = 0; i < SHA256_DIGEST_BYTES; i++) diff |= aDigest[i] ^ bDigest[i];
117
- return diff === 0;
109
+ return timingSafeEqual(getSigningKeyInternals(a).keyDigest, getSigningKeyInternals(b).keyDigest);
118
110
  }
119
111
  /**
120
112
  * Returns the derived HMAC webcrypto.CryptoKey held inside the handle.
@@ -130,19 +122,6 @@ function getSigningKeyInternals(key) {
130
122
  if (keyInternals === void 0) throw new Error("invalid signing key");
131
123
  return keyInternals;
132
124
  }
133
- async function deriveHmacKey(bytes) {
134
- const base = await crypto.subtle.importKey("raw", bytes, "HKDF", false, ["deriveKey"]);
135
- return crypto.subtle.deriveKey({
136
- name: "HKDF",
137
- hash: "SHA-256",
138
- salt: /* @__PURE__ */ new Uint8Array(),
139
- info: hmacInfo
140
- }, base, {
141
- name: "HMAC",
142
- hash: "SHA-256",
143
- length: 256
144
- }, false, ["sign", "verify"]);
145
- }
146
125
  //#endregion
147
126
  //#region src/codecs/signed/index.ts
148
127
  /**
@@ -203,4 +182,4 @@ function createSignedTimestampId(brand, opts) {
203
182
  //#endregion
204
183
  export { importSigningKey as i, decodeSigningKey as n, encodeSigningKey as r, createSignedTimestampId as t };
205
184
 
206
- //# sourceMappingURL=signed-4h2BnlWx.mjs.map
185
+ //# sourceMappingURL=signed-BTz3ZFYE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signed-BTz3ZFYE.mjs","names":[],"sources":["../src/codecs/signed/layout.ts","../src/codecs/signed/key.ts","../src/codecs/signed/index.ts"],"sourcesContent":["import type { webcrypto } from \"node:crypto\";\nimport type { Id, LayoutOps, Prefix } from \"../../types.js\";\nimport { timingSafeEqual } from \"../_kernel/crypto.js\";\nimport { payloadBytesFromId, toWireId } from \"../../wire/envelope.js\";\nimport { payloadBase32Length, payloadByteLength } from \"../../wire/invariants.js\";\nimport {\n readTimestampMsFromBase32Suffix,\n timestampByteLength,\n writeTimestamp,\n} from \"../../wire/timestamp-bytes.js\";\n\nconst randomByteLength = 5;\nconst tagByteLength = 5;\nconst randomOffset = timestampByteLength; // 6\nconst tagOffset = randomOffset + randomByteLength; // 11\nconst signedContentByteLength = randomOffset + randomByteLength; // 11 (ts6 ‖ rand5)\n\nasync function computeTag(\n hmacKey: webcrypto.CryptoKey,\n brandBytes: Uint8Array,\n signedContent: Uint8Array,\n): Promise<Uint8Array> {\n const message = new Uint8Array(brandBytes.length + signedContent.length);\n message.set(brandBytes, 0);\n message.set(signedContent, brandBytes.length);\n const signature = new Uint8Array(\n await crypto.subtle.sign(\"HMAC\", hmacKey, message as Uint8Array<ArrayBuffer>),\n );\n return signature.subarray(0, tagByteLength);\n}\n\nexport function createSignedTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n brand: Brand,\n rng: (target: Uint8Array) => void,\n hmacKeys: readonly webcrypto.CryptoKey[],\n): LayoutOps<Brand> & {\n generateAt(ms: number): Promise<Id<Brand>>;\n tryVerify(id: Id<Brand>): Promise<boolean>;\n extractTimestamp(id: Id<Brand>): Date;\n minIdForTime(ms: number): Id<Brand>;\n maxIdForTime(ms: number): Id<Brand>;\n} {\n const signKey = hmacKeys[0]!;\n const brandBytes = new TextEncoder().encode(brand);\n const syncBuffer = new Uint8Array(payloadByteLength);\n\n return {\n generateAt: async (ms: number): Promise<Id<Brand>> => {\n const buffer = new Uint8Array(payloadByteLength);\n writeTimestamp(ms, buffer);\n rng(buffer.subarray(randomOffset, tagOffset));\n const tag = await computeTag(\n signKey,\n brandBytes,\n buffer.subarray(0, signedContentByteLength),\n );\n buffer.set(tag, tagOffset);\n return toWireId(prefix, buffer);\n },\n tryVerify: async (id: Id<Brand>): Promise<boolean> => {\n const payload = payloadBytesFromId(prefix, id);\n const storedTag = payload.subarray(tagOffset, payloadByteLength);\n const signedContent = payload.subarray(0, signedContentByteLength);\n for (const hmacKey of hmacKeys) {\n const expected = await computeTag(hmacKey, brandBytes, signedContent);\n if (timingSafeEqual(storedTag, expected)) return true;\n }\n return false;\n },\n extractTimestamp: (id: Id<Brand>): Date =>\n new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length))),\n minIdForTime: (ms: number): Id<Brand> => {\n writeTimestamp(ms, syncBuffer);\n syncBuffer.fill(0x00, randomOffset, payloadByteLength);\n return toWireId(prefix, syncBuffer);\n },\n maxIdForTime: (ms: number): Id<Brand> => {\n writeTimestamp(ms, syncBuffer);\n syncBuffer.fill(0xff, randomOffset, payloadByteLength);\n return toWireId(prefix, syncBuffer);\n },\n exampleWireId: (_ms?: number): Id<Brand> =>\n (prefix + \"0\".repeat(payloadBase32Length)) as Id<Brand>,\n };\n}\n","import type { webcrypto } from \"node:crypto\";\nimport { deriveKey, timingSafeEqual } from \"../_kernel/crypto.js\";\nimport {\n assertValidKeyMaterialByteLength,\n assertValidKeyring,\n decodeKeyMaterial,\n encodeKeyMaterial,\n} from \"../_kernel/key-material.js\";\n\nexport { assertValidKeyring };\n\n/** Wire encoding for signing key raw key bytes (not Crockford base32). */\nexport type SigningKeyFormat = \"hex\" | \"base64url\";\n\nconst hmacInfo = new TextEncoder().encode(\"@smonn/ids/signed/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 `@smonn/ids/signed/hmac`. The underlying `webcrypto.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 keyDigest: Uint8Array;\n hmacKey: webcrypto.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 * `@smonn/ids/signed/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 assertValidKeyMaterialByteLength(bytes.length, \"signing\");\n const [hmacKey, digestBuffer] = await Promise.all([\n deriveKey(bytes, hmacInfo, { name: \"HMAC\", hash: \"SHA-256\", length: 256 }, [\"sign\", \"verify\"]),\n crypto.subtle.digest(\"SHA-256\", bytes as Uint8Array<ArrayBuffer>),\n ]);\n const key = Object.freeze({}) as SigningKey;\n internals.set(key, { keyDigest: new Uint8Array(digestBuffer), 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 return encodeKeyMaterial(bytes, format, \"signing\", \"signing\");\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 return decodeKeyMaterial(encoded, format, \"signing\", \"signing\");\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 return timingSafeEqual(getSigningKeyInternals(a).keyDigest, getSigningKeyInternals(b).keyDigest);\n}\n\n/**\n * Returns the derived HMAC webcrypto.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): webcrypto.CryptoKey {\n return getSigningKeyInternals(key).hmacKey;\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","import { validateBrand } from \"../_kernel/brand.js\";\nimport { IdsError } from \"../../error.js\";\nimport { createSignedTimestampLayoutOps } from \"./layout.js\";\nimport { registerBrand } from \"../_kernel/registry.js\";\nimport { defaultRng } from \"../_kernel/rng.js\";\nimport type {\n Id,\n JsonSchema,\n ParseError,\n ParseResult,\n Prefix,\n StandardSchemaProps,\n ValidBrand,\n} from \"../../types.js\";\nimport { wireMethods } from \"../../wire/codec-shell.js\";\nimport {\n assertValidKeyring,\n decodeSigningKey,\n encodeSigningKey,\n getSigningKeyHmacKey,\n importSigningKey,\n signingKeysEqual,\n type SigningKey,\n type SigningKeyFormat,\n} from \"./key.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 } from \"../../error.js\";\nexport {\n decodeSigningKey,\n encodeSigningKey,\n importSigningKey,\n type SigningKey,\n type SigningKeyFormat,\n};\n\n/**\n * Configuration options for a Signed Timestamp codec instance.\n */\nexport type SignedTimestampOptions = {\n /**\n * Non-empty ordered signing keyring. The first entry is current — the only one\n * `generate` / `generateAt` sign with. `verify` / `safeVerify` trial every entry\n * until the tag matches. Duplicate raw secrets are rejected at construction.\n */\n keys: [SigningKey, ...SigningKey[]];\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes 5 random bytes into `target` for the random tail. 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 * Result returned by {@link SignedTimestampCodec.safeVerify}.\n *\n * On success, `id` is the canonical {@link Id}.\n * On failure, `error` is a {@link ParseError} for structural problems or\n * `\"verification_failed\"` when the HMAC tag does not match any entry in the\n * signing keyring.\n */\nexport type SafeVerifyResult<Brand extends string> =\n | { ok: true; id: Id<Brand> }\n | { ok: false; error: ParseError | \"verification_failed\" };\n\n/**\n * Codec returned by {@link createSignedTimestampId}.\n *\n * Keeps the 6-byte millisecond timestamp **readable and sortable** like the\n * Timestamp codec, but replaces half of the 10-byte random tail with a truncated\n * HMAC tag, making IDs **tamper-evident and verifiable without a database lookup**.\n *\n * Byte layout: `ts6 ‖ rand5 ‖ tag5` where the 40-bit tag =\n * `trunc(HMAC-SHA256(hmacKey, brand ‖ ts6 ‖ rand5), 40)`.\n *\n * - Async (HMAC): `generate`, `generateAt`, `verify`, `safeVerify`.\n * - Sync (no key / plaintext timestamp): all other methods.\n */\nexport type SignedTimestampCodec<Brand extends string> = {\n /** Produces a canonical ID signed with the current (first) key. */\n generate(): Promise<Id<Brand>>;\n /**\n * Produces a canonical ID with timestamp from `date`, signed with the current key.\n * Throws on invalid dates.\n */\n generateAt(date: Date): Promise<Id<Brand>>;\n /**\n * Recomputes the HMAC tag across every keyring entry.\n *\n * Throws `IdsError` with `code: \"verification_failed\"` if no entry matches.\n * Tamper of the brand, timestamp bytes, or random bytes all fail here.\n */\n verify(id: Id<Brand>): Promise<void>;\n /**\n * Non-throwing path for untrusted input.\n *\n * Structurally parses `input` first (same rules as {@link safeParse}), then\n * verifies the HMAC tag. Returns `{ ok: false, error }` on any failure —\n * {@link ParseError} for structural problems or `\"verification_failed\"` for tag\n * mismatch — without throwing.\n */\n safeVerify(input: unknown): Promise<SafeVerifyResult<Brand>>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>`.\n * Sync — the 6-byte timestamp is plaintext. Trusts the type; use `safeParse()` at boundaries first.\n *\n * Best-effort: the timestamp is returned **without checking the HMAC tag** — a tampered\n * or unsigned ID yields the attacker-controlled timestamp without error. Call\n * `verify()` / `safeVerify()` first if you need an authenticated timestamp.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /**\n * Tight lower bound sentinel for range scans (`ts(t) ‖ 0x00×10`).\n * **Not verifiable** — carries no valid tag.\n */\n minIdForTime(date: Date): Id<Brand>;\n /**\n * Tight upper bound sentinel for range scans (`ts(t) ‖ 0xff×10`).\n * **Not verifiable** — carries no valid tag.\n */\n maxIdForTime(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical `Id<Brand>` strings.\n * For untrusted input, use `safeParse()` or `safeVerify()` instead.\n */\n is(value: unknown): value is Id<Brand>;\n /** Normalise to canonical form, or throw on parse failure. */\n parse(value: unknown): Id<Brand>;\n /** Normalise to canonical form, or return `{ ok: false, error }`. */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * JSON Schema for the canonical wire form. The `pattern` matches the canonical stored\n * form only and is deliberately stricter than `parse()`/`safeParse()`, which accept\n * uppercase letters and Crockford aliases (`o`/`i`/`l`) before normalising. See ADR-0003.\n */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\n/**\n * Construct a {@link SignedTimestampCodec} for `brand`.\n *\n * `opts.keys` is a non-empty ordered signing keyring — the first entry is current\n * (used by `generate` / `generateAt`); all entries are tried on `verify` /\n * `safeVerify`; duplicate operator secrets are rejected at construction.\n *\n * @example\n * ```ts\n * const key = await importSigningKey(new Uint8Array(32));\n * const usr = createSignedTimestampId(\"usr\", { keys: [key] });\n *\n * const id = await usr.generate(); // Id<\"usr\">\n * await usr.verify(id); // passes\n * usr.extractTimestamp(id); // Date — sync, timestamp is plaintext\n * ```\n */\nexport function createSignedTimestampId<Brand extends string>(\n brand: Brand & ValidBrand<Brand>,\n opts: SignedTimestampOptions,\n): SignedTimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n assertValidKeyring(opts.keys, signingKeysEqual, \"signing\");\n\n const hmacKeys = opts.keys.map(getSigningKeyHmacKey);\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 = createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys);\n\n return {\n generate: () => layout.generateAt(now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n verify: async (id) => {\n const ok = await layout.tryVerify(id);\n if (!ok) throw new IdsError(\"verification_failed\", \"verification failed\");\n },\n safeVerify: async (input) => {\n const parsed = wire.safeParse(input);\n if (!parsed.ok) return parsed;\n const ok = await layout.tryVerify(parsed.id);\n if (!ok) return { ok: false, error: \"verification_failed\" };\n return { ok: true, id: parsed.id };\n },\n extractTimestamp: layout.extractTimestamp,\n minIdForTime: (date: Date) => layout.minIdForTime(date.getTime()),\n maxIdForTime: (date: Date) => layout.maxIdForTime(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":";;;;AAYA,MAAM,gBAAgB;AACtB,MAAM,eAAA;AACN,MAAM,YAAY;AAClB,MAAM,0BAA0B;AAEhC,eAAe,WACb,SACA,YACA,eACqB;CACrB,MAAM,UAAU,IAAI,WAAW,WAAW,SAAS,cAAc,MAAM;CACvE,QAAQ,IAAI,YAAY,CAAC;CACzB,QAAQ,IAAI,eAAe,WAAW,MAAM;CAI5C,OAAO,IAHe,WACpB,MAAM,OAAO,OAAO,KAAK,QAAQ,SAAS,OAAkC,CAE/D,CAAC,CAAC,SAAS,GAAG,aAAa;AAC5C;AAEA,SAAgB,+BACd,QACA,OACA,KACA,UAOA;CACA,MAAM,UAAU,SAAS;CACzB,MAAM,aAAa,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK;CACjD,MAAM,6BAAa,IAAI,WAAA,EAA4B;CAEnD,OAAO;EACL,YAAY,OAAO,OAAmC;GACpD,MAAM,yBAAS,IAAI,WAAA,EAA4B;GAC/C,eAAe,IAAI,MAAM;GACzB,IAAI,OAAO,SAAS,cAAc,SAAS,CAAC;GAC5C,MAAM,MAAM,MAAM,WAChB,SACA,YACA,OAAO,SAAS,GAAG,uBAAuB,CAC5C;GACA,OAAO,IAAI,KAAK,SAAS;GACzB,OAAO,SAAS,QAAQ,MAAM;EAChC;EACA,WAAW,OAAO,OAAoC;GACpD,MAAM,UAAU,mBAAmB,QAAQ,EAAE;GAC7C,MAAM,YAAY,QAAQ,SAAS,WAAA,EAA4B;GAC/D,MAAM,gBAAgB,QAAQ,SAAS,GAAG,uBAAuB;GACjE,KAAK,MAAM,WAAW,UAEpB,IAAI,gBAAgB,WAAW,MADR,WAAW,SAAS,YAAY,aAAa,CAC7B,GAAG,OAAO;GAEnD,OAAO;EACT;EACA,mBAAmB,OACjB,IAAI,KAAK,gCAAgC,GAAG,MAAM,OAAO,MAAM,CAAC,CAAC;EACnE,eAAe,OAA0B;GACvC,eAAe,IAAI,UAAU;GAC7B,WAAW,KAAK,GAAM,cAAA,EAA+B;GACrD,OAAO,SAAS,QAAQ,UAAU;EACpC;EACA,eAAe,OAA0B;GACvC,eAAe,IAAI,UAAU;GAC7B,WAAW,KAAK,KAAM,cAAA,EAA+B;GACrD,OAAO,SAAS,QAAQ,UAAU;EACpC;EACA,gBAAgB,QACb,SAAS,IAAI,OAAO,mBAAmB;CAC5C;AACF;;;ACvEA,MAAM,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,wBAAwB;AAyBlE,MAAM,4BAAY,IAAI,QAAyC;;;;;;;;;;;;AAa/D,eAAsB,iBAAiB,OAAwC;CAC7E,iCAAiC,MAAM,QAAQ,SAAS;CACxD,MAAM,CAAC,SAAS,gBAAgB,MAAM,QAAQ,IAAI,CAChD,UAAU,OAAO,UAAU;EAAE,MAAM;EAAQ,MAAM;EAAW,QAAQ;CAAI,GAAG,CAAC,QAAQ,QAAQ,CAAC,GAC7F,OAAO,OAAO,OAAO,WAAW,KAAgC,CAClE,CAAC;CACD,MAAM,MAAM,OAAO,OAAO,CAAC,CAAC;CAC5B,UAAU,IAAI,KAAK;EAAE,WAAW,IAAI,WAAW,YAAY;EAAG;CAAQ,CAAC;CACvE,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,iBAAiB,OAAmB,QAAkC;CACpF,OAAO,kBAAkB,OAAO,QAAQ,WAAW,SAAS;AAC9D;;;;;;;;;;AAWA,SAAgB,iBAAiB,SAAiB,QAAsC;CACtF,OAAO,kBAAkB,SAAS,QAAQ,WAAW,SAAS;AAChE;;;;;;;AAQA,SAAgB,iBAAiB,GAAe,GAAwB;CACtE,OAAO,gBAAgB,uBAAuB,CAAC,CAAC,CAAC,WAAW,uBAAuB,CAAC,CAAC,CAAC,SAAS;AACjG;;;;;;;AAQA,SAAgB,qBAAqB,KAAsC;CACzE,OAAO,uBAAuB,GAAG,CAAC,CAAC;AACrC;AAEA,SAAS,uBAAuB,KAAsC;CACpE,MAAM,eAAe,UAAU,IAAI,GAAG;CACtC,IAAI,iBAAiB,KAAA,GACnB,MAAM,IAAI,MAAM,qBAAqB;CAEvC,OAAO;AACT;;;;;;;;;;;;;;;;;;;;AC2CA,SAAgB,wBACd,OACA,MAC6B;CAC7B,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAC7C,mBAAmB,KAAK,MAAM,kBAAkB,SAAS;CAEzD,MAAM,WAAW,KAAK,KAAK,IAAI,oBAAoB;CACnD,MAAM,MAAM,KAAK,OAAO,KAAK;CAC7B,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,SAAwB,GAAG,MAAM;CACvC,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,SAAS,+BAA+B,QAAQ,OAAO,KAAK,QAAQ;CAE1E,OAAO;EACL,gBAAgB,OAAO,WAAW,IAAI,CAAC;EACvC,aAAa,SAAe,OAAO,WAAW,KAAK,QAAQ,CAAC;EAC5D,QAAQ,OAAO,OAAO;GAEpB,IAAI,CAAC,MADY,OAAO,UAAU,EAAE,GAC3B,MAAM,IAAI,SAAS,uBAAuB,qBAAqB;EAC1E;EACA,YAAY,OAAO,UAAU;GAC3B,MAAM,SAAS,KAAK,UAAU,KAAK;GACnC,IAAI,CAAC,OAAO,IAAI,OAAO;GAEvB,IAAI,CAAC,MADY,OAAO,UAAU,OAAO,EAAE,GAClC,OAAO;IAAE,IAAI;IAAO,OAAO;GAAsB;GAC1D,OAAO;IAAE,IAAI;IAAM,IAAI,OAAO;GAAG;EACnC;EACA,kBAAkB,OAAO;EACzB,eAAe,SAAe,OAAO,aAAa,KAAK,QAAQ,CAAC;EAChE,eAAe,SAAe,OAAO,aAAa,KAAK,QAAQ,CAAC;EAChE,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,WAAW,KAAK;EAChB,oBAAoB,KAAK,aAAa,OAAO,OAAO,cAAc,CAAC;EACnE,aAAa,KAAK;CACpB;AACF"}
package/dist/signed.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
- import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, r as ParseError, t as Id } from "./types-g7CiQDyE.mjs";
1
+ import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, o as ValidBrand, r as ParseError, t as Id } from "./types-wplmOgOK.mjs";
2
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-Dqyho9vp.mjs";
3
3
 
4
4
  //#region src/codecs/signed/key.d.ts
5
5
  /** Wire encoding for signing key raw key bytes (not Crockford base32). */
@@ -122,6 +122,10 @@ type SignedTimestampCodec<Brand extends string> = {
122
122
  /**
123
123
  * Decodes the creation `Date` from an `Id<Brand>`.
124
124
  * Sync — the 6-byte timestamp is plaintext. Trusts the type; use `safeParse()` at boundaries first.
125
+ *
126
+ * Best-effort: the timestamp is returned **without checking the HMAC tag** — a tampered
127
+ * or unsigned ID yields the attacker-controlled timestamp without error. Call
128
+ * `verify()` / `safeVerify()` first if you need an authenticated timestamp.
125
129
  */
126
130
  extractTimestamp(id: Id<Brand>): Date;
127
131
  /**
@@ -140,7 +144,12 @@ type SignedTimestampCodec<Brand extends string> = {
140
144
  */
141
145
  is(value: unknown): value is Id<Brand>; /** Normalise to canonical form, or throw on parse failure. */
142
146
  parse(value: unknown): Id<Brand>; /** Normalise to canonical form, or return `{ ok: false, error }`. */
143
- safeParse(value: unknown): ParseResult<Brand>; /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */
147
+ safeParse(value: unknown): ParseResult<Brand>;
148
+ /**
149
+ * JSON Schema for the canonical wire form. The `pattern` matches the canonical stored
150
+ * form only and is deliberately stricter than `parse()`/`safeParse()`, which accept
151
+ * uppercase letters and Crockford aliases (`o`/`i`/`l`) before normalising. See ADR-0003.
152
+ */
144
153
  toJsonSchema(): JsonSchema; /** Standard Schema validate entry point. */
145
154
  readonly "~standard": StandardSchemaProps<Brand>;
146
155
  };
@@ -161,7 +170,7 @@ type SignedTimestampCodec<Brand extends string> = {
161
170
  * usr.extractTimestamp(id); // Date — sync, timestamp is plaintext
162
171
  * ```
163
172
  */
164
- declare function createSignedTimestampId<Brand extends string>(brand: Brand, opts: SignedTimestampOptions): SignedTimestampCodec<Brand>;
173
+ declare function createSignedTimestampId<Brand extends string>(brand: Brand & ValidBrand<Brand>, opts: SignedTimestampOptions): SignedTimestampCodec<Brand>;
165
174
  //#endregion
166
175
  export { IdsError, type IdsErrorCode, SafeVerifyResult, SignedTimestampCodec, SignedTimestampOptions, type SigningKey, type SigningKeyFormat, createSignedTimestampId, decodeSigningKey, encodeSigningKey, importSigningKey, isIdsError };
167
176
  //# sourceMappingURL=signed.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"signed.d.mts","names":[],"sources":["../src/codecs/signed/key.ts","../src/codecs/signed/index.ts"],"mappings":";;;;;KAWY,gBAAA;AAAA,cAME,eAAA;;;AANF;AAA2B;;;;AAMzB;AAcd;;;;KAAY,UAAA;EAAA,UACA,eAAA;AAAA;;;;;;;;;;;;iBAqBU,gBAAA,CAAiB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,UAAA;AAoBnE;;;;;;;;;AAAA,iBAAgB,gBAAA,CAAiB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,gBAAA;AAa5D;;;;;;;;;AAAA,iBAAgB,gBAAA,CAAiB,OAAA,UAAiB,MAAA,EAAQ,gBAAA,GAAmB,UAAA;;;;;AA3EjE;KC2BA,sBAAA;EDrBE;;;AAAA;AAcd;ECaE,IAAA,GAAO,UAAA,KAAe,UAAA;EAEtB,GAAA,iBDdU;ECgBV,GAAA,IAAO,MAAA,EAAQ,UAAA,WDKK;ECHpB,mBAAA;AAAA;;;;;;;;;KAWU,gBAAA;EACN,EAAA;EAAU,EAAA,EAAI,EAAA,CAAG,KAAA;AAAA;EACjB,EAAA;EAAW,KAAA,EAAO,UAAA;AAAA;;;;;ADUoC;AAa5D;;;;;;;;KCRY,oBAAA;EDQiE,mECN3E,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA;;;AA1CzB;;EA+CE,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;;;;;;;EAOnC,MAAA,CAAO,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA;;;;;;;;AA1CvB;EAmDA,UAAA,CAAW,KAAA,YAAiB,OAAA,CAAQ,gBAAA,CAAiB,KAAA;EAxC3C;;;;EA6CV,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;EA3CX;;;;EAgDtB,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;EAK7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EArDP;AAAA;AAexB;;EA2CE,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;EAEhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EAE1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;EAEvC,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;;;;;;;;;;;;iBAoB5B,uBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
1
+ {"version":3,"file":"signed.d.mts","names":[],"sources":["../src/codecs/signed/key.ts","../src/codecs/signed/index.ts"],"mappings":";;;;;KAYY,gBAAA;AAAA,cAIE,eAAA;;;AAJF;AAA2B;;;;AAIzB;AAcd;;;;KAAY,UAAA;EAAA,UACA,eAAA;AAAA;;;;;;;;;;;;iBAqBU,gBAAA,CAAiB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,UAAA;AAoBnE;;;;;;;;;AAAA,iBAAgB,gBAAA,CAAiB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,gBAAA;AAa5D;;;;;;;;;AAAA,iBAAgB,gBAAA,CAAiB,OAAA,UAAiB,MAAA,EAAQ,gBAAA,GAAmB,UAAA;;;;;;KC9CjE,sBAAA;ED3B2B;;;;AAIzB;EC6BZ,IAAA,GAAO,UAAA,KAAe,UAAA,KDfZ;ECiBV,GAAA,iBDhBU;ECkBV,GAAA,IAAO,MAAA,EAAQ,UAAA,WDGjB;ECDE,mBAAA;AAAA;;;;;;;;;KAWU,gBAAA;EACN,EAAA;EAAU,EAAA,EAAI,EAAA,CAAG,KAAA;AAAA;EACjB,EAAA;EAAW,KAAA,EAAO,UAAA;AAAA;;;;;;ADQoC;AAa5D;;;;;;;KCNY,oBAAA;EDMiE,mECJ3E,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA;;;;AA1CzB;EA+CE,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;;;;;;;EAOnC,MAAA,CAAO,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA;;;;;;;;;EASvB,UAAA,CAAW,KAAA,YAAiB,OAAA,CAAQ,gBAAA,CAAiB,KAAA;EAxCvD;;;;;;;;EAiDE,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;;;;;EAKjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;AApDP;EAyDtB,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EA1CnB;;;;EA+CV,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;EAEhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EAE1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;;;EAMvC,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;;;;;;;;;;;;iBAoB5B,uBAAA,uBACd,KAAA,EAAO,KAAA,GAAQ,UAAA,CAAW,KAAA,GAC1B,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
package/dist/signed.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-4h2BnlWx.mjs";
2
+ import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-BTz3ZFYE.mjs";
3
3
  export { IdsError, createSignedTimestampId, decodeSigningKey, encodeSigningKey, importSigningKey, isIdsError };
@@ -1,5 +1,5 @@
1
- import { a as toWireId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-DvrTDa65.mjs";
2
- import { a as writeTimestamp, i as readTimestampMsFromBase32Suffix, n as fastTenByteRng } from "./rng-Clos6uC0.mjs";
1
+ import { a as toWireId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-C2NKQEx2.mjs";
2
+ import { a as writeTimestamp, i as readTimestampMsFromBase32Suffix, n as fastTenByteRng } from "./rng-BHFxX1Fc.mjs";
3
3
  //#region src/codecs/timestamp/layout.ts
4
4
  const randomByteLength = 10;
5
5
  /** Writes a 16-byte timestamp-layout payload into codec-owned scratch. */
@@ -35,7 +35,7 @@ function createTimestampLayoutOps(prefix, rng) {
35
35
  return toWireId(prefix, buffer);
36
36
  },
37
37
  exampleWireId: (ms) => {
38
- buildPayload(ms, rng, buffer, randomView);
38
+ buildPayload(ms ?? Date.now(), rng, buffer, randomView);
39
39
  return toWireId(prefix, buffer);
40
40
  }
41
41
  };
@@ -51,6 +51,13 @@ const defaultTimestampOptions = {
51
51
  *
52
52
  * @param brand - Entity type brand validated once at construction.
53
53
  * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.
54
+ * @example
55
+ * ```ts
56
+ * const users = createTimestampId("usr");
57
+ *
58
+ * const id = users.generate(); // Id<"usr">
59
+ * users.extractTimestamp(id); // Date
60
+ * ```
54
61
  */
55
62
  function createTimestampId(brand, opts = {}) {
56
63
  validateBrand(brand);
@@ -71,11 +78,11 @@ function createTimestampId(brand, opts = {}) {
71
78
  extractTimestamp: layout.extractTimestamp,
72
79
  minIdForTime: (date) => layout.minIdForTime(date.getTime()),
73
80
  maxIdForTime: (date) => layout.maxIdForTime(date.getTime()),
74
- toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(options.now())),
81
+ toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),
75
82
  "~standard": wire["~standard"]
76
83
  };
77
84
  }
78
85
  //#endregion
79
86
  export { createTimestampId as t };
80
87
 
81
- //# sourceMappingURL=timestamp-Cg9nRfnK.mjs.map
88
+ //# sourceMappingURL=timestamp-CleAIdZI.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamp-CleAIdZI.mjs","names":[],"sources":["../src/codecs/timestamp/layout.ts","../src/codecs/timestamp/index.ts"],"sourcesContent":["import type { Id, LayoutOps, 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): LayoutOps<Brand> & {\n generateAt(ms: number): Id<Brand>;\n extractTimestamp(id: Id<Brand>): Date;\n minIdForTime(ms: number): Id<Brand>;\n maxIdForTime(ms: number): Id<Brand>;\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 ?? Date.now(), rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n };\n}\n","import { validateBrand } from \"../_kernel/brand.js\";\nimport { createTimestampLayoutOps } from \"./layout.js\";\nimport { registerBrand } from \"../_kernel/registry.js\";\nimport { fastTenByteRng } from \"../_kernel/rng.js\";\nimport type {\n Id,\n JsonSchema,\n ParseResult,\n Prefix,\n StandardSchemaProps,\n ValidBrand,\n} 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 * Best-effort: decodes the timestamp bytes in the payload without any additional\n * verification. An ID that bypassed `safeParse()` (e.g. via a type assertion)\n * may return a plausible-looking but incorrect `Date`.\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 /**\n * JSON Schema for the canonical wire form. The `pattern` matches the canonical stored\n * form only and is deliberately stricter than `parse()`/`safeParse()`, which accept\n * uppercase letters and Crockford aliases (`o`/`i`/`l`) before normalising. See ADR-0003.\n */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\nconst defaultTimestampOptions: ResolvedTimestampOptions = {\n now: Date.now,\n // crypto.randomUUID harvest fast path (~7× faster than crypto.getRandomValues);\n // see fastTenByteRng. The Reverse Timestamp codec shares the identical 10-byte\n // random tail and the same default.\n rng: fastTenByteRng,\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 * @example\n * ```ts\n * const users = createTimestampId(\"usr\");\n *\n * const id = users.generate(); // Id<\"usr\">\n * users.extractTimestamp(id); // Date\n * ```\n */\nexport function createTimestampId<Brand extends string>(\n brand: Brand & ValidBrand<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()),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":";;;AASA,MAAM,mBAAA;;AAGN,SAAS,aACP,IACA,KACA,QACA,YACM;CACN,eAAe,IAAI,MAAM;CACzB,IAAI,UAAU;AAChB;;AAGA,SAAS,qBACP,IACA,MACA,QACA,YACM;CACN,eAAe,IAAI,MAAM;CACzB,WAAW,KAAK,IAAI;AACtB;;AAGA,SAAS,uBAA6C,QAAuB,IAAqB;CAChG,OAAO,IAAI,KAAK,gCAAgC,GAAG,MAAM,OAAO,MAAM,CAAC,CAAC;AAC1E;;AAGA,SAAgB,yBACd,QACA,KAMA;CAMA,MAAM,yBAAS,IAAI,WAAA,EAA4B;CAC/C,MAAM,aAAa,IAAI,WAAW,OAAO,QAAA,GAA6B,gBAAgB;CAEtF,OAAO;EACL,aAAa,OAA0B;GACrC,aAAa,IAAI,KAAK,QAAQ,UAAU;GACxC,OAAO,SAAS,QAAQ,MAAM;EAChC;EACA,mBAAmB,OAAwB,uBAAuB,QAAQ,EAAE;EAC5E,eAAe,OAA0B;GACvC,qBAAqB,IAAI,GAAM,QAAQ,UAAU;GACjD,OAAO,SAAS,QAAQ,MAAM;EAChC;EACA,eAAe,OAA0B;GACvC,qBAAqB,IAAI,KAAM,QAAQ,UAAU;GACjD,OAAO,SAAS,QAAQ,MAAM;EAChC;EACA,gBAAgB,OAA2B;GACzC,aAAa,MAAM,KAAK,IAAI,GAAG,KAAK,QAAQ,UAAU;GACtD,OAAO,SAAS,QAAQ,MAAM;EAChC;CACF;AACF;;;ACGA,MAAM,0BAAoD;CACxD,KAAK,KAAK;CAIV,KAAK;AACP;;;;;;;;;;;;;;AAeA,SAAgB,kBACd,OACA,OAAyB,CAAC,GACH;CACvB,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAE7C,MAAM,UAAU;EACd,KAAK,KAAK,OAAO,wBAAwB;EACzC,KAAK,KAAK,OAAO,wBAAwB;CAC3C;CAEA,MAAM,SAAwB,GAAG,MAAM;CACvC,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,SAAS,yBAAyB,QAAQ,QAAQ,GAAG;CAE3D,OAAO;EACL,gBAAgB,OAAO,WAAW,QAAQ,IAAI,CAAC;EAC/C,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,CAAC;EACnE,aAAa,KAAK;CACpB;AACF"}
@@ -1,5 +1,5 @@
1
- import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
- import { n as IdColumnCodec } from "./adapter-types-CdYJM6Sf.mjs";
1
+ import { n as IdColumnCodec } from "./adapter-types-CIc-4O-P.mjs";
2
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-Dqyho9vp.mjs";
3
3
  import { ValueTransformer } from "typeorm";
4
4
 
5
5
  //#region src/adapters/typeorm.d.ts
@@ -1 +1 @@
1
- {"version":3,"file":"typeorm.d.mts","names":[],"sources":["../src/adapters/typeorm.ts"],"mappings":";;;;;;;;;;;;;;;;;;AA0CkF;;;;;;;;;;;;;;;;;;;iBAAlE,aAAA,uBAAoC,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,gBAAA"}
1
+ {"version":3,"file":"typeorm.d.mts","names":[],"sources":["../src/adapters/typeorm.ts"],"mappings":";;;;;AAyCA;;;;;;;;;;;;;;AAAkF;;;;;;;;;;;;;;;;;;AAAlF,iBAAgB,aAAA,uBAAoC,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,gBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"typeorm.mjs","names":[],"sources":["../src/adapters/typeorm.ts"],"sourcesContent":["import type { ValueTransformer } from \"typeorm\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } 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\nexport type { IdColumnCodec };\n\n/**\n * TypeORM column transformer for `Id<Brand>`.\n *\n * Returns a `ValueTransformer` object suitable for use in a TypeORM `@Column`\n * decorator's `transformer` option.\n *\n * **Write path (`to`):** passes the `Id<Brand>` directly to the database — it is\n * already the canonical string form.\n *\n * **Read path (`from`):** normalises the raw database value via `codec.safeParse()`.\n * Throws `IdsError` with code `\"invalid_id\"` if the value does not parse as a valid\n * `Id<Brand>`.\n *\n * **TypeORM branding caveat:** TypeORM cannot brand a generated entity field type at\n * the schema level. Annotate the entity field explicitly: `id!: Id<\"usr\">`.\n *\n * @example\n * ```ts\n * import { idTransformer } from \"@smonn/ids/typeorm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n * import { Column, Entity } from \"typeorm\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Entity()\n * class User {\n * @Column({ type: \"text\", transformer: idTransformer(usr) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer {\n return {\n to(value: Id<Brand>): string {\n return value;\n },\n from(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,cAAoC,OAA+C;CACjG,OAAO;EACL,GAAG,OAA0B;GAC3B,OAAO;EACT;EACA,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF"}
1
+ {"version":3,"file":"typeorm.mjs","names":[],"sources":["../src/adapters/typeorm.ts"],"sourcesContent":["import type { ValueTransformer } from \"typeorm\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } 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 } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * TypeORM column transformer for `Id<Brand>`.\n *\n * Returns a `ValueTransformer` object suitable for use in a TypeORM `@Column`\n * decorator's `transformer` option.\n *\n * **Write path (`to`):** passes the `Id<Brand>` directly to the database — it is\n * already the canonical string form.\n *\n * **Read path (`from`):** normalises the raw database value via `codec.safeParse()`.\n * Throws `IdsError` with code `\"invalid_id\"` if the value does not parse as a valid\n * `Id<Brand>`.\n *\n * **TypeORM branding caveat:** TypeORM cannot brand a generated entity field type at\n * the schema level. Annotate the entity field explicitly: `id!: Id<\"usr\">`.\n *\n * @example\n * ```ts\n * import { idTransformer } from \"@smonn/ids/typeorm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n * import { Column, Entity } from \"typeorm\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Entity()\n * class User {\n * @Column({ type: \"text\", transformer: idTransformer(usr) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer {\n return {\n to(value: Id<Brand>): string {\n return value;\n },\n from(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,cAAoC,OAA+C;CACjG,OAAO;EACL,GAAG,OAA0B;GAC3B,OAAO;EACT;EACA,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF"}
@@ -1,9 +1,26 @@
1
1
  //#region src/types.d.ts
2
+ type LowerChar = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z";
3
+ /**
4
+ * Validator-style conditional type: resolves to `S` when `S` is exactly three
5
+ * lowercase `a–z` characters, and to `never` otherwise.
6
+ *
7
+ * When passed the generic `string` type it resolves to `string`, so existing
8
+ * dynamic-brand call sites (e.g. CLI, ORM adapters) are unaffected.
9
+ *
10
+ * Apply as a parameter intersection on codec constructors:
11
+ * ```ts
12
+ * function createTimestampId<Brand extends string>(brand: Brand & ValidBrand<Brand>, ...)
13
+ * ```
14
+ * TypeScript then validates the specific brand literal at each call site without
15
+ * materialising the 17 576-member all-brands union.
16
+ */
17
+ type ValidBrand<S extends string> = string extends S ? S : S extends `${infer _A extends LowerChar}${infer _B extends LowerChar}${infer _C extends LowerChar}` ? S : never;
2
18
  /** The brand plus trailing separator — e.g. `usr_` for brand `usr`. */
3
19
  type Prefix<Brand extends string> = `${Brand}_`;
20
+ declare const idBrand: unique symbol;
4
21
  /** A canonical branded ID string for `Brand`. Produced by `generate()` and `safeParse()`. */
5
22
  type Id<Brand extends string> = `${Prefix<Brand>}${string}` & {
6
- readonly __brand: Brand;
23
+ readonly [idBrand]: Brand;
7
24
  };
8
25
  /** Parse failure reason returned by `safeParse()`. */
9
26
  type ParseError = "not_string" | "invalid_prefix" | "invalid_base32";
@@ -42,5 +59,5 @@ type StandardSchemaProps<Brand extends string> = {
42
59
  };
43
60
  };
44
61
  //#endregion
45
- export { StandardSchemaProps as a, ParseResult as i, JsonSchema as n, ParseError as r, Id as t };
46
- //# sourceMappingURL=types-g7CiQDyE.d.mts.map
62
+ export { StandardSchemaProps as a, ParseResult as i, JsonSchema as n, ValidBrand as o, ParseError as r, Id as t };
63
+ //# sourceMappingURL=types-wplmOgOK.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-wplmOgOK.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";KAAK,SAAA;;;;;AAAA;AA0CL;;;;;;;;;KAAY,UAAA,oCAA8C,CAAA,GACtD,CAAA,GACA,CAAA,6BAA8B,SAAA,oBAA6B,SAAA,oBAA6B,SAAA,KACtF,CAAA;;KAIM,MAAA,4BAAkC,KAAA;AAAA,cAEhC,OAAA;;KAGF,EAAA,4BAA8B,MAAA,CAAO,KAAA;EAAA,UACrC,OAAA,GAAU,KAAA;AAAA;;KAIV,UAAA;;KAGA,WAAA;EACN,EAAA;EAAU,EAAA,EAAI,EAAA,CAAG,KAAA;AAAA;EACjB,EAAA;EAAW,KAAA,EAAO,UAAA;AAAA;;KAGZ,UAAA;EAAA,SACD,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;EAAA,SACA,OAAA;AAAA;;KASC,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"}