@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
package/dist/hono.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"hono.mjs","names":[],"sources":["../src/hono.ts"],"sourcesContent":["import { HTTPException } from \"hono/http-exception\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport type { IdParamFailure } from \"./adapter-types.js\";\nimport type { Id, ParseResult } from \"./types.js\";\n\nexport type { IdParamFailure };\n\ntype IdCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\n\n/** Options for `idParam`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of throwing when provided. The hook owns the response entirely —\n * the adapter neither throws nor writes a body.\n */\n onError?: (failure: IdParamFailure, c: Context) => Response | Promise<Response>;\n /**\n * Remap the default HTTP status for a failure reason without a full handler.\n * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.\n */\n status?: { brand_mismatch?: number; malformed?: number };\n};\n\n/**\n * Hono middleware that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** throws `HTTPException(status)` so the app's existing `onError` handler\n * controls rendering and content negotiation. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter neither\n * throws nor writes a response.\n *\n * **`options.status`:** remaps the default HTTP status for a reason without a full handler.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing ID → `reason: \"malformed\"`, default 400**\n *\n * On success, stores the canonical `Id<Brand>` in the Hono context under `paramName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idParam } from \"@smonn/ids/hono\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws HTTPException → app.onError renders it\n * app.get(\"/users/:id\", idParam(\"id\", usr), (c) => {\n * const id = c.get(\"id\"); // Id<\"usr\">, canonical\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/orgs/:id\", idParam(\"id\", org, {\n * onError: (failure, c) => c.json({ error: failure.reason }, failure.status),\n * }), handler);\n *\n * // Or a lightweight status remap without a full handler\n * app.get(\"/things/:id\", idParam(\"id\", thing, { status: { brand_mismatch: 400 } }), handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): MiddlewareHandler<{ Variables: Record<ParamKey, Id<Brand>> }> {\n return async (c, next) => {\n const raw = c.req.param(paramName);\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const reason =\n result.error === \"invalid_prefix\" ? (\"brand_mismatch\" as const) : (\"malformed\" as const);\n const defaultStatus = reason === \"brand_mismatch\" ? 404 : 400;\n const status = options?.status?.[reason] ?? defaultStatus;\n const failure: IdParamFailure = { reason, status };\n if (options?.onError) {\n return options.onError(failure, c);\n }\n throw new HTTPException(status as ConstructorParameters<typeof HTTPException>[0]);\n }\n c.set(paramName, result.id);\n await next();\n return;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,SAAgB,QACd,WACA,OACA,SAC+D;CAC/D,OAAO,OAAO,GAAG,SAAS;EACxB,MAAM,MAAM,EAAE,IAAI,MAAM,SAAS;EACjC,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,SACJ,OAAO,UAAU,mBAAoB,mBAA8B;GACrE,MAAM,gBAAgB,WAAW,mBAAmB,MAAM;GAC1D,MAAM,SAAS,SAAS,SAAS,WAAW;GAC5C,MAAM,UAA0B;IAAE;IAAQ;GAAO;GACjD,IAAI,SAAS,SACX,OAAO,QAAQ,QAAQ,SAAS,CAAC;GAEnC,MAAM,IAAI,cAAc,MAAwD;EAClF;EACA,EAAE,IAAI,WAAW,OAAO,EAAE;EAC1B,MAAM,KAAK;CAEb;AACF"}
1
+ {"version":3,"file":"hono.mjs","names":[],"sources":["../src/hono.ts"],"sourcesContent":["import { HTTPException } from \"hono/http-exception\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"./types.js\";\n\nexport type { IdParamFailure };\n\n/** Options for `idParam`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of throwing when provided. The hook owns the response entirely —\n * the adapter neither throws nor writes a body.\n */\n onError?: (failure: IdParamFailure, c: Context) => Response | Promise<Response>;\n /**\n * Remap the default HTTP status for a failure reason without a full handler.\n * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.\n */\n status?: { brand_mismatch?: number; malformed?: number };\n};\n\n/**\n * Hono middleware that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** throws `HTTPException(status)` so the app's existing `onError` handler\n * controls rendering and content negotiation. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter neither\n * throws nor writes a response.\n *\n * **`options.status`:** remaps the default HTTP status for a reason without a full handler.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing ID → `reason: \"malformed\"`, default 400**\n *\n * On success, stores the canonical `Id<Brand>` in the Hono context under `paramName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idParam } from \"@smonn/ids/hono\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws HTTPException → app.onError renders it\n * app.get(\"/users/:id\", idParam(\"id\", usr), (c) => {\n * const id = c.get(\"id\"); // Id<\"usr\">, canonical\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/orgs/:id\", idParam(\"id\", org, {\n * onError: (failure, c) => c.json({ error: failure.reason }, failure.status),\n * }), handler);\n *\n * // Or a lightweight status remap without a full handler\n * app.get(\"/things/:id\", idParam(\"id\", thing, { status: { brand_mismatch: 400 } }), handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): MiddlewareHandler<{ Variables: Record<ParamKey, Id<Brand>> }> {\n return async (c, next) => {\n const raw = c.req.param(paramName);\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n return options.onError(failure, c);\n }\n throw new HTTPException(failure.status as ConstructorParameters<typeof HTTPException>[0]);\n }\n c.set(paramName, result.id);\n await next();\n return;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,SAAgB,QACd,WACA,OACA,SAC+D;CAC/D,OAAO,OAAO,GAAG,SAAS;EACxB,MAAM,MAAM,EAAE,IAAI,MAAM,SAAS;EACjC,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SACX,OAAO,QAAQ,QAAQ,SAAS,CAAC;GAEnC,MAAM,IAAI,cAAc,QAAQ,MAAwD;EAC1F;EACA,EAAE,IAAI,WAAW,OAAO,EAAE;EAC1B,MAAM,KAAK;CAEb;AACF"}
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { t as createTimestampId } from "./timestamp-B5_UCzc6.mjs";
2
+ import { t as createTimestampId } from "./timestamp-BbZL8hwg.mjs";
3
3
  export { IdsError, createTimestampId, isIdsError };
@@ -0,0 +1,137 @@
1
+ import { t as IdsError } from "./error-Cp5qYZcv.mjs";
2
+ //#region src/bytes.ts
3
+ const hexDigits = "0123456789abcdef";
4
+ const invalidNibble = 255;
5
+ const hexCharCodeToNibble = (/* @__PURE__ */ new Uint8Array(128)).fill(invalidNibble);
6
+ for (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;
7
+ for (let i = 0; i < 6; i++) {
8
+ hexCharCodeToNibble[97 + i] = 10 + i;
9
+ hexCharCodeToNibble[65 + i] = 10 + i;
10
+ }
11
+ /** Lowercase hex encoding of raw bytes. */
12
+ function encodeHex(bytes) {
13
+ const codes = new Array(bytes.length * 2);
14
+ for (let i = 0; i < bytes.length; i++) {
15
+ const b = bytes[i];
16
+ codes[i * 2] = hexDigits.charCodeAt(b >>> 4);
17
+ codes[i * 2 + 1] = hexDigits.charCodeAt(b & 15);
18
+ }
19
+ return String.fromCharCode(...codes);
20
+ }
21
+ /** Decodes a hex string to raw bytes. Throws on non-hex input. */
22
+ function decodeHex(encoded) {
23
+ if (encoded.length % 2 !== 0) throw new Error("invalid hex");
24
+ const out = new Uint8Array(encoded.length / 2);
25
+ for (let i = 0; i < out.length; i++) {
26
+ const hiCode = encoded.charCodeAt(i * 2);
27
+ const loCode = encoded.charCodeAt(i * 2 + 1);
28
+ if (hiCode >= hexCharCodeToNibble.length || loCode >= hexCharCodeToNibble.length) throw new Error("invalid hex");
29
+ const hi = hexCharCodeToNibble[hiCode];
30
+ const lo = hexCharCodeToNibble[loCode];
31
+ if (hi === invalidNibble || lo === invalidNibble) throw new Error("invalid hex");
32
+ out[i] = hi << 4 | lo;
33
+ }
34
+ return out;
35
+ }
36
+ /** Base64url encoding without padding. */
37
+ function encodeBase64Url(bytes) {
38
+ let binary = "";
39
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
40
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
41
+ }
42
+ /** Decodes a base64url string to raw bytes. Throws on invalid input. */
43
+ function decodeBase64Url(encoded) {
44
+ const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
45
+ const pad = (4 - base64.length % 4) % 4;
46
+ const binary = atob(base64 + "=".repeat(pad));
47
+ const out = new Uint8Array(binary.length);
48
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
49
+ return out;
50
+ }
51
+ //#endregion
52
+ //#region src/key-material.ts
53
+ const validByteLengths = /* @__PURE__ */ new Set([
54
+ 16,
55
+ 24,
56
+ 32
57
+ ]);
58
+ function formatForError(value) {
59
+ try {
60
+ return String(value);
61
+ } catch {
62
+ return "[unprintable]";
63
+ }
64
+ }
65
+ function assertKeyMaterialFormat(format, noun) {
66
+ if (format !== "hex" && format !== "base64url") throw new IdsError("invalid_key_format", `invalid ${noun} key format: expected hex or base64url, got '${formatForError(format)}'`);
67
+ }
68
+ /**
69
+ * Throws `empty_keyring` when `keys` is empty.
70
+ * `noun` appears in the message (e.g. `"signing"` → "signing keyring must contain at least one key").
71
+ */
72
+ function assertNonEmptyKeyring(keys, noun) {
73
+ if (keys.length === 0) throw new IdsError("empty_keyring", `${noun} keyring must contain at least one key`);
74
+ }
75
+ /**
76
+ * Throws `duplicate_keyring_entry` when any two entries in `keys` compare equal.
77
+ * Uses the caller-supplied constant-time `keysEqual` comparator.
78
+ */
79
+ function assertNoDuplicateKeyringEntries(keys, keysEqual, noun) {
80
+ for (let i = 0; i < keys.length; i++) for (let j = i + 1; j < keys.length; j++) if (keysEqual(keys[i], keys[j])) throw new IdsError("duplicate_keyring_entry", `duplicate ${noun} key in keyring`);
81
+ }
82
+ /**
83
+ * Asserts that `keys` is non-empty and contains no pairwise duplicates.
84
+ *
85
+ * Combines {@link assertNonEmptyKeyring} and {@link assertNoDuplicateKeyringEntries}
86
+ * into a single call for codec constructors that validate a keyring at construction.
87
+ *
88
+ * @param keys - The keyring to validate.
89
+ * @param keysEqual - Constant-time comparator (e.g. `wrappingKeysEqual`, `signingKeysEqual`).
90
+ * @param noun - Noun used in error messages (e.g. `"wrapping"`, `"signing"`).
91
+ */
92
+ function assertValidKeyring(keys, keysEqual, noun) {
93
+ assertNonEmptyKeyring(keys, noun);
94
+ assertNoDuplicateKeyringEntries(keys, keysEqual, noun);
95
+ }
96
+ /** Throws `invalid_key_length` when `byteLength` is not 16, 24, or 32. */
97
+ function assertValidKeyMaterialByteLength(byteLength, noun) {
98
+ if (!validByteLengths.has(byteLength)) throw new IdsError("invalid_key_length", `invalid ${noun} key length: expected 16, 24, or 32 bytes, got ${byteLength}`);
99
+ }
100
+ /**
101
+ * Encodes raw key bytes as hex or base64url.
102
+ *
103
+ * `formatNoun` appears in format error messages; `lengthNoun` in length error messages.
104
+ * For most key types both are the same (e.g. `"wrapping"`, `"signing"`). For the
105
+ * Opaque key, they differ (`"opaque"` and `"AES"` respectively) to preserve the
106
+ * original human-readable messages.
107
+ */
108
+ function encodeKeyMaterial(bytes, format, formatNoun, lengthNoun) {
109
+ assertKeyMaterialFormat(format, formatNoun);
110
+ assertValidKeyMaterialByteLength(bytes.length, lengthNoun);
111
+ if (format === "hex") return encodeHex(bytes);
112
+ return encodeBase64Url(bytes);
113
+ }
114
+ /**
115
+ * Decodes a hex or base64url-encoded key string back to raw bytes.
116
+ *
117
+ * `formatNoun` appears in format error messages; `lengthNoun` in length error messages.
118
+ */
119
+ function decodeKeyMaterial(encoded, format, formatNoun, lengthNoun) {
120
+ assertKeyMaterialFormat(format, formatNoun);
121
+ let bytes;
122
+ if (format === "hex") {
123
+ 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");
124
+ if (!/^[0-9a-fA-F]+$/.test(encoded)) throw new IdsError("invalid_key_encoding", "invalid hex key: expected [0-9a-fA-F] only");
125
+ bytes = decodeHex(encoded);
126
+ } else try {
127
+ bytes = decodeBase64Url(encoded);
128
+ } catch {
129
+ throw new IdsError("invalid_key_encoding", "invalid base64url key");
130
+ }
131
+ assertValidKeyMaterialByteLength(bytes.length, lengthNoun);
132
+ return bytes;
133
+ }
134
+ //#endregion
135
+ export { encodeKeyMaterial as i, assertValidKeyring as n, decodeKeyMaterial as r, assertValidKeyMaterialByteLength as t };
136
+
137
+ //# sourceMappingURL=key-material-gOnqTNoV.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-material-gOnqTNoV.mjs","names":[],"sources":["../src/bytes.ts","../src/key-material.ts"],"sourcesContent":["const hexDigits = \"0123456789abcdef\";\n\nconst invalidNibble = 0xff;\nconst hexCharCodeToNibble = new Uint8Array(128).fill(invalidNibble);\nfor (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;\nfor (let i = 0; i < 6; i++) {\n hexCharCodeToNibble[97 + i] = 10 + i;\n hexCharCodeToNibble[65 + i] = 10 + i;\n}\n\n/** Lowercase hex encoding of raw bytes. */\nexport function encodeHex(bytes: Uint8Array): string {\n // oxlint-disable-next-line no-new-array\n const codes = new Array<number>(bytes.length * 2);\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i]!;\n codes[i * 2] = hexDigits.charCodeAt(b >>> 4);\n codes[i * 2 + 1] = hexDigits.charCodeAt(b & 0x0f);\n }\n return String.fromCharCode(...codes);\n}\n\n/** Decodes a hex string to raw bytes. Throws on non-hex input. */\nexport function decodeHex(encoded: string): Uint8Array {\n if (encoded.length % 2 !== 0) throw new Error(\"invalid hex\");\n const out = new Uint8Array(encoded.length / 2);\n for (let i = 0; i < out.length; i++) {\n const hiCode = encoded.charCodeAt(i * 2);\n const loCode = encoded.charCodeAt(i * 2 + 1);\n if (hiCode >= hexCharCodeToNibble.length || loCode >= hexCharCodeToNibble.length) {\n throw new Error(\"invalid hex\");\n }\n const hi = hexCharCodeToNibble[hiCode]!;\n const lo = hexCharCodeToNibble[loCode]!;\n if (hi === invalidNibble || lo === invalidNibble) {\n throw new Error(\"invalid hex\");\n }\n out[i] = (hi << 4) | lo;\n }\n return out;\n}\n\n/** Base64url encoding without padding. */\nexport function encodeBase64Url(bytes: Uint8Array): string {\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** Decodes a base64url string to raw bytes. Throws on invalid input. */\nexport function decodeBase64Url(encoded: string): Uint8Array {\n const base64 = encoded.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const pad = (4 - (base64.length % 4)) % 4;\n const binary = atob(base64 + \"=\".repeat(pad));\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n}\n","import { decodeBase64Url, decodeHex, encodeBase64Url, encodeHex } from \"./bytes.js\";\nimport { IdsError } from \"./error.js\";\n\ntype KeyMaterialFormat = \"hex\" | \"base64url\";\n\nconst validByteLengths = new Set([16, 24, 32]);\n\nfunction formatForError(value: unknown): string {\n try {\n return String(value);\n } catch {\n return \"[unprintable]\";\n }\n}\n\nfunction assertKeyMaterialFormat(\n format: unknown,\n noun: string,\n): asserts format is KeyMaterialFormat {\n if (format !== \"hex\" && format !== \"base64url\") {\n throw new IdsError(\n \"invalid_key_format\",\n `invalid ${noun} key format: expected hex or base64url, got '${formatForError(format)}'`,\n );\n }\n}\n\n/**\n * Throws `empty_keyring` when `keys` is empty.\n * `noun` appears in the message (e.g. `\"signing\"` → \"signing keyring must contain at least one key\").\n */\nfunction assertNonEmptyKeyring<K = unknown>(keys: readonly K[], noun: string): void {\n if (keys.length === 0) {\n throw new IdsError(\"empty_keyring\", `${noun} keyring must contain at least one key`);\n }\n}\n\n/**\n * Throws `duplicate_keyring_entry` when any two entries in `keys` compare equal.\n * Uses the caller-supplied constant-time `keysEqual` comparator.\n */\nfunction assertNoDuplicateKeyringEntries<K>(\n keys: readonly K[],\n keysEqual: (a: K, b: K) => boolean,\n noun: string,\n): void {\n for (let i = 0; i < keys.length; i++) {\n for (let j = i + 1; j < keys.length; j++) {\n if (keysEqual(keys[i]!, keys[j]!)) {\n throw new IdsError(\"duplicate_keyring_entry\", `duplicate ${noun} key in keyring`);\n }\n }\n }\n}\n\n/**\n * Asserts that `keys` is non-empty and contains no pairwise duplicates.\n *\n * Combines {@link assertNonEmptyKeyring} and {@link assertNoDuplicateKeyringEntries}\n * into a single call for codec constructors that validate a keyring at construction.\n *\n * @param keys - The keyring to validate.\n * @param keysEqual - Constant-time comparator (e.g. `wrappingKeysEqual`, `signingKeysEqual`).\n * @param noun - Noun used in error messages (e.g. `\"wrapping\"`, `\"signing\"`).\n */\nexport function assertValidKeyring<K>(\n keys: readonly K[],\n keysEqual: (a: K, b: K) => boolean,\n noun: string,\n): void {\n assertNonEmptyKeyring(keys, noun);\n assertNoDuplicateKeyringEntries(keys, keysEqual, noun);\n}\n\n/** Throws `invalid_key_length` when `byteLength` is not 16, 24, or 32. */\nexport function assertValidKeyMaterialByteLength(byteLength: number, noun: string): void {\n if (!validByteLengths.has(byteLength)) {\n throw new IdsError(\n \"invalid_key_length\",\n `invalid ${noun} key length: expected 16, 24, or 32 bytes, got ${byteLength}`,\n );\n }\n}\n\n/**\n * Encodes raw key bytes as hex or base64url.\n *\n * `formatNoun` appears in format error messages; `lengthNoun` in length error messages.\n * For most key types both are the same (e.g. `\"wrapping\"`, `\"signing\"`). For the\n * Opaque key, they differ (`\"opaque\"` and `\"AES\"` respectively) to preserve the\n * original human-readable messages.\n */\nexport function encodeKeyMaterial(\n bytes: Uint8Array,\n format: KeyMaterialFormat,\n formatNoun: string,\n lengthNoun: string,\n): string {\n assertKeyMaterialFormat(format, formatNoun);\n assertValidKeyMaterialByteLength(bytes.length, lengthNoun);\n if (format === \"hex\") return encodeHex(bytes);\n return encodeBase64Url(bytes);\n}\n\n/**\n * Decodes a hex or base64url-encoded key string back to raw bytes.\n *\n * `formatNoun` appears in format error messages; `lengthNoun` in length error messages.\n */\nexport function decodeKeyMaterial(\n encoded: string,\n format: KeyMaterialFormat,\n formatNoun: string,\n lengthNoun: string,\n): Uint8Array {\n assertKeyMaterialFormat(format, formatNoun);\n let bytes: Uint8Array;\n if (format === \"hex\") {\n if (encoded.length === 0 || encoded.length % 2 !== 0) {\n throw new IdsError(\n \"invalid_key_encoding\",\n \"invalid hex key: length must be a positive even number of characters\",\n );\n }\n if (!/^[0-9a-fA-F]+$/.test(encoded)) {\n throw new IdsError(\"invalid_key_encoding\", \"invalid hex key: expected [0-9a-fA-F] only\");\n }\n bytes = decodeHex(encoded);\n } else {\n try {\n bytes = decodeBase64Url(encoded);\n } catch {\n throw new IdsError(\"invalid_key_encoding\", \"invalid base64url key\");\n }\n }\n assertValidKeyMaterialByteLength(bytes.length, lengthNoun);\n return bytes;\n}\n"],"mappings":";;AAAA,MAAM,YAAY;AAElB,MAAM,gBAAgB;AACtB,MAAM,uCAAsB,IAAI,WAAW,GAAG,EAAA,CAAE,KAAK,aAAa;AAClE,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,oBAAoB,KAAK,KAAK;AAC3D,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;CAC1B,oBAAoB,KAAK,KAAK,KAAK;CACnC,oBAAoB,KAAK,KAAK,KAAK;AACrC;;AAGA,SAAgB,UAAU,OAA2B;CAEnD,MAAM,QAAQ,IAAI,MAAc,MAAM,SAAS,CAAC;CAChD,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,IAAI,MAAM;EAChB,MAAM,IAAI,KAAK,UAAU,WAAW,MAAM,CAAC;EAC3C,MAAM,IAAI,IAAI,KAAK,UAAU,WAAW,IAAI,EAAI;CAClD;CACA,OAAO,OAAO,aAAa,GAAG,KAAK;AACrC;;AAGA,SAAgB,UAAU,SAA6B;CACrD,IAAI,QAAQ,SAAS,MAAM,GAAG,MAAM,IAAI,MAAM,aAAa;CAC3D,MAAM,MAAM,IAAI,WAAW,QAAQ,SAAS,CAAC;CAC7C,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;EACnC,MAAM,SAAS,QAAQ,WAAW,IAAI,CAAC;EACvC,MAAM,SAAS,QAAQ,WAAW,IAAI,IAAI,CAAC;EAC3C,IAAI,UAAU,oBAAoB,UAAU,UAAU,oBAAoB,QACxE,MAAM,IAAI,MAAM,aAAa;EAE/B,MAAM,KAAK,oBAAoB;EAC/B,MAAM,KAAK,oBAAoB;EAC/B,IAAI,OAAO,iBAAiB,OAAO,eACjC,MAAM,IAAI,MAAM,aAAa;EAE/B,IAAI,KAAM,MAAM,IAAK;CACvB;CACA,OAAO;AACT;;AAGA,SAAgB,gBAAgB,OAA2B;CACzD,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,UAAU,OAAO,aAAa,MAAM,EAAG;CAC9E,OAAO,KAAK,MAAM,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,EAAE;AAC/E;;AAGA,SAAgB,gBAAgB,SAA6B;CAC3D,MAAM,SAAS,QAAQ,QAAQ,MAAM,GAAG,CAAC,CAAC,QAAQ,MAAM,GAAG;CAC3D,MAAM,OAAO,IAAK,OAAO,SAAS,KAAM;CACxC,MAAM,SAAS,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC;CAC5C,MAAM,MAAM,IAAI,WAAW,OAAO,MAAM;CACxC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,IAAI,KAAK,OAAO,WAAW,CAAC;CACpE,OAAO;AACT;;;ACpDA,MAAM,mCAAmB,IAAI,IAAI;CAAC;CAAI;CAAI;AAAE,CAAC;AAE7C,SAAS,eAAe,OAAwB;CAC9C,IAAI;EACF,OAAO,OAAO,KAAK;CACrB,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,wBACP,QACA,MACqC;CACrC,IAAI,WAAW,SAAS,WAAW,aACjC,MAAM,IAAI,SACR,sBACA,WAAW,KAAK,+CAA+C,eAAe,MAAM,EAAE,EACxF;AAEJ;;;;;AAMA,SAAS,sBAAmC,MAAoB,MAAoB;CAClF,IAAI,KAAK,WAAW,GAClB,MAAM,IAAI,SAAS,iBAAiB,GAAG,KAAK,uCAAuC;AAEvF;;;;;AAMA,SAAS,gCACP,MACA,WACA,MACM;CACN,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAC/B,KAAK,IAAI,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KACnC,IAAI,UAAU,KAAK,IAAK,KAAK,EAAG,GAC9B,MAAM,IAAI,SAAS,2BAA2B,aAAa,KAAK,gBAAgB;AAIxF;;;;;;;;;;;AAYA,SAAgB,mBACd,MACA,WACA,MACM;CACN,sBAAsB,MAAM,IAAI;CAChC,gCAAgC,MAAM,WAAW,IAAI;AACvD;;AAGA,SAAgB,iCAAiC,YAAoB,MAAoB;CACvF,IAAI,CAAC,iBAAiB,IAAI,UAAU,GAClC,MAAM,IAAI,SACR,sBACA,WAAW,KAAK,iDAAiD,YACnE;AAEJ;;;;;;;;;AAUA,SAAgB,kBACd,OACA,QACA,YACA,YACQ;CACR,wBAAwB,QAAQ,UAAU;CAC1C,iCAAiC,MAAM,QAAQ,UAAU;CACzD,IAAI,WAAW,OAAO,OAAO,UAAU,KAAK;CAC5C,OAAO,gBAAgB,KAAK;AAC9B;;;;;;AAOA,SAAgB,kBACd,SACA,QACA,YACA,YACY;CACZ,wBAAwB,QAAQ,UAAU;CAC1C,IAAI;CACJ,IAAI,WAAW,OAAO;EACpB,IAAI,QAAQ,WAAW,KAAK,QAAQ,SAAS,MAAM,GACjD,MAAM,IAAI,SACR,wBACA,sEACF;EAEF,IAAI,CAAC,iBAAiB,KAAK,OAAO,GAChC,MAAM,IAAI,SAAS,wBAAwB,4CAA4C;EAEzF,QAAQ,UAAU,OAAO;CAC3B,OACE,IAAI;EACF,QAAQ,gBAAgB,OAAO;CACjC,QAAQ;EACN,MAAM,IAAI,SAAS,wBAAwB,uBAAuB;CACpE;CAEF,iCAAiC,MAAM,QAAQ,UAAU;CACzD,OAAO;AACT"}
package/dist/kysely.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
2
2
  import { t as Id } from "./types-g7CiQDyE.mjs";
3
- import { t as IdColumnCodec } from "./drizzle-CeSni5PB.mjs";
3
+ import { n as IdColumnCodec } from "./adapter-types-unUcmMXC.mjs";
4
4
  import { ColumnType } from "kysely";
5
5
 
6
6
  //#region src/kysely.d.ts
package/dist/kysely.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
+ import { t as readIdColumn } from "./adapter-types-BY-wrYYB.mjs";
2
3
  //#region src/kysely.ts
3
4
  /**
4
5
  * Kysely column adapter bound to a codec.
@@ -31,9 +32,7 @@ function idColumn(codec) {
31
32
  return value;
32
33
  },
33
34
  fromDriver(value) {
34
- const result = codec.safeParse(value);
35
- if (!result.ok) throw new IdsError("invalid_id", `invalid ID from database: ${result.error}`, { cause: result.error });
36
- return result.id;
35
+ return readIdColumn(codec, value);
37
36
  }
38
37
  };
39
38
  }
@@ -1 +1 @@
1
- {"version":3,"file":"kysely.mjs","names":[],"sources":["../src/kysely.ts"],"sourcesContent":["import type { ColumnType } from \"kysely\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport type { IdColumnCodec } from \"./drizzle.js\";\nimport type { Id } from \"./types.js\";\n\nexport type { IdColumnCodec } from \"./drizzle.js\";\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\n\n/**\n * Kysely column type mapping for `Id<Brand>`.\n *\n * Use this in your Kysely `Database` interface to type a column as `Id<Brand>` at\n * the TypeScript level. Pair it with `idColumn(codec)` for runtime read/write\n * transformation.\n *\n * @example\n * ```ts\n * import type { IdColumnType } from \"@smonn/ids/kysely\";\n * import type { Id } from \"@smonn/ids\";\n *\n * interface Database {\n * users: { id: IdColumnType<\"usr\"> };\n * }\n * ```\n */\nexport type IdColumnType<Brand extends string> = ColumnType<Id<Brand>, Id<Brand>, Id<Brand>>;\n\n/**\n * Kysely column adapter bound to a codec.\n *\n * Returns an object with `fromDriver` / `toDriver` helpers that mirror the read/write\n * contract of the Drizzle adapter — same error message, same strictness (safeParse on\n * read, identity on write).\n *\n * **Write path:** passes the `Id<Brand>` directly to the driver — it is already\n * the canonical string form.\n *\n * **Read path:** normalises the raw DB string via `codec.safeParse()`. Throws if\n * the value does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/kysely\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const usrCol = idColumn(usr);\n *\n * // In a query result handler:\n * const id = usrCol.fromDriver(row.id);\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): {\n toDriver(value: Id<Brand>): string;\n fromDriver(value: string): Id<Brand>;\n} {\n return {\n toDriver(value: Id<Brand>): string {\n return value;\n },\n fromDriver(value: string): Id<Brand> {\n const result = codec.safeParse(value);\n if (!result.ok) {\n throw new IdsError(\"invalid_id\", `invalid ID from database: ${result.error}`, {\n cause: result.error,\n });\n }\n return result.id;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgB,SACd,OAIA;CACA,OAAO;EACL,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,MAAM,SAAS,MAAM,UAAU,KAAK;GACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,SAAS,cAAc,6BAA6B,OAAO,SAAS,EAC5E,OAAO,OAAO,MAChB,CAAC;GAEH,OAAO,OAAO;EAChB;CACF;AACF"}
1
+ {"version":3,"file":"kysely.mjs","names":[],"sources":["../src/kysely.ts"],"sourcesContent":["import type { ColumnType } from \"kysely\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"./types.js\";\n\nexport type { IdColumnCodec } from \"./adapter-types.js\";\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\n\n/**\n * Kysely column type mapping for `Id<Brand>`.\n *\n * Use this in your Kysely `Database` interface to type a column as `Id<Brand>` at\n * the TypeScript level. Pair it with `idColumn(codec)` for runtime read/write\n * transformation.\n *\n * @example\n * ```ts\n * import type { IdColumnType } from \"@smonn/ids/kysely\";\n * import type { Id } from \"@smonn/ids\";\n *\n * interface Database {\n * users: { id: IdColumnType<\"usr\"> };\n * }\n * ```\n */\nexport type IdColumnType<Brand extends string> = ColumnType<Id<Brand>, Id<Brand>, Id<Brand>>;\n\n/**\n * Kysely column adapter bound to a codec.\n *\n * Returns an object with `fromDriver` / `toDriver` helpers that mirror the read/write\n * contract of the Drizzle adapter — same error message, same strictness (safeParse on\n * read, identity on write).\n *\n * **Write path:** passes the `Id<Brand>` directly to the driver — it is already\n * the canonical string form.\n *\n * **Read path:** normalises the raw DB string via `codec.safeParse()`. Throws if\n * the value does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/kysely\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const usrCol = idColumn(usr);\n *\n * // In a query result handler:\n * const id = usrCol.fromDriver(row.id);\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): {\n toDriver(value: Id<Brand>): string;\n fromDriver(value: string): Id<Brand>;\n} {\n return {\n toDriver(value: Id<Brand>): string {\n return value;\n },\n fromDriver(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgB,SACd,OAIA;CACA,OAAO;EACL,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF"}
@@ -1,12 +1,12 @@
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-DH-UO4UR.mjs";
3
- import { r as writeTimestamp, t as readTimestampMs } from "./timestamp-bytes-BBY7JI33.mjs";
4
- import { i as encodeHex, n as decodeHex, r as encodeBase64Url, t as decodeBase64Url } from "./bytes-lhzKVaBV.mjs";
1
+ import { a as toWireId, i as payloadBytesFromId, n as registerBrand, r as payloadBase32Length, s as validateBrand, t as wireMethods } from "./codec-shell-CW2sD6BU.mjs";
2
+ import { r as writeTimestamp, t as readTimestampMs } from "./timestamp-bytes-DoFjLjDp.mjs";
3
+ import { i as encodeKeyMaterial, r as decodeKeyMaterial, t as assertValidKeyMaterialByteLength } from "./key-material-gOnqTNoV.mjs";
4
+ import { t as defaultRng } from "./rng-CPJOx_nE.mjs";
5
5
  //#region src/layouts/opaque.ts
6
- const zeroIv = new Uint8Array(16);
6
+ const zeroIv = /* @__PURE__ */ new Uint8Array(16);
7
7
  const pkcsPad = 16;
8
8
  function buildPlaintext(ms, rng) {
9
- const plaintext = new Uint8Array(16);
9
+ const plaintext = /* @__PURE__ */ new Uint8Array(16);
10
10
  writeTimestamp(ms, plaintext);
11
11
  rng(plaintext.subarray(6, 16));
12
12
  return plaintext;
@@ -18,13 +18,13 @@ async function encryptPayload(key, plaintext) {
18
18
  }, key, plaintext)).subarray(0, 16);
19
19
  }
