@smonn/ids 0.8.0 → 0.9.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 (71) hide show
  1. package/README.md +210 -32
  2. package/dist/adapter-types-BY-wrYYB.mjs +27 -0
  3. package/dist/adapter-types-BY-wrYYB.mjs.map +1 -0
  4. package/dist/adapter-types-unUcmMXC.d.mts +20 -0
  5. package/dist/adapter-types-unUcmMXC.d.mts.map +1 -0
  6. package/dist/cli.mjs +342 -71
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/{codec-shell-DH-UO4UR.mjs → codec-shell-CW2sD6BU.mjs} +6 -5
  9. package/dist/codec-shell-CW2sD6BU.mjs.map +1 -0
  10. package/dist/drizzle.d.mts +33 -2
  11. package/dist/drizzle.d.mts.map +1 -0
  12. package/dist/drizzle.mjs +2 -3
  13. package/dist/drizzle.mjs.map +1 -1
  14. package/dist/express.d.mts +2 -5
  15. package/dist/express.d.mts.map +1 -1
  16. package/dist/express.mjs +3 -8
  17. package/dist/express.mjs.map +1 -1
  18. package/dist/fastify.d.mts +2 -5
  19. package/dist/fastify.d.mts.map +1 -1
  20. package/dist/fastify.mjs +3 -8
  21. package/dist/fastify.mjs.map +1 -1
  22. package/dist/hono.d.mts +2 -5
  23. package/dist/hono.d.mts.map +1 -1
  24. package/dist/hono.mjs +3 -8
  25. package/dist/hono.mjs.map +1 -1
  26. package/dist/index.mjs +1 -1
  27. package/dist/key-material-gOnqTNoV.mjs +137 -0
  28. package/dist/key-material-gOnqTNoV.mjs.map +1 -0
  29. package/dist/kysely.d.mts +1 -1
  30. package/dist/kysely.mjs +2 -3
  31. package/dist/kysely.mjs.map +1 -1
  32. package/dist/{opaque-uvjOFY_0.mjs → opaque-BpqxV8oB.mjs} +12 -48
  33. package/dist/opaque-BpqxV8oB.mjs.map +1 -0
  34. package/dist/opaque.d.mts +8 -0
  35. package/dist/opaque.d.mts.map +1 -1
  36. package/dist/opaque.mjs +1 -1
  37. package/dist/prisma.d.mts +4 -18
  38. package/dist/prisma.d.mts.map +1 -1
  39. package/dist/prisma.mjs +3 -5
  40. package/dist/prisma.mjs.map +1 -1
  41. package/dist/{reverse-BgFU6JHw.mjs → reverse-d5uEoIET.mjs} +5 -7
  42. package/dist/reverse-d5uEoIET.mjs.map +1 -0
  43. package/dist/reverse.d.mts.map +1 -1
  44. package/dist/reverse.mjs +1 -1
  45. package/dist/rng-CPJOx_nE.mjs +9 -0
  46. package/dist/rng-CPJOx_nE.mjs.map +1 -0
  47. package/dist/signed-BnRSC03a.mjs +207 -0
  48. package/dist/signed-BnRSC03a.mjs.map +1 -0
  49. package/dist/signed.d.mts.map +1 -1
  50. package/dist/signed.mjs +1 -255
  51. package/dist/{timestamp-B5_UCzc6.mjs → timestamp-BbZL8hwg.mjs} +5 -5
  52. package/dist/{timestamp-B5_UCzc6.mjs.map → timestamp-BbZL8hwg.mjs.map} +1 -1
  53. package/dist/{timestamp-bytes-BBY7JI33.mjs → timestamp-bytes-DoFjLjDp.mjs} +3 -2
  54. package/dist/timestamp-bytes-DoFjLjDp.mjs.map +1 -0
  55. package/dist/{wrapped-0vL72Nje.mjs → wrapped-BI9UXnAm.mjs} +33 -62
  56. package/dist/wrapped-BI9UXnAm.mjs.map +1 -0
  57. package/dist/wrapped.d.mts.map +1 -1
  58. package/dist/wrapped.mjs +1 -1
  59. package/package.json +5 -5
  60. package/dist/adapter-types-oHCCSgOO.d.mts +0 -12
  61. package/dist/adapter-types-oHCCSgOO.d.mts.map +0 -1
  62. package/dist/bytes-lhzKVaBV.mjs +0 -53
  63. package/dist/bytes-lhzKVaBV.mjs.map +0 -1
  64. package/dist/codec-shell-DH-UO4UR.mjs.map +0 -1
  65. package/dist/drizzle-CeSni5PB.d.mts +0 -44
  66. package/dist/drizzle-CeSni5PB.d.mts.map +0 -1
  67. package/dist/opaque-uvjOFY_0.mjs.map +0 -1
  68. package/dist/reverse-BgFU6JHw.mjs.map +0 -1
  69. package/dist/signed.mjs.map +0 -1
  70. package/dist/timestamp-bytes-BBY7JI33.mjs.map +0 -1
  71. package/dist/wrapped-0vL72Nje.mjs.map +0 -1
@@ -0,0 +1,207 @@
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-CW2sD6BU.mjs";
3
+ import { n as readTimestampMsFromBase32Suffix, r as writeTimestamp } from "./timestamp-bytes-DoFjLjDp.mjs";
4
+ import { i as encodeKeyMaterial, n as assertValidKeyring, r as decodeKeyMaterial, t as assertValidKeyMaterialByteLength } from "./key-material-gOnqTNoV.mjs";
5
+ import { t as defaultRng } from "./rng-CPJOx_nE.mjs";
6
+ const tagByteLength = 5;
7
+ const randomOffset = 6;
8
+ const tagOffset = 11;
9
+ const signedContentByteLength = 11;
10
+ async function computeTag(hmacKey, brandBytes, signedContent) {
11
+ const message = new Uint8Array(brandBytes.length + signedContent.length);
12
+ message.set(brandBytes, 0);
13
+ message.set(signedContent, brandBytes.length);
14
+ return new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, message)).subarray(0, tagByteLength);
15
+ }
16
+ function tagsEqual(a, b) {
17
+ /* v8 ignore next -- defensive guard; both call sites always pass tagByteLength-byte arrays */
18
+ if (a.length !== b.length) return false;
19
+ let diff = 0;
20
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
21
+ return diff === 0;
22
+ }
23
+ function createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys) {
24
+ const signKey = hmacKeys[0];
25
+ const brandBytes = new TextEncoder().encode(brand);
26
+ const syncBuffer = /* @__PURE__ */ new Uint8Array(16);
27
+ return {
28
+ generateAt: async (ms) => {
29
+ const buffer = /* @__PURE__ */ new Uint8Array(16);
30
+ writeTimestamp(ms, buffer);
31
+ rng(buffer.subarray(randomOffset, tagOffset));
32
+ const tag = await computeTag(signKey, brandBytes, buffer.subarray(0, signedContentByteLength));
33
+ buffer.set(tag, tagOffset);
34
+ return toWireId(prefix, buffer);
35
+ },
36
+ tryVerify: async (id) => {
37
+ const payload = payloadBytesFromId(prefix, id);
38
+ const storedTag = payload.subarray(tagOffset, 16);
39
+ const signedContent = payload.subarray(0, signedContentByteLength);
40
+ for (const hmacKey of hmacKeys) if (tagsEqual(storedTag, await computeTag(hmacKey, brandBytes, signedContent))) return true;
41
+ return false;
42
+ },
43
+ extractTimestamp: (id) => new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length))),
44
+ minIdForTime: (ms) => {
45
+ writeTimestamp(ms, syncBuffer);
46
+ syncBuffer.fill(0, randomOffset, 16);
47
+ return toWireId(prefix, syncBuffer);
48
+ },
49
+ maxIdForTime: (ms) => {
50
+ writeTimestamp(ms, syncBuffer);
51
+ syncBuffer.fill(255, randomOffset, 16);
52
+ return toWireId(prefix, syncBuffer);
53
+ },
54
+ exampleWireId: () => prefix + "0".repeat(payloadBase32Length)
55
+ };
56
+ }
57
+ //#endregion
58
+ //#region src/signing-key.ts
59
+ const hmacInfo = new TextEncoder().encode("ids/signed-timestamp/hmac");
60
+ const SHA256_DIGEST_BYTES = 32;
61
+ const internals = /* @__PURE__ */ new WeakMap();
62
+ /**
63
+ * Import raw operator key material into a {@link SigningKey} handle.
64
+ *
65
+ * Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label
66
+ * `ids/signed-timestamp/hmac`. Accepts 16, 24, or 32 bytes. To store or
67
+ * transport key material, use {@link encodeSigningKey} / {@link decodeSigningKey}
68
+ * (`"hex"` or `"base64url"` — not Crockford base32).
69
+ *
70
+ * @param bytes - 16, 24, or 32 raw key bytes.
71
+ * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
72
+ */
73
+ async function importSigningKey(bytes) {
74
+ assertValidKeyMaterialByteLength(bytes.length, "signing");
75
+ const [hmacKey, digestBuffer] = await Promise.all([deriveHmacKey(bytes), crypto.subtle.digest("SHA-256", bytes)]);
76
+ const key = Object.freeze({});
77
+ internals.set(key, {
78
+ keyDigest: new Uint8Array(digestBuffer),
79
+ hmacKey
80
+ });
81
+ return key;
82
+ }
83
+ /**
84
+ * Encode raw signing operator key material for storage in env vars or secret managers.
85
+ *
86
+ * Supports `"hex"` (lowercase) and `"base64url"`. Output round-trips through
87
+ * {@link decodeSigningKey} back to the original bytes.
88
+ *
89
+ * @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
90
+ * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
91
+ */
92
+ function encodeSigningKey(bytes, format) {
93
+ return encodeKeyMaterial(bytes, format, "signing", "signing");
94
+ }
95
+ /**
96
+ * Decode key material emitted by {@link encodeSigningKey} back to raw bytes.
97
+ *
98
+ * The result can be passed directly to {@link importSigningKey}.
99
+ *
100
+ * @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
101
+ * @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.
102
+ * @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.
103
+ */
104
+ function decodeSigningKey(encoded, format) {
105
+ return decodeKeyMaterial(encoded, format, "signing", "signing");
106
+ }
107
+ /**
108
+ * Returns true when two handles were imported from the same raw key material.
109
+ *
110
+ * Uses a constant-time comparison so duplicate detection over key material does
111
+ * not leak the position of the first differing byte through a timing side channel.
112
+ */
113
+ function signingKeysEqual(a, b) {
114
+ const aDigest = getSigningKeyInternals(a).keyDigest;
115
+ const bDigest = getSigningKeyInternals(b).keyDigest;
116
+ let diff = 0;
117
+ for (let i = 0; i < SHA256_DIGEST_BYTES; i++) diff |= aDigest[i] ^ bDigest[i];
118
+ return diff === 0;
119
+ }
120
+ /**
121
+ * Returns the derived HMAC CryptoKey held inside the handle.
122
+ *
123
+ * Intentional module-internal escape hatch for codec implementations (e.g. `createSignedTimestampId`).
124
+ * Not re-exported from `@smonn/ids/signed`; external callers cannot reach this.
125
+ */
126
+ function getSigningKeyHmacKey(key) {
127
+ return getSigningKeyInternals(key).hmacKey;
128
+ }
129
+ function getSigningKeyInternals(key) {
130
+ const keyInternals = internals.get(key);
131
+ if (keyInternals === void 0) throw new Error("invalid signing key");
132
+ return keyInternals;
133
+ }
134
+ async function deriveHmacKey(bytes) {
135
+ const base = await crypto.subtle.importKey("raw", bytes, "HKDF", false, ["deriveKey"]);
136
+ return crypto.subtle.deriveKey({
137
+ name: "HKDF",
138
+ hash: "SHA-256",
139
+ salt: /* @__PURE__ */ new Uint8Array(),
140
+ info: hmacInfo
141
+ }, base, {
142
+ name: "HMAC",
143
+ hash: "SHA-256",
144
+ length: 256
145
+ }, false, ["sign", "verify"]);
146
+ }
147
+ //#endregion
148
+ //#region src/signed.ts
149
+ /**
150
+ * Construct a {@link SignedTimestampCodec} for `brand`.
151
+ *
152
+ * `opts.keys` is a non-empty ordered signing keyring — the first entry is current
153
+ * (used by `generate` / `generateAt`); all entries are tried on `verify` /
154
+ * `safeVerify`; duplicate operator secrets are rejected at construction.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * const key = await importSigningKey(new Uint8Array(32));
159
+ * const usr = createSignedTimestampId("usr", { keys: [key] });
160
+ *
161
+ * const id = await usr.generate(); // Id<"usr">
162
+ * await usr.verify(id); // passes
163
+ * usr.extractTimestamp(id); // Date — sync, timestamp is plaintext
164
+ * ```
165
+ */
166
+ function createSignedTimestampId(brand, opts) {
167
+ validateBrand(brand);
168
+ registerBrand(brand, opts.allowDuplicateBrand);
169
+ assertValidKeyring(opts.keys, signingKeysEqual, "signing");
170
+ const hmacKeys = opts.keys.map(getSigningKeyHmacKey);
171
+ const now = opts.now ?? Date.now;
172
+ const rng = opts.rng ?? defaultRng;
173
+ const prefix = `${brand}_`;
174
+ const wire = wireMethods(prefix);
175
+ const layout = createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys);
176
+ return {
177
+ generate: () => layout.generateAt(now()),
178
+ generateAt: (date) => layout.generateAt(date.getTime()),
179
+ verify: async (id) => {
180
+ if (!await layout.tryVerify(id)) throw new IdsError("verification_failed", "verification failed");
181
+ },
182
+ safeVerify: async (input) => {
183
+ const parsed = wire.safeParse(input);
184
+ if (!parsed.ok) return parsed;
185
+ if (!await layout.tryVerify(parsed.id)) return {
186
+ ok: false,
187
+ error: "verification_failed"
188
+ };
189
+ return {
190
+ ok: true,
191
+ id: parsed.id
192
+ };
193
+ },
194
+ extractTimestamp: layout.extractTimestamp,
195
+ minIdForTime: (date) => layout.minIdForTime(date.getTime()),
196
+ maxIdForTime: (date) => layout.maxIdForTime(date.getTime()),
197
+ is: wire.is,
198
+ parse: wire.parse,
199
+ safeParse: wire.safeParse,
200
+ toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),
201
+ "~standard": wire["~standard"]
202
+ };
203
+ }
204
+ //#endregion
205
+ export { importSigningKey as i, decodeSigningKey as n, encodeSigningKey as r, createSignedTimestampId as t };
206
+
207
+ //# sourceMappingURL=signed-BnRSC03a.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signed-BnRSC03a.mjs","names":[],"sources":["../src/layouts/signed-timestamp.ts","../src/signing-key.ts","../src/signed.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.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: 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\nfunction tagsEqual(a: Uint8Array, b: Uint8Array): boolean {\n /* v8 ignore next -- defensive guard; both call sites always pass tagByteLength-byte arrays */\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;\n return diff === 0;\n}\n\nexport function createSignedTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n brand: Brand,\n rng: (target: Uint8Array) => void,\n hmacKeys: readonly CryptoKey[],\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 (tagsEqual(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: (): Id<Brand> => (prefix + \"0\".repeat(payloadBase32Length)) as Id<Brand>,\n };\n}\n","import {\n assertValidKeyMaterialByteLength,\n assertValidKeyring,\n decodeKeyMaterial,\n encodeKeyMaterial,\n} from \"./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(\"ids/signed-timestamp/hmac\");\n\nconst SHA256_DIGEST_BYTES = 32;\n\ndeclare const signingKeyBrand: unique symbol;\n\n/**\n * Opaque imported handle for one operator signing key.\n *\n * Holds a single HMAC-SHA-256 key derived via HKDF under the domain-separation\n * label `ids/signed-timestamp/hmac`. The underlying `CryptoKey` is held\n * internally and never exposed to callers. Obtain handles via\n * {@link importSigningKey} and pass them to `createSignedTimestampId` as the\n * `keys` signing keyring.\n *\n * Distinct from both the **Opaque key** and the **Wrapping key** — the same\n * raw key material must not silently serve multiple codecs without an explicit import.\n */\nexport type SigningKey = {\n readonly [signingKeyBrand]: \"SigningKey\";\n};\n\ntype SigningKeyInternals = {\n keyDigest: Uint8Array;\n hmacKey: CryptoKey;\n};\n\nconst internals = new WeakMap<SigningKey, SigningKeyInternals>();\n\n/**\n * Import raw operator key material into a {@link SigningKey} handle.\n *\n * Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label\n * `ids/signed-timestamp/hmac`. Accepts 16, 24, or 32 bytes. To store or\n * transport key material, use {@link encodeSigningKey} / {@link decodeSigningKey}\n * (`\"hex\"` or `\"base64url\"` — not Crockford base32).\n *\n * @param bytes - 16, 24, or 32 raw key bytes.\n * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.\n */\nexport async function importSigningKey(bytes: Uint8Array): Promise<SigningKey> {\n assertValidKeyMaterialByteLength(bytes.length, \"signing\");\n const [hmacKey, digestBuffer] = await Promise.all([\n deriveHmacKey(bytes),\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 const aDigest = getSigningKeyInternals(a).keyDigest;\n const bDigest = getSigningKeyInternals(b).keyDigest;\n let diff = 0;\n for (let i = 0; i < SHA256_DIGEST_BYTES; i++) {\n diff |= aDigest[i]! ^ bDigest[i]!;\n }\n return diff === 0;\n}\n\n/**\n * Returns the derived HMAC CryptoKey held inside the handle.\n *\n * Intentional module-internal escape hatch for codec implementations (e.g. `createSignedTimestampId`).\n * Not re-exported from `@smonn/ids/signed`; external callers cannot reach this.\n */\nexport function getSigningKeyHmacKey(key: SigningKey): CryptoKey {\n return getSigningKeyInternals(key).hmacKey;\n}\n\nfunction getSigningKeyInternals(key: SigningKey): SigningKeyInternals {\n const keyInternals = internals.get(key);\n if (keyInternals === undefined) {\n throw new Error(\"invalid signing key\");\n }\n return keyInternals;\n}\n\nasync function deriveHmacKey(bytes: Uint8Array): Promise<CryptoKey> {\n const base = await crypto.subtle.importKey(\n \"raw\",\n bytes as Uint8Array<ArrayBuffer>,\n \"HKDF\",\n false,\n [\"deriveKey\"],\n );\n return crypto.subtle.deriveKey(\n { name: \"HKDF\", hash: \"SHA-256\", salt: new Uint8Array(), info: hmacInfo },\n base,\n { name: \"HMAC\", hash: \"SHA-256\", length: 256 },\n false,\n [\"sign\", \"verify\"],\n );\n}\n","import { validateBrand } from \"./brand.js\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport { createSignedTimestampLayoutOps } from \"./layouts/signed-timestamp.js\";\nimport { registerBrand } from \"./registry.js\";\nimport { defaultRng } from \"./rng.js\";\nimport type {\n Id,\n JsonSchema,\n ParseError,\n ParseResult,\n Prefix,\n StandardSchemaProps,\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 \"./signing-key.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\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 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 /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\n/**\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,\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":";;;;;AAUA,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,SAAS,UAAU,GAAe,GAAwB;;CAExD,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,QAAQ,EAAE,KAAM,EAAE;CACrD,OAAO,SAAS;AAClB;AAEA,SAAgB,+BACd,QACA,OACA,KACA,UACA;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,UAAU,WAAW,MADF,WAAW,SAAS,YAAY,aAAa,CACnC,GAAG,OAAO;GAE7C,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,qBAAiC,SAAS,IAAI,OAAO,mBAAmB;CAC1E;AACF;;;ACxEA,MAAM,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,2BAA2B;AAErE,MAAM,sBAAsB;AAyB5B,MAAM,4BAAY,IAAI,QAAyC;;;;;;;;;;;;AAa/D,eAAsB,iBAAiB,OAAwC;CAC7E,iCAAiC,MAAM,QAAQ,SAAS;CACxD,MAAM,CAAC,SAAS,gBAAgB,MAAM,QAAQ,IAAI,CAChD,cAAc,KAAK,GACnB,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,MAAM,UAAU,uBAAuB,CAAC,CAAC,CAAC;CAC1C,MAAM,UAAU,uBAAuB,CAAC,CAAC,CAAC;CAC1C,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,qBAAqB,KACvC,QAAQ,QAAQ,KAAM,QAAQ;CAEhC,OAAO,SAAS;AAClB;;;;;;;AAQA,SAAgB,qBAAqB,KAA4B;CAC/D,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;AAEA,eAAe,cAAc,OAAuC;CAClE,MAAM,OAAO,MAAM,OAAO,OAAO,UAC/B,OACA,OACA,QACA,OACA,CAAC,WAAW,CACd;CACA,OAAO,OAAO,OAAO,UACnB;EAAE,MAAM;EAAQ,MAAM;EAAW,sBAAM,IAAI,WAAW;EAAG,MAAM;CAAS,GACxE,MACA;EAAE,MAAM;EAAQ,MAAM;EAAW,QAAQ;CAAI,GAC7C,OACA,CAAC,QAAQ,QAAQ,CACnB;AACF;;;;;;;;;;;;;;;;;;;;ACWA,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"signed.d.mts","names":[],"sources":["../src/signing-key.ts","../src/signed.ts"],"mappings":";;;;;KAIY,gBAAA;AAAA,cAME,eAAA;;AANd;;;;AAAY;AAA2B;;;;AAMzB;AAcd;KAAY,UAAA;EAAA,UACA,eAAA;AAAA;AAAA;AAqBZ;;;;;;;;;;AArBY,iBAqBU,gBAAA,CAAiB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,UAAA;;;AAAA;AAiBnE;;;;;;iBAAgB,gBAAA,CAAiB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,gBAAA;;;AAAA;AAgB5D;;;;;;iBAAgB,gBAAA,CAAiB,OAAA,UAAiB,MAAA,EAAQ,gBAAA,GAAmB,UAAA;;;;;AA3EjE;KCiCA,sBAAA;ED3BE;;;AAAA;AAcd;ECmBE,IAAA,GAAO,UAAA,KAAe,UAAA;EAEtB,GAAA,iBDpBU;ECsBV,GAAA,IAAO,MAAA,EAAQ,UAAA,WDDK;ECGpB,mBAAA;AAAA;;;;;;;;;KAWU,gBAAA;EACN,EAAA;EAAU,EAAA,EAAI,EAAA,CAAG,KAAA;AAAA;EACjB,EAAA;EAAW,KAAA,EAAO,UAAA;AAAA;;;;;ADCoC;AAgB5D;;;;;;;;KCFY,oBAAA;EDEiE,mECA3E,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;;;;;;;;;;;;;;;;;;iBAwB5B,uBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
1
+ {"version":3,"file":"signed.d.mts","names":[],"sources":["../src/signing-key.ts","../src/signed.ts"],"mappings":";;;;;KAUY,gBAAA;AAAA,cAME,eAAA;;;;AANF;AAA2B;;;;AAMzB;AAcd;;;KAAY,UAAA;EAAA,UACA,eAAA;AAAA;;;;;;;;;;;;iBAqBU,gBAAA,CAAiB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,UAAA;AAAA;AAoBnE;;;;;;;;AApBmE,iBAoBnD,gBAAA,CAAiB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,gBAAA;AAAA;AAa5D;;;;;;;;AAb4D,iBAa5C,gBAAA,CAAiB,OAAA,UAAiB,MAAA,EAAQ,gBAAA,GAAmB,UAAA;;;;;AA3EjE;KC4BA,sBAAA;EDtBE;;;AAAA;AAcd;ECcE,IAAA,GAAO,UAAA,KAAe,UAAA;EAEtB,GAAA,iBDfU;ECiBV,GAAA,IAAO,MAAA,EAAQ,UAAA,WDIK;ECFpB,mBAAA;AAAA;;;;;;;;;KAWU,gBAAA;EACN,EAAA;EAAU,EAAA,EAAI,EAAA,CAAG,KAAA;AAAA;EACjB,EAAA;EAAW,KAAA,EAAO,UAAA;AAAA;;;;;ADSoC;AAa5D;;;;;;;;KCPY,oBAAA;EDOiE,mECL3E,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"}
package/dist/signed.mjs CHANGED
@@ -1,257 +1,3 @@
1
1
  import { n as isIdsError, 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-DH-UO4UR.mjs";
3
- import { n as readTimestampMsFromBase32Suffix, r as writeTimestamp } from "./timestamp-bytes-BBY7JI33.mjs";
4
- import { i as encodeHex, n as decodeHex, r as encodeBase64Url, t as decodeBase64Url } from "./bytes-lhzKVaBV.mjs";
5
- const tagByteLength = 5;
6
- const randomOffset = 6;
7
- const tagOffset = 11;
8
- const signedContentByteLength = 11;
9
- async function computeTag(hmacKey, brandBytes, signedContent) {
10
- const message = new Uint8Array(brandBytes.length + signedContent.length);
11
- message.set(brandBytes, 0);
12
- message.set(signedContent, brandBytes.length);
13
- return new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, message)).subarray(0, tagByteLength);
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
- function createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys) {
23
- const signKey = hmacKeys[0];
24
- const brandBytes = new TextEncoder().encode(brand);
25
- const syncBuffer = new Uint8Array(16);
26
- return {
27
- generateAt: async (ms) => {
28
- const buffer = new Uint8Array(16);
29
- writeTimestamp(ms, buffer);
30
- rng(buffer.subarray(randomOffset, tagOffset));
31
- const tag = await computeTag(signKey, brandBytes, buffer.subarray(0, signedContentByteLength));
32
- buffer.set(tag, tagOffset);
33
- return toWireId(prefix, buffer);
34
- },
35
- tryVerify: async (id) => {
36
- const payload = payloadBytesFromId(prefix, id);
37
- const storedTag = payload.subarray(tagOffset, 16);
38
- const signedContent = payload.subarray(0, signedContentByteLength);
39
- for (const hmacKey of hmacKeys) if (tagsEqual(storedTag, await computeTag(hmacKey, brandBytes, signedContent))) return true;
40
- return false;
41
- },
42
- extractTimestamp: (id) => new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length))),
43
- minIdForTime: (ms) => {
44
- writeTimestamp(ms, syncBuffer);
45
- syncBuffer.fill(0, randomOffset, 16);
46
- return toWireId(prefix, syncBuffer);
47
- },
48
- maxIdForTime: (ms) => {
49
- writeTimestamp(ms, syncBuffer);
50
- syncBuffer.fill(255, randomOffset, 16);
51
- return toWireId(prefix, syncBuffer);
52
- },
53
- exampleWireId: () => prefix + "0".repeat(payloadBase32Length)
54
- };
55
- }
56
- //#endregion
57
- //#region src/signing-key.ts
58
- const validKeyByteLengths = new Set([
59
- 16,
60
- 24,
61
- 32
62
- ]);
63
- const hmacInfo = new TextEncoder().encode("ids/signed-timestamp/hmac");
64
- const internals = /* @__PURE__ */ new WeakMap();
65
- /**
66
- * Import raw operator key material into a {@link SigningKey} handle.
67
- *
68
- * Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label
69
- * `ids/signed-timestamp/hmac`. Accepts 16, 24, or 32 bytes. To store or
70
- * transport key material, use {@link encodeSigningKey} / {@link decodeSigningKey}
71
- * (`"hex"` or `"base64url"` — not Crockford base32).
72
- *
73
- * @param bytes - 16, 24, or 32 raw key bytes.
74
- * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
75
- */
76
- async function importSigningKey(bytes) {
77
- assertValidKeyByteLength(bytes.length);
78
- const hmacKey = await deriveHmacKey(bytes);
79
- const key = Object.freeze({});
80
- internals.set(key, {
81
- rawBytes: bytes.slice(),
82
- hmacKey
83
- });
84
- return key;
85
- }
86
- /**
87
- * Encode raw signing operator key material for storage in env vars or secret managers.
88
- *
89
- * Supports `"hex"` (lowercase) and `"base64url"`. Output round-trips through
90
- * {@link decodeSigningKey} back to the original bytes.
91
- *
92
- * @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
93
- * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
94
- */
95
- function encodeSigningKey(bytes, format) {
96
- assertSigningKeyFormat(format);
97
- assertValidKeyByteLength(bytes.length);
98
- if (format === "hex") return encodeHex(bytes);
99
- return encodeBase64Url(bytes);
100
- }
101
- /**
102
- * Decode key material emitted by {@link encodeSigningKey} back to raw bytes.
103
- *
104
- * The result can be passed directly to {@link importSigningKey}.
105
- *
106
- * @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
107
- * @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.
108
- * @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.
109
- */
110
- function decodeSigningKey(encoded, format) {
111
- assertSigningKeyFormat(format);
112
- let bytes;
113
- if (format === "hex") {
114
- if (encoded.length === 0 || encoded.length % 2 !== 0) throw new IdsError("invalid_key_encoding", "invalid hex key: length must be a positive even number of characters");
115
- if (!/^[0-9a-fA-F]+$/.test(encoded)) throw new IdsError("invalid_key_encoding", "invalid hex key: expected [0-9a-fA-F] only");
116
- bytes = decodeHex(encoded);
117
- } else try {
118
- bytes = decodeBase64Url(encoded);
119
- } catch {
120
- throw new IdsError("invalid_key_encoding", "invalid base64url key");
121
- }
122
- assertValidKeyByteLength(bytes.length);
123
- return bytes;
124
- }
125
- /**
126
- * Returns true when two handles were imported from the same raw key material.
127
- *
128
- * Uses a constant-time comparison so duplicate detection over key material does
129
- * not leak the position of the first differing byte through a timing side channel.
130
- */
131
- function signingKeysEqual(a, b) {
132
- const aBytes = getSigningKeyInternals(a).rawBytes;
133
- const bBytes = getSigningKeyInternals(b).rawBytes;
134
- if (aBytes.length !== bBytes.length) return false;
135
- let diff = 0;
136
- for (let i = 0; i < aBytes.length; i++) diff |= aBytes[i] ^ bBytes[i];
137
- return diff === 0;
138
- }
139
- /**
140
- * Returns the derived HMAC CryptoKey held inside the handle.
141
- *
142
- * Intentional module-internal escape hatch for codec implementations (e.g. `createSignedTimestampId`).
143
- * Not re-exported from `@smonn/ids/signed`; external callers cannot reach this.
144
- */
145
- function getSigningKeyHmacKey(key) {
146
- return getSigningKeyInternals(key).hmacKey;
147
- }
148
- /**
149
- * Asserts that a signing keyring is non-empty.
150
- * @throws {IdsError} `empty_keyring` if the array is empty.
151
- */
152
- function assertNonEmptySigningKeyring(keys) {
153
- if (keys.length === 0) throw new IdsError("empty_keyring", "signing keyring must contain at least one key");
154
- }
155
- /**
156
- * Asserts that no two entries in the signing keyring share the same raw bytes.
157
- * @throws {IdsError} `duplicate_keyring_entry` if a duplicate is found.
158
- */
159
- function assertNonDuplicateSigningKeys(keys) {
160
- for (let i = 0; i < keys.length; i++) for (let j = i + 1; j < keys.length; j++) if (signingKeysEqual(keys[i], keys[j])) throw new IdsError("duplicate_keyring_entry", "duplicate signing key in keyring");
161
- }
162
- function getSigningKeyInternals(key) {
163
- const keyInternals = internals.get(key);
164
- if (keyInternals === void 0) throw new Error("invalid signing key");
165
- return keyInternals;
166
- }
167
- async function deriveHmacKey(bytes) {
168
- const base = await crypto.subtle.importKey("raw", bytes, "HKDF", false, ["deriveKey"]);
169
- return crypto.subtle.deriveKey({
170
- name: "HKDF",
171
- hash: "SHA-256",
172
- salt: new Uint8Array(),
173
- info: hmacInfo
174
- }, base, {
175
- name: "HMAC",
176
- hash: "SHA-256",
177
- length: 256
178
- }, false, ["sign", "verify"]);
179
- }
180
- function assertValidKeyByteLength(byteLength) {
181
- if (!validKeyByteLengths.has(byteLength)) throw new IdsError("invalid_key_length", `invalid signing key length: expected 16, 24, or 32 bytes, got ${byteLength}`);
182
- }
183
- function assertSigningKeyFormat(format) {
184
- if (format !== "hex" && format !== "base64url") throw new IdsError("invalid_key_format", `invalid signing key format: expected hex or base64url, got '${formatForError(format)}'`);
185
- }
186
- function formatForError(value) {
187
- try {
188
- return String(value);
189
- } catch {
190
- return "[unprintable]";
191
- }
192
- }
193
- //#endregion
194
- //#region src/signed.ts
195
- function defaultRng(target) {
196
- crypto.getRandomValues(target);
197
- }
198
- /**
199
- * Construct a {@link SignedTimestampCodec} for `brand`.
200
- *
201
- * `opts.keys` is a non-empty ordered signing keyring — the first entry is current
202
- * (used by `generate` / `generateAt`); all entries are tried on `verify` /
203
- * `safeVerify`; duplicate operator secrets are rejected at construction.
204
- *
205
- * @example
206
- * ```ts
207
- * const key = await importSigningKey(new Uint8Array(32));
208
- * const usr = createSignedTimestampId("usr", { keys: [key] });
209
- *
210
- * const id = await usr.generate(); // Id<"usr">
211
- * await usr.verify(id); // passes
212
- * usr.extractTimestamp(id); // Date — sync, timestamp is plaintext
213
- * ```
214
- */
215
- function createSignedTimestampId(brand, opts) {
216
- validateBrand(brand);
217
- registerBrand(brand, opts.allowDuplicateBrand);
218
- assertNonEmptySigningKeyring(opts.keys);
219
- assertNonDuplicateSigningKeys(opts.keys);
220
- const hmacKeys = opts.keys.map(getSigningKeyHmacKey);
221
- const now = opts.now ?? Date.now;
222
- const rng = opts.rng ?? defaultRng;
223
- const prefix = `${brand}_`;
224
- const wire = wireMethods(prefix);
225
- const layout = createSignedTimestampLayoutOps(prefix, brand, rng, hmacKeys);
226
- return {
227
- generate: () => layout.generateAt(now()),
228
- generateAt: (date) => layout.generateAt(date.getTime()),
229
- verify: async (id) => {
230
- if (!await layout.tryVerify(id)) throw new IdsError("verification_failed", "verification failed");
231
- },
232
- safeVerify: async (input) => {
233
- const parsed = wire.safeParse(input);
234
- if (!parsed.ok) return parsed;
235
- if (!await layout.tryVerify(parsed.id)) return {
236
- ok: false,
237
- error: "verification_failed"
238
- };
239
- return {
240
- ok: true,
241
- id: parsed.id
242
- };
243
- },
244
- extractTimestamp: layout.extractTimestamp,
245
- minIdForTime: (date) => layout.minIdForTime(date.getTime()),
246
- maxIdForTime: (date) => layout.maxIdForTime(date.getTime()),
247
- is: wire.is,
248
- parse: wire.parse,
249
- safeParse: wire.safeParse,
250
- toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),
251
- "~standard": wire["~standard"]
252
- };
253
- }
254
- //#endregion
2
+ import { i as importSigningKey, n as decodeSigningKey, r as encodeSigningKey, t as createSignedTimestampId } from "./signed-BnRSC03a.mjs";
255
3
  export { IdsError, createSignedTimestampId, decodeSigningKey, encodeSigningKey, importSigningKey, isIdsError };