20
20
  async function decryptPayload(key, c1) {
21
- const c2Input = new Uint8Array(16);
21
+ const c2Input = /* @__PURE__ */ new Uint8Array(16);
22
22
  for (let i = 0; i < 16; i++) c2Input[i] = pkcsPad ^ c1[i];
23
23
  const c2Encrypted = new Uint8Array(await crypto.subtle.encrypt({
24
24
  name: "AES-CBC",
25
25
  iv: zeroIv
26
26
  }, key, c2Input));
27
- const ciphertext = new Uint8Array(32);
27
+ const ciphertext = /* @__PURE__ */ new Uint8Array(32);
28
28
  ciphertext.set(c1, 0);
29
29
  ciphertext.set(c2Encrypted.subarray(0, 16), 16);
30
30
  return new Uint8Array(await crypto.subtle.decrypt({
@@ -55,11 +55,6 @@ function createOpaqueLayoutOps(prefix, key, rng) {
55
55
  }
56
56
  //#endregion
57
57
  //#region src/opaque-key.ts
58
- const validAesKeyByteLengths = new Set([
59
- 16,
60
- 24,
61
- 32
62
- ]);
63
58
  const opaqueKeyInternals = /* @__PURE__ */ new WeakMap();
64
59
  /**
65
60
  * Imports raw AES key bytes into an {@link OpaqueKey} handle for the Opaque
@@ -72,7 +67,7 @@ const opaqueKeyInternals = /* @__PURE__ */ new WeakMap();
72
67
  * @param bytes - 16, 24, or 32 raw key bytes.
73
68
  */
74
69
  async function importOpaqueKey(bytes) {
75
- assertValidAesKeyByteLength(bytes.length);
70
+ assertValidKeyMaterialByteLength(bytes.length, "AES");
76
71
  const cryptoKey = await crypto.subtle.importKey("raw", bytes, "AES-CBC", false, ["encrypt", "decrypt"]);
77
72
  const key = Object.freeze({});
78
73
  opaqueKeyInternals.set(key, cryptoKey);
@@ -90,10 +85,7 @@ function getOpaqueKeyCryptoKey(key) {
90
85
  * @param format - `hex` (lowercase) or `base64url`.
91
86
  */
92
87
  function encodeOpaqueKey(bytes, format) {
93
- assertOpaqueKeyFormat(format);
94
- assertValidAesKeyByteLength(bytes.length);
95
- if (format === "hex") return encodeHex(bytes);
96
- return encodeBase64Url(bytes);
88
+ return encodeKeyMaterial(bytes, format, "opaque", "AES");
97
89
  }
98
90
  /**
99
91
  * Decodes key material emitted by `encodeOpaqueKey` (or `ids keygen`) back to raw bytes.
@@ -102,38 +94,10 @@ function encodeOpaqueKey(bytes, format) {
102
94
  * @param format - Must match how the string was encoded.
103
95
  */
104
96
  function decodeOpaqueKey(encoded, format) {
105
- assertOpaqueKeyFormat(format);
106
- let bytes;
107
- if (format === "hex") {
108
- 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");
109
- if (!/^[0-9a-fA-F]+$/.test(encoded)) throw new IdsError("invalid_key_encoding", "invalid hex key: expected [0-9a-fA-F] only");
110
- bytes = decodeHex(encoded);
111
- } else try {
112
- bytes = decodeBase64Url(encoded);
113
- } catch {
114
- throw new IdsError("invalid_key_encoding", "invalid base64url key");
115
- }
116
- assertValidAesKeyByteLength(bytes.length);
117
- return bytes;
118
- }
119
- function assertValidAesKeyByteLength(byteLength) {
120
- if (!validAesKeyByteLengths.has(byteLength)) throw new IdsError("invalid_key_length", `invalid AES key length: expected 16, 24, or 32 bytes, got ${byteLength}`);
121
- }
122
- function assertOpaqueKeyFormat(format) {
123
- if (format !== "hex" && format !== "base64url") throw new IdsError("invalid_key_format", `invalid opaque key format: expected hex or base64url, got '${formatForError(format)}'`);
124
- }
125
- function formatForError(value) {
126
- try {
127
- return String(value);
128
- } catch {
129
- return "[unprintable]";
130
- }
97
+ return decodeKeyMaterial(encoded, format, "opaque", "AES");
131
98
  }
132
99
  //#endregion
133
100
  //#region src/opaque.ts
134
- function defaultRng(target) {
135
- crypto.getRandomValues(target);
136
- }
137
101
  /**
138
102
  * Creates an Opaque Timestamp codec for `brand` (three lowercase a–z characters).
139
103
  *
@@ -164,4 +128,4 @@ function createOpaqueTimestampId(brand, opts) {
164
128
  //#endregion
165
129
  export { importOpaqueKey as i, decodeOpaqueKey as n, encodeOpaqueKey as r, createOpaqueTimestampId as t };
166
130
 
167
- //# sourceMappingURL=opaque-uvjOFY_0.mjs.map
131
+ //# sourceMappingURL=opaque-BpqxV8oB.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opaque-BpqxV8oB.mjs","names":[],"sources":["../src/layouts/opaque.ts","../src/opaque-key.ts","../src/opaque.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { payloadBytesFromId, toWireId } from \"../wire/envelope.js\";\nimport { payloadBase32Length, payloadByteLength } from \"../wire/invariants.js\";\nimport { readTimestampMs, timestampByteLength, writeTimestamp } from \"../wire/timestamp-bytes.js\";\n\nconst zeroIv = new Uint8Array(payloadByteLength);\nconst pkcsPad = 0x10;\n\nfunction buildPlaintext(ms: number, rng: (target: Uint8Array) => void): Uint8Array {\n const plaintext = new Uint8Array(payloadByteLength);\n writeTimestamp(ms, plaintext);\n rng(plaintext.subarray(timestampByteLength, payloadByteLength));\n return plaintext;\n}\n\nasync function encryptPayload(key: CryptoKey, plaintext: Uint8Array): Promise<Uint8Array> {\n const encrypted = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n plaintext as Uint8Array<ArrayBuffer>,\n ),\n );\n return encrypted.subarray(0, payloadByteLength);\n}\n\n// AES-CBC strip-and-reconstruct decrypt (ADR-0004). The wire carries only C1\n// (16 bytes); C2 = AES_K(P2 XOR C1) where P2 is the PKCS#7 pad block (0x10×16).\n// Recompute C2 via CBC encrypt of (P2 XOR C1) with IV=0, then decrypt C1‖C2.\nasync function decryptPayload(key: CryptoKey, c1: Uint8Array): Promise<Uint8Array> {\n const c2Input = new Uint8Array(payloadByteLength);\n for (let i = 0; i < payloadByteLength; i++) c2Input[i] = pkcsPad ^ c1[i]!;\n const c2Encrypted = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n c2Input as Uint8Array<ArrayBuffer>,\n ),\n );\n const ciphertext = new Uint8Array(payloadByteLength * 2);\n ciphertext.set(c1, 0);\n ciphertext.set(c2Encrypted.subarray(0, payloadByteLength), payloadByteLength);\n return new Uint8Array(\n await crypto.subtle.decrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n ciphertext as Uint8Array<ArrayBuffer>,\n ),\n );\n}\n\nasync function extractTimestampFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n id: Id<Brand>,\n): Promise<Date> {\n const plaintext = await decryptPayload(key, payloadBytesFromId(prefix, id));\n return new Date(readTimestampMs(plaintext));\n}\n\n/** Produces a canonical encrypted wire ID. Per-call plaintext/ciphertext buffers —\n * subtle dominates this path; reuse would be safe but not worth pinning to spec detail. */\nasync function generateWireId<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n rng: (target: Uint8Array) => void,\n ms: number,\n): Promise<Id<Brand>> {\n const plaintext = buildPlaintext(ms, rng);\n const encrypted = await encryptPayload(key, plaintext);\n return toWireId(prefix, encrypted);\n}\n\n/** Structural placeholder for JSON Schema (encrypt is async). */\nfunction schemaExample<Brand extends string>(prefix: Prefix<Brand>): string {\n return prefix + \"0\".repeat(payloadBase32Length);\n}\n\n/** Layout ops binder for the Opaque Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */\nexport function createOpaqueLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n key: CryptoKey,\n rng: (target: Uint8Array) => void,\n) {\n return {\n generateAt: (ms: number): Promise<Id<Brand>> => generateWireId(prefix, key, rng, ms),\n extractTimestamp: (id: Id<Brand>): Promise<Date> => extractTimestampFromId(prefix, key, id),\n exampleWireId: (): Id<Brand> => schemaExample(prefix) as Id<Brand>,\n };\n}\n","import {\n assertValidKeyMaterialByteLength,\n decodeKeyMaterial,\n encodeKeyMaterial,\n} from \"./key-material.js\";\n\n/** Wire encoding for opaque AES key material (not Crockford base32). */\nexport type OpaqueKeyFormat = \"hex\" | \"base64url\";\n\ndeclare const opaqueKeyBrand: unique symbol;\n\n/**\n * Opaque imported handle for one AES key used by the Opaque Timestamp codec.\n *\n * Holds the underlying `CryptoKey` internally; callers never access it directly.\n * Obtain handles via {@link importOpaqueKey} and pass them to\n * `createOpaqueTimestampId` as the `key` option.\n *\n * Distinct from the `WrappingKey` used by `@smonn/ids/wrapped` — one raw\n * secret must not silently serve both codecs without an explicit import.\n */\nexport type OpaqueKey = {\n readonly [opaqueKeyBrand]: \"OpaqueKey\";\n};\n\nconst opaqueKeyInternals = new WeakMap<OpaqueKey, CryptoKey>();\n\n/**\n * Imports raw AES key bytes into an {@link OpaqueKey} handle for the Opaque\n * Timestamp codec.\n *\n * Accepts 16, 24, or 32 bytes (AES-128 / AES-192 / AES-256 strength).\n * To store or transport key material, use {@link encodeOpaqueKey} /\n * {@link decodeOpaqueKey} (`\"hex\"` or `\"base64url\"` — not Crockford base32).\n *\n * @param bytes - 16, 24, or 32 raw key bytes.\n */\nexport async function importOpaqueKey(bytes: Uint8Array): Promise<OpaqueKey> {\n assertValidKeyMaterialByteLength(bytes.length, \"AES\");\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n bytes as Uint8Array<ArrayBuffer>,\n \"AES-CBC\",\n false,\n [\"encrypt\", \"decrypt\"],\n );\n const key = Object.freeze({}) as OpaqueKey;\n opaqueKeyInternals.set(key, cryptoKey);\n return key;\n}\n\nexport function getOpaqueKeyCryptoKey(key: OpaqueKey): CryptoKey {\n const cryptoKey = opaqueKeyInternals.get(key);\n if (cryptoKey === undefined) {\n throw new Error(\"invalid opaque key\");\n }\n return cryptoKey;\n}\n\n/**\n * Encodes raw AES key bytes for storage in env vars or secret managers.\n *\n * @param bytes - 16, 24, or 32 raw key bytes (AES-128/192/256).\n * @param format - `hex` (lowercase) or `base64url`.\n */\nexport function encodeOpaqueKey(bytes: Uint8Array, format: OpaqueKeyFormat): string {\n return encodeKeyMaterial(bytes, format, \"opaque\", \"AES\");\n}\n\n/**\n * Decodes key material emitted by `encodeOpaqueKey` (or `ids keygen`) back to raw bytes.\n *\n * @param encoded - Hex or base64url string.\n * @param format - Must match how the string was encoded.\n */\nexport function decodeOpaqueKey(encoded: string, format: OpaqueKeyFormat): Uint8Array {\n return decodeKeyMaterial(encoded, format, \"opaque\", \"AES\");\n}\n","import { validateBrand } from \"./brand.js\";\nimport { createOpaqueLayoutOps } from \"./layouts/opaque.js\";\nimport { getOpaqueKeyCryptoKey, type OpaqueKey } from \"./opaque-key.js\";\nimport { registerBrand } from \"./registry.js\";\nimport { defaultRng } from \"./rng.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nexport {\n decodeOpaqueKey,\n encodeOpaqueKey,\n importOpaqueKey,\n type OpaqueKey,\n type OpaqueKeyFormat,\n} from \"./opaque-key.js\";\n\n/**\n * Configuration options for an Opaque Timestamp codec instance.\n */\nexport type OpaqueTimestampOptions = {\n /**\n * {@link OpaqueKey} handle for AES-CBC encryption and decryption.\n * Obtain via {@link importOpaqueKey}.\n *\n * A single key, not a ring: rotation is forward-only and caller-tracked —\n * hold one codec per key epoch and select it from your own records. The\n * library cannot trial keys (the payload is unauthenticated). See ADR-0013.\n */\n key: OpaqueKey;\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * A brand-scoped codec for generating and validating Opaque Timestamp IDs.\n *\n * Same wire shape as the Timestamp codec (`{brand}_` + 26 base32 chars) but the\n * payload is AES-CBC encrypted. `generate`, `generateAt`, and `extractTimestamp`\n * are async; parsing methods are sync. No `minIdForTime` / `maxIdForTime` —\n * encrypted payloads do not sort by creation time.\n */\nexport type OpaqueTimestampCodec<Brand extends string> = {\n /** Produces a new canonical encrypted ID using the codec's `now` and `rng`. */\n generate(): Promise<Id<Brand>>;\n /** Produces a new canonical encrypted ID with timestamp bytes from `date`. Throws on invalid dates. */\n generateAt(date: Date): Promise<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 * Decrypts and decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n *\n * Requires the same key used at generation; a wrong key returns a plausible\n * but wrong `Date`, never an error. With rotation, select the codec for the\n * ID's key epoch from your own records — the library cannot. See ADR-0013.\n */\n extractTimestamp(id: Id<Brand>): Promise<Date>;\n /** JSON Schema for the canonical wire form (`example` is a structural placeholder). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\n/**\n * Creates an Opaque Timestamp codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Required `key` (an {@link OpaqueKey} from {@link importOpaqueKey}) plus\n * optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createOpaqueTimestampId<Brand extends string>(\n brand: Brand,\n opts: OpaqueTimestampOptions,\n): OpaqueTimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const cryptoKey = getOpaqueKeyCryptoKey(opts.key);\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 = createOpaqueLayoutOps(prefix, cryptoKey, 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 toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":"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"}
package/dist/opaque.d.mts CHANGED
@@ -52,6 +52,10 @@ type OpaqueTimestampOptions = {
52
52
  /**
53
53
  * {@link OpaqueKey} handle for AES-CBC encryption and decryption.
54
54
  * Obtain via {@link importOpaqueKey}.
55
+ *
56
+ * A single key, not a ring: rotation is forward-only and caller-tracked —
57
+ * hold one codec per key epoch and select it from your own records. The
58
+ * library cannot trial keys (the payload is unauthenticated). See ADR-0013.
55
59
  */
56
60
  key: OpaqueKey; /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */
57
61
  now?: () => number; /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */
@@ -84,6 +88,10 @@ type OpaqueTimestampCodec<Brand extends string> = {
84
88
  safeParse(value: unknown): ParseResult<Brand>;
85
89
  /**
86
90
  * Decrypts and decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.
91
+ *
92
+ * Requires the same key used at generation; a wrong key returns a plausible
93
+ * but wrong `Date`, never an error. With rotation, select the codec for the
94
+ * ID's key epoch from your own records — the library cannot. See ADR-0013.
87
95
  */
88
96
  extractTimestamp(id: Id<Brand>): Promise<Date>; /** JSON Schema for the canonical wire form (`example` is a structural placeholder). */
89
97
  toJsonSchema(): JsonSchema; /** Standard Schema validate entry point. */
@@ -1 +1 @@
1
- {"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/opaque-key.ts","../src/opaque.ts"],"mappings":";;;;;KAIY,eAAA;AAAA,cAIE,cAAA;;AAJd;;;;AAAY;AAA0B;;;;KAgB1B,SAAA;EAAA,UACA,cAAA;AAAA;;;AAAA;AAeZ;;;;;;;iBAAsB,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;;;;;AAAA;AA4BlE;iBAAgB,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;;;;iBAa3C,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;;;KCzD/D,sBAAA;EDhB0B;;;;ECqBpC,GAAA,EAAK,SAAA,EDLP;ECOE,GAAA;EAEA,GAAA,IAAO,MAAA,EAAQ,UAAA,WDRL;ECUV,mBAAA;AAAA;;;;;;;;;KAWU,oBAAA;iFAEV,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA,IDRyC;ECUhE,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;EDkBrB;;;;ECbd,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;;;EAIhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EDsB5B;;;EClBE,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;EAIvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA,CAAQ,IAAA,GDcgC;ECZzE,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;AA/C5C;;;;;;;AAAA,iBA6DgB,uBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
1
+ {"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/opaque-key.ts","../src/opaque.ts"],"mappings":";;;;;KAOY,eAAA;AAAA,cAEE,cAAA;;AAFd;;;;AAAY;AAA0B;;;;KAc1B,SAAA;EAAA,UACA,cAAA;AAAA;;;AAAA;AAeZ;;;;;;;iBAAsB,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;;;;;AAAA;AA4BlE;iBAAgB,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;;;;iBAU3C,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;;;KCtD/D,sBAAA;EDd0B;;;;AAExB;AAYd;;;ECSE,GAAA,EAAK,SAAA,EDRK;ECUV,GAAA,iBDKoB;ECHpB,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;;;;;;;;ADCgE;KCUtD,oBAAA;EDkBI,+EChBd,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA,IDgBkC;ECdzD,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;;;;;EAKnC,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;EDmBlC;;;ECfE,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;EAI1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;EDWkC;AAAA;;;;ACtD3E;;EAmDE,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA,CAAQ,IAAA,GAtC1B;EAwCf,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;AAxC1C;AAWF;;iBAuCgB,uBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
package/dist/opaque.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-uvjOFY_0.mjs";
2
+ import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-BpqxV8oB.mjs";
3
3
  export { IdsError, createOpaqueTimestampId, decodeOpaqueKey, encodeOpaqueKey, importOpaqueKey, isIdsError };
package/dist/prisma.d.mts CHANGED
@@ -1,22 +1,9 @@
1
1
  import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
2
- import { i as ParseResult, t as Id } from "./types-g7CiQDyE.mjs";
2
+ import { t as Id } from "./types-g7CiQDyE.mjs";
3
+ import { n as IdColumnCodec } from "./adapter-types-unUcmMXC.mjs";
3
4
 
4
5
  //#region src/prisma.d.ts
5
6
  /**
6
- * Minimum codec interface required by the Prisma adapter.
7
- *
8
- * Any codec variant satisfies this type — TimestampCodec, OpaqueTimestampCodec,
9
- * ReverseTimestampCodec, and WrappedKeyCodec all expose `safeParse`. The adapter
10
- * never calls key-dependent methods.
11
- *
12
- * Intentionally the same structural shape as the Drizzle adapter's IdColumnCodec.
13
- * Do NOT import IdColumnCodec from `@smonn/ids/drizzle` — that would create
14
- * cross-adapter coupling.
15
- */
16
- type IdColumnCodec<Brand extends string> = {
17
- safeParse(value: unknown): ParseResult<Brand>;
18
- };
19
- /**
20
7
  * Read/write transform pair for integrating `Id<Brand>` with Prisma extensions.
21
8
  *
22
9
  * **Prisma casting caveat:** Prisma cannot fully brand a generated model field
@@ -46,8 +33,7 @@ type IdTransform<Brand extends string> = {
46
33
  /**
47
34
  * Creates a read/write transform pair for use with Prisma's `$extends` extension model.
48
35
  *
49
- * Works with any codec variant exposing `safeParse` (TimestampCodec,
50
- * OpaqueTimestampCodec, ReverseTimestampCodec, WrappedKeyCodec).
36
+ * Works with any codec variant exposing `safeParse`.
51
37
  *
52
38
  * **Prisma casting caveat:** Prisma's `$extends` result component can add
53
39
  * typed computed accessors to model instances, but cannot retroactively
@@ -81,5 +67,5 @@ type IdTransform<Brand extends string> = {
81
67
  */
82
68
  declare function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand>;
83
69
  //#endregion
84
- export { IdColumnCodec, IdTransform, IdsError, type IdsErrorCode, idField, isIdsError };
70
+ export { type IdColumnCodec, IdTransform, IdsError, type IdsErrorCode, idField, isIdsError };
85
71
  //# sourceMappingURL=prisma.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/prisma.ts"],"mappings":";;;;AAiBA;;;;;;;;;;AACyC;AADzC,KAAY,aAAA;EACV,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;;;;;;KAY7B,WAAA;;;;;;;AAgBM;EARhB,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;EA+CX;;;;;;;EAvCd,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;AAAA;;;;;;;AAuCsE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAxE,OAAA,uBAA8B,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,WAAA,CAAY,KAAA"}
1
+ {"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/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"}
package/dist/prisma.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
+ import { t as readIdColumn } from "./adapter-types-BY-wrYYB.mjs";
2
3
  //#region src/prisma.ts
3
4
  /**
4
5
  * Creates a read/write transform pair for use with Prisma's `$extends` extension model.
5
6
  *
6
- * Works with any codec variant exposing `safeParse` (TimestampCodec,
7
- * OpaqueTimestampCodec, ReverseTimestampCodec, WrappedKeyCodec).
7
+ * Works with any codec variant exposing `safeParse`.
8
8
  *
9
9
  * **Prisma casting caveat:** Prisma's `$extends` result component can add
10
10
  * typed computed accessors to model instances, but cannot retroactively
@@ -39,9 +39,7 @@ import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
39
39
  function idField(codec) {
40
40
  return {
41
41
  read(value) {
42
- const result = codec.safeParse(value);
43
- if (!result.ok) throw new IdsError("invalid_id", `invalid ID from database: ${result.error}`, { cause: result.error });
44
- return result.id;
42
+ return readIdColumn(codec, value);
45
43
  },
46
44
  write(value) {
47
45
  return value;
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.mjs","names":[],"sources":["../src/prisma.ts"],"sourcesContent":["import { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport type { Id, ParseResult } from \"./types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\n\n/**\n * Minimum codec interface required by the Prisma adapter.\n *\n * Any codec variant satisfies this type — TimestampCodec, OpaqueTimestampCodec,\n * ReverseTimestampCodec, and WrappedKeyCodec all expose `safeParse`. The adapter\n * never calls key-dependent methods.\n *\n * Intentionally the same structural shape as the Drizzle adapter's IdColumnCodec.\n * Do NOT import IdColumnCodec from `@smonn/ids/drizzle` — that would create\n * cross-adapter coupling.\n */\nexport type IdColumnCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\n\n/**\n * Read/write transform pair for integrating `Id<Brand>` with Prisma extensions.\n *\n * **Prisma casting caveat:** Prisma cannot fully brand a generated model field\n * type at the schema level. The `read` function asserts `Id<Brand>` at the\n * TypeScript level, but Prisma's generated types for the model field will not\n * reflect this branding. Callers consuming the validated value from a Prisma\n * result component may need an explicit `as Id<Brand>` cast at the call site.\n */\nexport type IdTransform<Brand extends string> = {\n /**\n * Read transform: validates the raw database value via `safeParse` and returns\n * `Id<Brand>`. Throws if the value is missing, malformed, or belongs to a\n * different brand.\n *\n * Use in a Prisma `$extends` result component's `compute` function.\n */\n read(value: unknown): Id<Brand>;\n /**\n * Write transform: passes `Id<Brand>` through as its canonical string form.\n * `Id<Brand>` is already the canonical string, so this is an identity function\n * at runtime.\n *\n * Use in a Prisma `$extends` query component or explicit `data` mapping.\n */\n write(value: Id<Brand>): string;\n};\n\n/**\n * Creates a read/write transform pair for use with Prisma's `$extends` extension model.\n *\n * Works with any codec variant exposing `safeParse` (TimestampCodec,\n * OpaqueTimestampCodec, ReverseTimestampCodec, WrappedKeyCodec).\n *\n * **Prisma casting caveat:** Prisma's `$extends` result component can add\n * typed computed accessors to model instances, but cannot retroactively\n * re-type an existing schema field at the Prisma Client level. The `read`\n * function asserts `Id<Brand>`, but callers will need an explicit\n * `as Id<Brand>` cast at consumption sites where Prisma's generated types\n * are expected.\n *\n * @example\n * ```ts\n * import { idField } from \"@smonn/ids/prisma\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const userIdField = idField(usr);\n *\n * const xprisma = prisma.$extends({\n * result: {\n * user: {\n * id: {\n * needs: { id: true },\n * compute(user) {\n * // Cast required: Prisma cannot brand the generated type at schema level\n * return userIdField.read(user.id) as Id<\"usr\">;\n * },\n * },\n * },\n * },\n * });\n * ```\n */\nexport function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand> {\n return {\n read(value: unknown): Id<Brand> {\n const result = codec.safeParse(value);\n if (!result.ok) {\n throw new IdsError(\"invalid_id\", `invalid ID from database: ${result.error}`, {\n cause: result.error,\n });\n }\n return result.id;\n },\n write(value: Id<Brand>): string {\n return value;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqFA,SAAgB,QAA8B,OAAiD;CAC7F,OAAO;EACL,KAAK,OAA2B;GAC9B,MAAM,SAAS,MAAM,UAAU,KAAK;GACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,SAAS,cAAc,6BAA6B,OAAO,SAAS,EAC5E,OAAO,OAAO,MAChB,CAAC;GAEH,OAAO,OAAO;EAChB;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;CACF;AACF"}
1
+ {"version":3,"file":"prisma.mjs","names":[],"sources":["../src/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,5 +1,6 @@
1
- import { a as toWireId, i as payloadBytesFromId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-DH-UO4UR.mjs";
2
- import { r as writeTimestamp } from "./timestamp-bytes-BBY7JI33.mjs";
1
+ import { a as toWireId, i as payloadBytesFromId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-CW2sD6BU.mjs";
2
+ import { r as writeTimestamp } from "./timestamp-bytes-DoFjLjDp.mjs";
3
+ import { t as defaultRng } from "./rng-CPJOx_nE.mjs";
3
4
  //#region src/layouts/reverse-timestamp.ts
4
5
  const randomByteLength = 10;
5
6
  /** Writes inverted timestamp bytes, then fills random portion. */
@@ -23,7 +24,7 @@ function extractReverseTimestampFromId(prefix, id) {
23
24
  }
24
25
  /** Layout ops binder for the Reverse Timestamp variant. */
25
26
  function createReverseTimestampLayoutOps(prefix, rng) {
26
- const buffer = new Uint8Array(16);
27
+ const buffer = /* @__PURE__ */ new Uint8Array(16);
27
28
  const randomView = new Uint8Array(buffer.buffer, 6, randomByteLength);
28
29
  return {
29
30
  generateAt: (ms) => {
@@ -47,9 +48,6 @@ function createReverseTimestampLayoutOps(prefix, rng) {
47
48
  }
48
49
  //#endregion
49
50
  //#region src/reverse.ts
50
- function defaultRng(target) {
51
- crypto.getRandomValues(target);
52
- }
53
51
  /**
54
52
  * Creates a Reverse Timestamp codec for `brand` (three lowercase a–z characters).
55
53
  *
@@ -84,4 +82,4 @@ function createReverseTimestampId(brand, opts = {}) {
84
82
  //#endregion
85
83
  export { createReverseTimestampId as t };
86
84
 
87
- //# sourceMappingURL=reverse-BgFU6JHw.mjs.map
85
+ //# sourceMappingURL=reverse-d5uEoIET.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reverse-d5uEoIET.mjs","names":[],"sources":["../src/layouts/reverse-timestamp.ts","../src/reverse.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { payloadBytesFromId, toWireId } from \"../wire/envelope.js\";\nimport { payloadByteLength } from \"../wire/invariants.js\";\nimport { timestampByteLength, writeTimestamp } from \"../wire/timestamp-bytes.js\";\n\nconst randomByteLength: number = payloadByteLength - timestampByteLength;\n\n/** Writes inverted timestamp bytes, then fills random portion. */\nfunction buildReversePayload(\n ms: number,\n rng: (target: Uint8Array) => void,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n for (let i = 0; i < timestampByteLength; i++) {\n buffer[i] = ~buffer[i]! & 0xff;\n }\n rng(randomView);\n}\n\n/** Writes inverted timestamp bytes, then fills random portion with a sentinel. */\nfunction buildReverseSentinelPayload(\n ms: number,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n for (let i = 0; i < timestampByteLength; i++) {\n buffer[i] = ~buffer[i]! & 0xff;\n }\n randomView.fill(fill);\n}\n\n/** Decodes the original timestamp by inverting the first 6 payload bytes. */\nfunction extractReverseTimestampFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n id: Id<Brand>,\n): Date {\n const bytes = payloadBytesFromId(prefix, id);\n let ms = 0;\n for (let i = 0; i < timestampByteLength; i++) {\n ms = ms * 256 + (~bytes[i]! & 0xff);\n }\n return new Date(ms);\n}\n\n/** Layout ops binder for the Reverse Timestamp variant. */\nexport function createReverseTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n rng: (target: Uint8Array) => void,\n) {\n const buffer = new Uint8Array(payloadByteLength);\n const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);\n\n return {\n generateAt: (ms: number): Id<Brand> => {\n buildReversePayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n extractTimestamp: (id: Id<Brand>): Date => extractReverseTimestampFromId(prefix, id),\n minIdForTime: (ms: number): Id<Brand> => {\n buildReverseSentinelPayload(ms, 0x00, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n maxIdForTime: (ms: number): Id<Brand> => {\n buildReverseSentinelPayload(ms, 0xff, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n exampleWireId: (ms: number): Id<Brand> => {\n buildReversePayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n };\n}\n","import { validateBrand } from \"./brand.js\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport { createReverseTimestampLayoutOps } from \"./layouts/reverse-timestamp.js\";\nimport { registerBrand } from \"./registry.js\";\nimport { defaultRng } from \"./rng.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\n\n/**\n * Configuration options for a Reverse Timestamp codec instance.\n */\nexport type ReverseTimestampOptions = {\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * A brand-scoped codec for generating and validating Reverse Timestamp IDs.\n *\n * Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a\n * 16-byte payload (6-byte bitwise-inverted ms timestamp + 10 random bytes). IDs sort\n * by creation time in **descending** (newest-first) order.\n *\n * Range queries across a time interval [t_old, t_new] should scan from\n * `minIdForTime(t_new)` to `maxIdForTime(t_old)` — the reversed sort order means\n * newer timestamps produce lexicographically smaller IDs.\n *\n * Constructed via `createReverseTimestampId(brand)` from `@smonn/ids/reverse`.\n */\nexport type ReverseTimestampCodec<Brand extends string> = {\n /** Produces a new canonical ID using the codec's `now` and `rng`. */\n generate(): Id<Brand>;\n /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */\n generateAt(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>` by inverting the timestamp bytes.\n * Trusts the type — use `safeParse()` at boundaries first.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /**\n * Lexicographically smallest ID for any ID generated at `date` (random portion `0x00`).\n * Because timestamps are inverted, a newer `date` yields a lexicographically smaller result —\n * use `minIdForTime(t_new)` as the lower bound when scanning [t_old, t_new].\n * Throws on invalid dates.\n */\n minIdForTime(date: Date): Id<Brand>;\n /**\n * Lexicographically largest ID for any ID generated at `date` (random portion `0xff`).\n * Because timestamps are inverted, an older `date` yields a lexicographically larger result —\n * use `maxIdForTime(t_old)` as the upper bound when scanning [t_old, t_new].\n * Throws on invalid dates.\n */\n maxIdForTime(date: Date): Id<Brand>;\n /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\n/**\n * Creates a Reverse Timestamp codec for `brand` (three lowercase a–z characters).\n *\n * IDs sort newest-first: the 48-bit timestamp field is bitwise-inverted before encoding,\n * so lexicographic ID order equals descending creation-time order. `extractTimestamp`\n * inverts back to recover the original millisecond.\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createReverseTimestampId<Brand extends string>(\n brand: Brand,\n opts: ReverseTimestampOptions = {},\n): ReverseTimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const now = opts.now ?? Date.now;\n const rng = opts.rng ?? defaultRng;\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createReverseTimestampLayoutOps(prefix, rng);\n\n return {\n generate: () => layout.generateAt(now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n minIdForTime: (date: Date) => layout.minIdForTime(date.getTime()),\n maxIdForTime: (date: Date) => layout.maxIdForTime(date.getTime()),\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(now())),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":";;;;AAKA,MAAM,mBAAA;;AAGN,SAAS,oBACP,IACA,KACA,QACA,YACM;CACN,eAAe,IAAI,MAAM;CACzB,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KACvC,OAAO,KAAK,CAAC,OAAO,KAAM;CAE5B,IAAI,UAAU;AAChB;;AAGA,SAAS,4BACP,IACA,MACA,QACA,YACM;CACN,eAAe,IAAI,MAAM;CACzB,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KACvC,OAAO,KAAK,CAAC,OAAO,KAAM;CAE5B,WAAW,KAAK,IAAI;AACtB;;AAGA,SAAS,8BACP,QACA,IACM;CACN,MAAM,QAAQ,mBAAmB,QAAQ,EAAE;CAC3C,IAAI,KAAK;CACT,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KACvC,KAAK,KAAK,OAAO,CAAC,MAAM,KAAM;CAEhC,OAAO,IAAI,KAAK,EAAE;AACpB;;AAGA,SAAgB,gCACd,QACA,KACA;CACA,MAAM,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,OAA0B;GACxC,oBAAoB,IAAI,KAAK,QAAQ,UAAU;GAC/C,OAAO,SAAS,QAAQ,MAAM;EAChC;CACF;AACF;;;;;;;;;;;;;ACcA,SAAgB,yBACd,OACA,OAAgC,CAAC,GACH;CAC9B,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAE7C,MAAM,MAAM,KAAK,OAAO,KAAK;CAC7B,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,SAAwB,GAAG,MAAM;CACvC,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,SAAS,gCAAgC,QAAQ,GAAG;CAE1D,OAAO;EACL,gBAAgB,OAAO,WAAW,IAAI,CAAC;EACvC,aAAa,SAAe,OAAO,WAAW,KAAK,QAAQ,CAAC;EAC5D,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,WAAW,KAAK;EAChB,kBAAkB,OAAO;EACzB,eAAe,SAAe,OAAO,aAAa,KAAK,QAAQ,CAAC;EAChE,eAAe,SAAe,OAAO,aAAa,KAAK,QAAQ,CAAC;EAChE,oBAAoB,KAAK,aAAa,OAAO,OAAO,cAAc,IAAI,CAAC,CAAC;EACxE,aAAa,KAAK;CACpB;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"reverse.d.mts","names":[],"sources":["../src/reverse.ts"],"mappings":";;;;AAaA;;;AAAA,KAAY,uBAAA;+EAEV,GAAA;EAEA,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;AAgBF;;;;;;;;;;;;;AAAA,KAAY,qBAAA;uEAEV,QAAA,IAAY,EAAA,CAAG,KAAA;EAEf,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;EAK3B,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;;;EAIhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EA2BJ;;;EAvBtB,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;;EAKvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;;;;;;;EAOjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;;;EAO7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;;;;;iBAiB5B,wBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,GAAM,uBAAA,GACL,qBAAA,CAAsB,KAAA"}
1
+ {"version":3,"file":"reverse.d.mts","names":[],"sources":["../src/reverse.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"}
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-BgFU6JHw.mjs";
2
+ import { t as createReverseTimestampId } from "./reverse-d5uEoIET.mjs";
3
3
  export { IdsError, createReverseTimestampId, isIdsError };
@@ -0,0 +1,9 @@
1
+ //#region src/rng.ts
2
+ /** Default RNG: writes cryptographically random bytes via `crypto.getRandomValues`. */
3
+ function defaultRng(target) {
4
+ crypto.getRandomValues(target);
5
+ }
6
+ //#endregion
7
+ export { defaultRng as t };
8
+
9
+ //# sourceMappingURL=rng-CPJOx_nE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rng-CPJOx_nE.mjs","names":[],"sources":["../src/rng.ts"],"sourcesContent":["/** Default RNG: writes cryptographically random bytes via `crypto.getRandomValues`. */\nexport function defaultRng(target: Uint8Array): void {\n crypto.getRandomValues(target as Uint8Array<ArrayBuffer>);\n}\n"],"mappings":";;AACA,SAAgB,WAAW,QAA0B;CACnD,OAAO,gBAAgB,MAAiC;AAC1D"}