256
-
257
- //# sourceMappingURL=signed.mjs.map
@@ -1,5 +1,5 @@
1
- import { a as toWireId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-DH-UO4UR.mjs";
2
- import { n as readTimestampMsFromBase32Suffix, r as writeTimestamp } from "./timestamp-bytes-BBY7JI33.mjs";
1
+ import { a as toWireId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-CW2sD6BU.mjs";
2
+ import { n as readTimestampMsFromBase32Suffix, r as writeTimestamp } from "./timestamp-bytes-DoFjLjDp.mjs";
3
3
  //#region src/layouts/timestamp.ts
4
4
  const randomByteLength = 10;
5
5
  /** Writes a 16-byte timestamp-layout payload into codec-owned scratch. */
@@ -18,7 +18,7 @@ function extractTimestampFromId(prefix, id) {
18
18
  }
19
19
  /** Layout ops binder for the Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */
20
20
  function createTimestampLayoutOps(prefix, rng) {
21
- const buffer = new Uint8Array(16);
21
+ const buffer = /* @__PURE__ */ new Uint8Array(16);
22
22
  const randomView = new Uint8Array(buffer.buffer, 6, randomByteLength);
23
23
  return {
24
24
  generateAt: (ms) => {
@@ -42,7 +42,7 @@ function createTimestampLayoutOps(prefix, rng) {
42
42
  }
43
43
  //#endregion
44
44
  //#region src/timestamp.ts
45
- const hexCharCodeToNibble = new Uint8Array(128);
45
+ const hexCharCodeToNibble = /* @__PURE__ */ new Uint8Array(128);
46
46
  for (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;
47
47
  for (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;
48
48
  const defaultTimestampOptions = {
@@ -93,4 +93,4 @@ function createTimestampId(brand, opts = {}) {
93
93
  //#endregion
94
94
  export { createTimestampId as t };
95
95
 
96
- //# sourceMappingURL=timestamp-B5_UCzc6.mjs.map
96
+ //# sourceMappingURL=timestamp-BbZL8hwg.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"timestamp-B5_UCzc6.mjs","names":[],"sources":["../src/layouts/timestamp.ts","../src/timestamp.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { toWireId } from \"../wire/envelope.js\";\nimport { payloadByteLength } from \"../wire/invariants.js\";\nimport {\n readTimestampMsFromBase32Suffix,\n timestampByteLength,\n writeTimestamp,\n} from \"../wire/timestamp-bytes.js\";\n\nconst randomByteLength: number = payloadByteLength - timestampByteLength;\n\n/** Writes a 16-byte timestamp-layout payload into codec-owned scratch. */\nfunction buildPayload(\n ms: number,\n rng: (target: Uint8Array) => void,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n rng(randomView);\n}\n\n/** Writes sentinel min/max random bytes into codec-owned scratch. */\nfunction buildSentinelPayload(\n ms: number,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n randomView.fill(fill);\n}\n\n/** Decodes the creation timestamp from a trusted wire ID. */\nfunction extractTimestampFromId<Brand extends string>(prefix: Prefix<Brand>, id: Id<Brand>): Date {\n return new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length)));\n}\n\n/** Layout ops binder for the Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */\nexport function createTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n rng: (target: Uint8Array) => void,\n) {\n // Per-codec scratch buffer. Shared across generateAt(), minIdForTime(),\n // maxIdForTime(), and exampleWireId() — all are synchronous and overwrite both\n // the timestamp and random slices before encoding, so successive callers see\n // their own freshly-written bytes. toWireId reads the buffer and returns an\n // independent string, so the caller never sees the buffer itself.\n const buffer = new Uint8Array(payloadByteLength);\n const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);\n\n return {\n generateAt: (ms: number): Id<Brand> => {\n buildPayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n extractTimestamp: (id: Id<Brand>): Date => extractTimestampFromId(prefix, id),\n minIdForTime: (ms: number): Id<Brand> => {\n buildSentinelPayload(ms, 0x00, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n maxIdForTime: (ms: number): Id<Brand> => {\n buildSentinelPayload(ms, 0xff, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n exampleWireId: (ms: number): Id<Brand> => {\n buildPayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n };\n}\n","import { validateBrand } from \"./brand.js\";\nimport { createTimestampLayoutOps } from \"./layouts/timestamp.js\";\nimport { registerBrand } from \"./registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\n\n/**\n * Configuration options for a codec instance.\n */\nexport type TimestampOptions = {\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to a `crypto.randomUUID` fast path. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\ntype ResolvedTimestampOptions = Required<Pick<TimestampOptions, \"now\" | \"rng\">> &\n Pick<TimestampOptions, \"allowDuplicateBrand\">;\n\n/**\n * A brand-scoped codec for generating and validating public-facing IDs.\n *\n * Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a\n * 16-byte payload (6-byte ms timestamp + 10 random bytes). IDs sort by creation\n * time in ascending order.\n *\n * For encrypted IDs, use `createOpaqueTimestampId` from `@smonn/ids/opaque`.\n */\nexport type TimestampCodec<Brand extends string> = {\n /** Produces a new canonical ID using the codec's `now` and `rng`. */\n generate(): Id<Brand>;\n /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */\n generateAt(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /** Tight lower bound for any ID generated at `date` (random portion `0x00`). Throws on invalid dates. */\n minIdForTime(date: Date): Id<Brand>;\n /** Tight upper bound for any ID generated at `date` (random portion `0xff`). Throws on invalid dates. */\n maxIdForTime(date: Date): Id<Brand>;\n /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\n// hex charCode → 0–15 nibble, for decoding UUIDv4 strings into bytes.\n// Covers ['0'-'9' = 48–57] and ['a'-'f' = 97–102]; UUIDs are lowercase per spec.\nconst hexCharCodeToNibble = new Uint8Array(128);\nfor (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;\nfor (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;\n\nconst defaultTimestampOptions: ResolvedTimestampOptions = {\n now: Date.now,\n // crypto.randomUUID is ~7× faster than crypto.getRandomValues in Node 24\n // (~84 ns vs ~610 ns for a 16-byte fill — likely because the UUID path has\n // a tight fixed-format fast path). We use the 122 random bits of a UUIDv4\n // string as our entropy source, harvesting 10 fully-random bytes from\n // positions where no version (hex 12) or variant (hex 16) bits sit.\n // String layout: \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\" — bytes 0–5 are\n // string[0..7]+string[9..12], bytes 6–9 are string[24..31].\n rng: (target) => {\n const s = crypto.randomUUID();\n target[0] =\n (hexCharCodeToNibble[s.charCodeAt(0)]! << 4) | hexCharCodeToNibble[s.charCodeAt(1)]!;\n target[1] =\n (hexCharCodeToNibble[s.charCodeAt(2)]! << 4) | hexCharCodeToNibble[s.charCodeAt(3)]!;\n target[2] =\n (hexCharCodeToNibble[s.charCodeAt(4)]! << 4) | hexCharCodeToNibble[s.charCodeAt(5)]!;\n target[3] =\n (hexCharCodeToNibble[s.charCodeAt(6)]! << 4) | hexCharCodeToNibble[s.charCodeAt(7)]!;\n target[4] =\n (hexCharCodeToNibble[s.charCodeAt(9)]! << 4) | hexCharCodeToNibble[s.charCodeAt(10)]!;\n target[5] =\n (hexCharCodeToNibble[s.charCodeAt(11)]! << 4) | hexCharCodeToNibble[s.charCodeAt(12)]!;\n target[6] =\n (hexCharCodeToNibble[s.charCodeAt(24)]! << 4) | hexCharCodeToNibble[s.charCodeAt(25)]!;\n target[7] =\n (hexCharCodeToNibble[s.charCodeAt(26)]! << 4) | hexCharCodeToNibble[s.charCodeAt(27)]!;\n target[8] =\n (hexCharCodeToNibble[s.charCodeAt(28)]! << 4) | hexCharCodeToNibble[s.charCodeAt(29)]!;\n target[9] =\n (hexCharCodeToNibble[s.charCodeAt(30)]! << 4) | hexCharCodeToNibble[s.charCodeAt(31)]!;\n },\n};\n\n/**\n * Creates a codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createTimestampId<Brand extends string>(\n brand: Brand,\n opts: TimestampOptions = {},\n): TimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const options = {\n now: opts.now ?? defaultTimestampOptions.now,\n rng: opts.rng ?? defaultTimestampOptions.rng,\n } satisfies ResolvedTimestampOptions;\n\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createTimestampLayoutOps(prefix, options.rng);\n\n return {\n generate: () => layout.generateAt(options.now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n minIdForTime: (date: Date) => layout.minIdForTime(date.getTime()),\n maxIdForTime: (date: Date) => layout.maxIdForTime(date.getTime()),\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(options.now())),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"timestamp-BbZL8hwg.mjs","names":[],"sources":["../src/layouts/timestamp.ts","../src/timestamp.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { toWireId } from \"../wire/envelope.js\";\nimport { payloadByteLength } from \"../wire/invariants.js\";\nimport {\n readTimestampMsFromBase32Suffix,\n timestampByteLength,\n writeTimestamp,\n} from \"../wire/timestamp-bytes.js\";\n\nconst randomByteLength: number = payloadByteLength - timestampByteLength;\n\n/** Writes a 16-byte timestamp-layout payload into codec-owned scratch. */\nfunction buildPayload(\n ms: number,\n rng: (target: Uint8Array) => void,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n rng(randomView);\n}\n\n/** Writes sentinel min/max random bytes into codec-owned scratch. */\nfunction buildSentinelPayload(\n ms: number,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n randomView.fill(fill);\n}\n\n/** Decodes the creation timestamp from a trusted wire ID. */\nfunction extractTimestampFromId<Brand extends string>(prefix: Prefix<Brand>, id: Id<Brand>): Date {\n return new Date(readTimestampMsFromBase32Suffix(id.slice(prefix.length)));\n}\n\n/** Layout ops binder for the Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */\nexport function createTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n rng: (target: Uint8Array) => void,\n) {\n // Per-codec scratch buffer. Shared across generateAt(), minIdForTime(),\n // maxIdForTime(), and exampleWireId() — all are synchronous and overwrite both\n // the timestamp and random slices before encoding, so successive callers see\n // their own freshly-written bytes. toWireId reads the buffer and returns an\n // independent string, so the caller never sees the buffer itself.\n const buffer = new Uint8Array(payloadByteLength);\n const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);\n\n return {\n generateAt: (ms: number): Id<Brand> => {\n buildPayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n extractTimestamp: (id: Id<Brand>): Date => extractTimestampFromId(prefix, id),\n minIdForTime: (ms: number): Id<Brand> => {\n buildSentinelPayload(ms, 0x00, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n maxIdForTime: (ms: number): Id<Brand> => {\n buildSentinelPayload(ms, 0xff, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n exampleWireId: (ms: number): Id<Brand> => {\n buildPayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n };\n}\n","import { validateBrand } from \"./brand.js\";\nimport { createTimestampLayoutOps } from \"./layouts/timestamp.js\";\nimport { registerBrand } from \"./registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\n\n/**\n * Configuration options for a codec instance.\n */\nexport type TimestampOptions = {\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to a `crypto.randomUUID` fast path. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\ntype ResolvedTimestampOptions = Required<Pick<TimestampOptions, \"now\" | \"rng\">> &\n Pick<TimestampOptions, \"allowDuplicateBrand\">;\n\n/**\n * A brand-scoped codec for generating and validating public-facing IDs.\n *\n * Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a\n * 16-byte payload (6-byte ms timestamp + 10 random bytes). IDs sort by creation\n * time in ascending order.\n *\n * For encrypted IDs, use `createOpaqueTimestampId` from `@smonn/ids/opaque`.\n */\nexport type TimestampCodec<Brand extends string> = {\n /** Produces a new canonical ID using the codec's `now` and `rng`. */\n generate(): Id<Brand>;\n /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */\n generateAt(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /** Tight lower bound for any ID generated at `date` (random portion `0x00`). Throws on invalid dates. */\n minIdForTime(date: Date): Id<Brand>;\n /** Tight upper bound for any ID generated at `date` (random portion `0xff`). Throws on invalid dates. */\n maxIdForTime(date: Date): Id<Brand>;\n /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\n// hex charCode → 0–15 nibble, for decoding UUIDv4 strings into bytes.\n// Covers ['0'-'9' = 48–57] and ['a'-'f' = 97–102]; UUIDs are lowercase per spec.\nconst hexCharCodeToNibble = new Uint8Array(128);\nfor (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;\nfor (let i = 0; i < 6; i++) hexCharCodeToNibble[97 + i] = 10 + i;\n\nconst defaultTimestampOptions: ResolvedTimestampOptions = {\n now: Date.now,\n // crypto.randomUUID is ~7× faster than crypto.getRandomValues in Node 24\n // (~84 ns vs ~610 ns for a 16-byte fill — likely because the UUID path has\n // a tight fixed-format fast path). We use the 122 random bits of a UUIDv4\n // string as our entropy source, harvesting 10 fully-random bytes from\n // positions where no version (hex 12) or variant (hex 16) bits sit.\n // String layout: \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\" — bytes 0–5 are\n // string[0..7]+string[9..12], bytes 6–9 are string[24..31].\n rng: (target) => {\n const s = crypto.randomUUID();\n target[0] =\n (hexCharCodeToNibble[s.charCodeAt(0)]! << 4) | hexCharCodeToNibble[s.charCodeAt(1)]!;\n target[1] =\n (hexCharCodeToNibble[s.charCodeAt(2)]! << 4) | hexCharCodeToNibble[s.charCodeAt(3)]!;\n target[2] =\n (hexCharCodeToNibble[s.charCodeAt(4)]! << 4) | hexCharCodeToNibble[s.charCodeAt(5)]!;\n target[3] =\n (hexCharCodeToNibble[s.charCodeAt(6)]! << 4) | hexCharCodeToNibble[s.charCodeAt(7)]!;\n target[4] =\n (hexCharCodeToNibble[s.charCodeAt(9)]! << 4) | hexCharCodeToNibble[s.charCodeAt(10)]!;\n target[5] =\n (hexCharCodeToNibble[s.charCodeAt(11)]! << 4) | hexCharCodeToNibble[s.charCodeAt(12)]!;\n target[6] =\n (hexCharCodeToNibble[s.charCodeAt(24)]! << 4) | hexCharCodeToNibble[s.charCodeAt(25)]!;\n target[7] =\n (hexCharCodeToNibble[s.charCodeAt(26)]! << 4) | hexCharCodeToNibble[s.charCodeAt(27)]!;\n target[8] =\n (hexCharCodeToNibble[s.charCodeAt(28)]! << 4) | hexCharCodeToNibble[s.charCodeAt(29)]!;\n target[9] =\n (hexCharCodeToNibble[s.charCodeAt(30)]! << 4) | hexCharCodeToNibble[s.charCodeAt(31)]!;\n },\n};\n\n/**\n * Creates a codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createTimestampId<Brand extends string>(\n brand: Brand,\n opts: TimestampOptions = {},\n): TimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const options = {\n now: opts.now ?? defaultTimestampOptions.now,\n rng: opts.rng ?? defaultTimestampOptions.rng,\n } satisfies ResolvedTimestampOptions;\n\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createTimestampLayoutOps(prefix, options.rng);\n\n return {\n generate: () => layout.generateAt(options.now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n minIdForTime: (date: Date) => layout.minIdForTime(date.getTime()),\n maxIdForTime: (date: Date) => layout.maxIdForTime(date.getTime()),\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(options.now())),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":"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"}
@@ -1,8 +1,9 @@
1
- import { o as decodeBase32 } from "./codec-shell-DH-UO4UR.mjs";
1
+ import { o as decodeBase32 } from "./codec-shell-CW2sD6BU.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) {
5
5
  if (Number.isNaN(ms)) throw new Error("timestamp is not a number");
6
+ if (!Number.isInteger(ms)) throw new Error("timestamp is not an integer");
6
7
  if (ms < 0) throw new Error("timestamp is negative");
7
8
  if (ms >= 2 ** 48) throw new Error("timestamp exceeds 48-bit range");
8
9
  for (let i = 5; i >= 0; i--) {
@@ -23,4 +24,4 @@ function readTimestampMsFromBase32Suffix(base32Suffix) {
23
24
  //#endregion
24
25
  export { readTimestampMsFromBase32Suffix as n, writeTimestamp as r, readTimestampMs as t };
25
26
 
26
- //# sourceMappingURL=timestamp-bytes-BBY7JI33.mjs.map
27
+ //# sourceMappingURL=timestamp-bytes-DoFjLjDp.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamp-bytes-DoFjLjDp.mjs","names":[],"sources":["../src/wire/timestamp-bytes.ts"],"sourcesContent":["import { decodeBase32 } from \"../base32.js\";\n\n// Timestamp byte layout: first N bytes of the plaintext payload encode a\n// big-endian Unix-ms timestamp. Shared by timestamp-family layouts.\nexport const timestampByteLength: number = 6;\n\nconst timestampBase32Length: number = Math.ceil((timestampByteLength * 8) / 5);\n\n/** Write the timestamp in big-endian; encoded via mod-256 to avoid 32-bit bitwise coercion. */\nexport function writeTimestamp(ms: number, buffer: Uint8Array): void {\n if (Number.isNaN(ms)) throw new Error(\"timestamp is not a number\");\n if (!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"],"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"}