@smonn/ids 0.9.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +7 -5
  2. package/dist/{adapter-types-BY-wrYYB.mjs → adapter-types-7wWdELSh.mjs} +2 -2
  3. package/dist/adapter-types-7wWdELSh.mjs.map +1 -0
  4. package/dist/{adapter-types-unUcmMXC.d.mts → adapter-types-CdYJM6Sf.d.mts} +2 -2
  5. package/dist/adapter-types-CdYJM6Sf.d.mts.map +1 -0
  6. package/dist/cli.mjs +97 -13
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/{codec-shell-CW2sD6BU.mjs → codec-shell-DvrTDa65.mjs} +4 -4
  9. package/dist/codec-shell-DvrTDa65.mjs.map +1 -0
  10. package/dist/digest-CknNw2wa.mjs +147 -0
  11. package/dist/digest-CknNw2wa.mjs.map +1 -0
  12. package/dist/digest.d.mts +124 -0
  13. package/dist/digest.d.mts.map +1 -0
  14. package/dist/digest.mjs +3 -0
  15. package/dist/drizzle.d.mts +3 -3
  16. package/dist/drizzle.d.mts.map +1 -1
  17. package/dist/drizzle.mjs +2 -2
  18. package/dist/drizzle.mjs.map +1 -1
  19. package/dist/error-Cp5qYZcv.mjs.map +1 -1
  20. package/dist/{error-DTr4i6Ic.d.mts → error-JIPylU_E.d.mts} +2 -2
  21. package/dist/{error-DTr4i6Ic.d.mts.map → error-JIPylU_E.d.mts.map} +1 -1
  22. package/dist/express.d.mts +2 -2
  23. package/dist/express.d.mts.map +1 -1
  24. package/dist/express.mjs +2 -2
  25. package/dist/express.mjs.map +1 -1
  26. package/dist/fastify.d.mts +3 -3
  27. package/dist/fastify.d.mts.map +1 -1
  28. package/dist/fastify.mjs +3 -3
  29. package/dist/fastify.mjs.map +1 -1
  30. package/dist/hono.d.mts +5 -4
  31. package/dist/hono.d.mts.map +1 -1
  32. package/dist/hono.mjs +4 -3
  33. package/dist/hono.mjs.map +1 -1
  34. package/dist/index.d.mts +2 -2
  35. package/dist/index.d.mts.map +1 -1
  36. package/dist/index.mjs +1 -1
  37. package/dist/{key-material-gOnqTNoV.mjs → key-material-f29JIyrz.mjs} +3 -3
  38. package/dist/key-material-f29JIyrz.mjs.map +1 -0
  39. package/dist/kysely.d.mts +3 -3
  40. package/dist/kysely.d.mts.map +1 -1
  41. package/dist/kysely.mjs +2 -2
  42. package/dist/kysely.mjs.map +1 -1
  43. package/dist/{opaque-BpqxV8oB.mjs → opaque-ayT0KdCt.mjs} +8 -8
  44. package/dist/opaque-ayT0KdCt.mjs.map +1 -0
  45. package/dist/opaque.d.mts +4 -4
  46. package/dist/opaque.d.mts.map +1 -1
  47. package/dist/opaque.mjs +1 -1
  48. package/dist/prisma.d.mts +3 -3
  49. package/dist/prisma.d.mts.map +1 -1
  50. package/dist/prisma.mjs +2 -2
  51. package/dist/prisma.mjs.map +1 -1
  52. package/dist/{reverse-d5uEoIET.mjs → reverse-BRZRc1_U.mjs} +6 -6
  53. package/dist/reverse-BRZRc1_U.mjs.map +1 -0
  54. package/dist/reverse.d.mts +2 -2
  55. package/dist/reverse.d.mts.map +1 -1
  56. package/dist/reverse.mjs +1 -1
  57. package/dist/{rng-CPJOx_nE.mjs → rng-DHxioKyI.mjs} +2 -2
  58. package/dist/rng-DHxioKyI.mjs.map +1 -0
  59. package/dist/{signed-BnRSC03a.mjs → signed-C8OMt3TJ.mjs} +10 -10
  60. package/dist/signed-C8OMt3TJ.mjs.map +1 -0
  61. package/dist/signed.d.mts +5 -5
  62. package/dist/signed.d.mts.map +1 -1
  63. package/dist/signed.mjs +1 -1
  64. package/dist/{timestamp-BbZL8hwg.mjs → timestamp-DBwVjDkg.mjs} +5 -5
  65. package/dist/timestamp-DBwVjDkg.mjs.map +1 -0
  66. package/dist/{timestamp-bytes-DoFjLjDp.mjs → timestamp-bytes-DvhWHDa-.mjs} +2 -2
  67. package/dist/timestamp-bytes-DvhWHDa-.mjs.map +1 -0
  68. package/dist/{wrapped-BI9UXnAm.mjs → wrapped-CDTiPwNM.mjs} +29 -12
  69. package/dist/wrapped-CDTiPwNM.mjs.map +1 -0
  70. package/dist/wrapped.d.mts +4 -4
  71. package/dist/wrapped.d.mts.map +1 -1
  72. package/dist/wrapped.mjs +1 -1
  73. package/package.json +5 -3
  74. package/dist/adapter-types-BY-wrYYB.mjs.map +0 -1
  75. package/dist/adapter-types-unUcmMXC.d.mts.map +0 -1
  76. package/dist/codec-shell-CW2sD6BU.mjs.map +0 -1
  77. package/dist/key-material-gOnqTNoV.mjs.map +0 -1
  78. package/dist/opaque-BpqxV8oB.mjs.map +0 -1
  79. package/dist/reverse-d5uEoIET.mjs.map +0 -1
  80. package/dist/rng-CPJOx_nE.mjs.map +0 -1
  81. package/dist/signed-BnRSC03a.mjs.map +0 -1
  82. package/dist/timestamp-BbZL8hwg.mjs.map +0 -1
  83. package/dist/timestamp-bytes-DoFjLjDp.mjs.map +0 -1
  84. package/dist/wrapped-BI9UXnAm.mjs.map +0 -1
@@ -0,0 +1,147 @@
1
+ import { t as IdsError } from "./error-Cp5qYZcv.mjs";
2
+ import { a as toWireId, n as registerBrand, r as payloadBase32Length, s as validateBrand, t as wireMethods } from "./codec-shell-DvrTDa65.mjs";
3
+ import { i as encodeKeyMaterial, r as decodeKeyMaterial, t as assertValidKeyMaterialByteLength } from "./key-material-f29JIyrz.mjs";
4
+ //#region src/codecs/digest/layout.ts
5
+ const encoder = new TextEncoder();
6
+ function writeLen32(value, target, offset) {
7
+ target[offset] = value >>> 24 & 255;
8
+ target[offset + 1] = value >>> 16 & 255;
9
+ target[offset + 2] = value >>> 8 & 255;
10
+ target[offset + 3] = value & 255;
11
+ }
12
+ function buildMessage(brandBytes, nsBytes, material) {
13
+ const msgLen = 4 + brandBytes.length + 4 + nsBytes.length + material.length;
14
+ const message = new Uint8Array(msgLen);
15
+ let offset = 0;
16
+ writeLen32(brandBytes.length, message, offset);
17
+ offset += 4;
18
+ message.set(brandBytes, offset);
19
+ offset += brandBytes.length;
20
+ writeLen32(nsBytes.length, message, offset);
21
+ offset += 4;
22
+ message.set(nsBytes, offset);
23
+ offset += nsBytes.length;
24
+ message.set(material, offset);
25
+ return message;
26
+ }
27
+ function createDigestLayoutOps(prefix, brand, ns, hmacKey) {
28
+ const brandBytes = encoder.encode(brand);
29
+ const nsBytes = encoder.encode(ns);
30
+ return {
31
+ digest: async (material) => {
32
+ const message = buildMessage(brandBytes, nsBytes, typeof material === "string" ? encoder.encode(material) : material);
33
+ return toWireId(prefix, new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, message)).subarray(0, 16));
34
+ },
35
+ exampleWireId: () => prefix + "0".repeat(payloadBase32Length)
36
+ };
37
+ }
38
+ //#endregion
39
+ //#region src/codecs/digest/key.ts
40
+ const hmacInfo = new TextEncoder().encode("@smonn/ids/digest/hmac");
41
+ const internals = /* @__PURE__ */ new WeakMap();
42
+ /**
43
+ * Import raw operator key material into a {@link DigestKey} handle.
44
+ *
45
+ * Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label
46
+ * `@smonn/ids/digest/hmac`. Accepts 16, 24, or 32 bytes. To store or transport key
47
+ * material, use {@link encodeDigestKey} / {@link decodeDigestKey}
48
+ * (`"hex"` or `"base64url"` — not Crockford base32).
49
+ *
50
+ * @param bytes - 16, 24, or 32 raw key bytes.
51
+ * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
52
+ */
53
+ async function importDigestKey(bytes) {
54
+ assertValidKeyMaterialByteLength(bytes.length, "digest");
55
+ const hmacKey = await deriveHmacKey(bytes);
56
+ const key = Object.freeze({});
57
+ internals.set(key, { hmacKey });
58
+ return key;
59
+ }
60
+ /**
61
+ * Encode raw digest operator key material for storage in env vars or secret managers.
62
+ *
63
+ * Supports `"hex"` (lowercase) and `"base64url"`. Output round-trips through
64
+ * {@link decodeDigestKey} back to the original bytes.
65
+ *
66
+ * @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
67
+ * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
68
+ */
69
+ function encodeDigestKey(bytes, format) {
70
+ return encodeKeyMaterial(bytes, format, "digest", "digest");
71
+ }
72
+ /**
73
+ * Decode key material emitted by {@link encodeDigestKey} back to raw bytes.
74
+ *
75
+ * The result can be passed directly to {@link importDigestKey}.
76
+ *
77
+ * @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
78
+ * @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.
79
+ * @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.
80
+ */
81
+ function decodeDigestKey(encoded, format) {
82
+ return decodeKeyMaterial(encoded, format, "digest", "digest");
83
+ }
84
+ /**
85
+ * Returns the derived HMAC webcrypto.CryptoKey held inside the handle.
86
+ *
87
+ * Intentional module-internal escape hatch for codec implementations.
88
+ * Not re-exported from `@smonn/ids/digest`; external callers cannot reach this.
89
+ */
90
+ function getDigestKeyHmacKey(key) {
91
+ const keyInternals = internals.get(key);
92
+ /* v8 ignore next -- defensive guard; only reachable with a forged DigestKey handle */
93
+ if (keyInternals === void 0) throw new Error("invalid digest key");
94
+ return keyInternals.hmacKey;
95
+ }
96
+ async function deriveHmacKey(bytes) {
97
+ const base = await crypto.subtle.importKey("raw", bytes, "HKDF", false, ["deriveKey"]);
98
+ return crypto.subtle.deriveKey({
99
+ name: "HKDF",
100
+ hash: "SHA-256",
101
+ salt: /* @__PURE__ */ new Uint8Array(),
102
+ info: hmacInfo
103
+ }, base, {
104
+ name: "HMAC",
105
+ hash: "SHA-256",
106
+ length: 256
107
+ }, false, ["sign"]);
108
+ }
109
+ //#endregion
110
+ //#region src/codecs/digest/index.ts
111
+ /**
112
+ * Construct a {@link DigestCodec} for `brand`.
113
+ *
114
+ * `opts.ns` is the required namespace — the same material under a
115
+ * different `ns` yields a different ID. `opts.key` is the single operator
116
+ * Digest key; there is no keyring.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * const key = await importDigestKey(new Uint8Array(32));
121
+ * const idk = createDigestId("idk", { ns: "checkout", key });
122
+ *
123
+ * const id = await idk.digest("order-123"); // Id<"idk">
124
+ * idk.is(id); // true
125
+ * ```
126
+ */
127
+ function createDigestId(brand, opts) {
128
+ validateBrand(brand);
129
+ registerBrand(brand, opts.allowDuplicateBrand);
130
+ if (typeof opts.ns !== "string" || opts.ns.trim() === "") throw new IdsError("invalid_namespace", "invalid namespace: ns must be a non-empty, non-whitespace string");
131
+ const hmacKey = getDigestKeyHmacKey(opts.key);
132
+ const prefix = `${brand}_`;
133
+ const wire = wireMethods(prefix);
134
+ const layout = createDigestLayoutOps(prefix, brand, opts.ns, hmacKey);
135
+ return {
136
+ digest: layout.digest,
137
+ is: wire.is,
138
+ parse: wire.parse,
139
+ safeParse: wire.safeParse,
140
+ toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),
141
+ "~standard": wire["~standard"]
142
+ };
143
+ }
144
+ //#endregion
145
+ export { importDigestKey as i, decodeDigestKey as n, encodeDigestKey as r, createDigestId as t };
146
+
147
+ //# sourceMappingURL=digest-CknNw2wa.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest-CknNw2wa.mjs","names":[],"sources":["../src/codecs/digest/layout.ts","../src/codecs/digest/key.ts","../src/codecs/digest/index.ts"],"sourcesContent":["import type { webcrypto } from \"node:crypto\";\nimport type { Id, Prefix } from \"../../types.js\";\nimport { toWireId } from \"../../wire/envelope.js\";\nimport { payloadBase32Length, payloadByteLength } from \"../../wire/invariants.js\";\n\nconst encoder = new TextEncoder();\n\nfunction writeLen32(value: number, target: Uint8Array, offset: number): void {\n target[offset] = (value >>> 24) & 0xff;\n target[offset + 1] = (value >>> 16) & 0xff;\n target[offset + 2] = (value >>> 8) & 0xff;\n target[offset + 3] = value & 0xff;\n}\n\nfunction buildMessage(\n brandBytes: Uint8Array,\n nsBytes: Uint8Array,\n material: Uint8Array,\n): Uint8Array {\n const msgLen = 4 + brandBytes.length + 4 + nsBytes.length + material.length;\n const message = new Uint8Array(msgLen);\n let offset = 0;\n writeLen32(brandBytes.length, message, offset);\n offset += 4;\n message.set(brandBytes, offset);\n offset += brandBytes.length;\n writeLen32(nsBytes.length, message, offset);\n offset += 4;\n message.set(nsBytes, offset);\n offset += nsBytes.length;\n message.set(material, offset);\n return message;\n}\n\nexport function createDigestLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n brand: Brand,\n ns: string,\n hmacKey: webcrypto.CryptoKey,\n) {\n const brandBytes = encoder.encode(brand);\n const nsBytes = encoder.encode(ns);\n\n return {\n digest: async (material: string | Uint8Array): Promise<Id<Brand>> => {\n const materialBytes = typeof material === \"string\" ? encoder.encode(material) : material;\n const message = buildMessage(brandBytes, nsBytes, materialBytes);\n const hmacOutput = new Uint8Array(\n await crypto.subtle.sign(\"HMAC\", hmacKey, message as Uint8Array<ArrayBuffer>),\n );\n const payload = hmacOutput.subarray(0, payloadByteLength);\n return toWireId(prefix, payload);\n },\n exampleWireId: (): Id<Brand> => (prefix + \"0\".repeat(payloadBase32Length)) as Id<Brand>,\n };\n}\n","import type { webcrypto } from \"node:crypto\";\nimport {\n assertValidKeyMaterialByteLength,\n decodeKeyMaterial,\n encodeKeyMaterial,\n} from \"../_kernel/key-material.js\";\n\n/** Wire encoding for digest operator key material (not Crockford base32). */\nexport type DigestKeyFormat = \"hex\" | \"base64url\";\n\nconst hmacInfo = new TextEncoder().encode(\"@smonn/ids/digest/hmac\");\n\ndeclare const digestKeyBrand: unique symbol;\n\n/**\n * Opaque imported handle for one operator Digest key.\n *\n * Holds a single HMAC-SHA-256 key derived via HKDF under the domain-separation\n * label `@smonn/ids/digest/hmac`. The underlying `webcrypto.CryptoKey` is held internally and\n * never exposed to callers. Obtain handles via {@link importDigestKey} and pass\n * them to `createDigestId` as the `key` option.\n *\n * Unlike the other keyed codecs, the Digest codec holds exactly one key — there\n * is no keyring. Re-keying is a deliberate, breaking operator action (every ID\n * changes), never an in-band rotation.\n *\n * Distinct from the **Opaque key**, **Wrapping key**, and **Signing key** — the\n * same raw bytes imported as a `DigestKey` are cryptographically independent of\n * any other codec's key.\n */\nexport type DigestKey = {\n readonly [digestKeyBrand]: \"DigestKey\";\n};\n\ntype DigestKeyInternals = {\n hmacKey: webcrypto.CryptoKey;\n};\n\nconst internals = new WeakMap<DigestKey, DigestKeyInternals>();\n\n/**\n * Import raw operator key material into a {@link DigestKey} handle.\n *\n * Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label\n * `@smonn/ids/digest/hmac`. Accepts 16, 24, or 32 bytes. To store or transport key\n * material, use {@link encodeDigestKey} / {@link decodeDigestKey}\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 importDigestKey(bytes: Uint8Array): Promise<DigestKey> {\n assertValidKeyMaterialByteLength(bytes.length, \"digest\");\n const hmacKey = await deriveHmacKey(bytes);\n const key = Object.freeze({}) as DigestKey;\n internals.set(key, { hmacKey });\n return key;\n}\n\n/**\n * Encode raw digest operator key material for storage in env vars or secret managers.\n *\n * Supports `\"hex\"` (lowercase) and `\"base64url\"`. Output round-trips through\n * {@link decodeDigestKey} 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 encodeDigestKey(bytes: Uint8Array, format: DigestKeyFormat): string {\n return encodeKeyMaterial(bytes, format, \"digest\", \"digest\");\n}\n\n/**\n * Decode key material emitted by {@link encodeDigestKey} back to raw bytes.\n *\n * The result can be passed directly to {@link importDigestKey}.\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 decodeDigestKey(encoded: string, format: DigestKeyFormat): Uint8Array {\n return decodeKeyMaterial(encoded, format, \"digest\", \"digest\");\n}\n\n/**\n * Returns the derived HMAC webcrypto.CryptoKey held inside the handle.\n *\n * Intentional module-internal escape hatch for codec implementations.\n * Not re-exported from `@smonn/ids/digest`; external callers cannot reach this.\n */\nexport function getDigestKeyHmacKey(key: DigestKey): webcrypto.CryptoKey {\n const keyInternals = internals.get(key);\n /* v8 ignore next -- defensive guard; only reachable with a forged DigestKey handle */\n if (keyInternals === undefined) throw new Error(\"invalid digest key\");\n return keyInternals.hmacKey;\n}\n\nasync function deriveHmacKey(bytes: Uint8Array): Promise<webcrypto.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\"],\n );\n}\n","import { validateBrand } from \"../_kernel/brand.js\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"../../error.js\";\nimport { createDigestLayoutOps } from \"./layout.js\";\nimport { registerBrand } from \"../_kernel/registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"../../types.js\";\nimport { wireMethods } from \"../../wire/codec-shell.js\";\nimport {\n decodeDigestKey,\n encodeDigestKey,\n getDigestKeyHmacKey,\n importDigestKey,\n type DigestKey,\n type DigestKeyFormat,\n} from \"./key.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\nexport { decodeDigestKey, encodeDigestKey, importDigestKey, type DigestKey, type DigestKeyFormat };\n\n/**\n * Configuration options for a Digest codec instance.\n */\nexport type DigestOptions = {\n /**\n * Non-secret, required namespace. The same material under a different\n * `ns` yields a different ID, so one key can serve multiple unlinkable namespaces.\n * Must be non-empty and not whitespace-only.\n */\n ns: string;\n /**\n * Single operator digest key. The Digest codec holds exactly one key — there\n * is no keyring. Re-keying is a deliberate, breaking operator action.\n */\n key: DigestKey;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * Codec returned by {@link createDigestId}.\n *\n * Maps caller **material** to a stable public ID under one **Digest key**:\n * the same material always yields the same ID, and the material cannot be\n * recovered from the ID (**equality leakage** is the intended property).\n *\n * - `digest` is async (WebCrypto HMAC).\n * - `is`, `parse`, `safeParse`, `toJsonSchema`, and `~standard` are synchronous\n * and require no key material — they validate prefix and base32 shape only.\n * - There is no reverse method (`unwrap`, `verify`, `extractTimestamp`) — the\n * codec is one-way by definition.\n */\nexport type DigestCodec<Brand extends string> = {\n /**\n * Digest `material` into a stable canonical {@link Id}.\n *\n * The same `(brand, ns, key, material)` tuple always returns the same ID.\n * Strings are UTF-8 encoded; byte arrays are used as-is.\n */\n digest(material: string | Uint8Array): Promise<Id<Brand>>;\n /** Strict type guard: `true` only for already-canonical `Id<Brand>` strings. */\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 DigestCodec} for `brand`.\n *\n * `opts.ns` is the required namespace — the same material under a\n * different `ns` yields a different ID. `opts.key` is the single operator\n * Digest key; there is no keyring.\n *\n * @example\n * ```ts\n * const key = await importDigestKey(new Uint8Array(32));\n * const idk = createDigestId(\"idk\", { ns: \"checkout\", key });\n *\n * const id = await idk.digest(\"order-123\"); // Id<\"idk\">\n * idk.is(id); // true\n * ```\n */\nexport function createDigestId<Brand extends string>(\n brand: Brand,\n opts: DigestOptions,\n): DigestCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n if (typeof opts.ns !== \"string\" || opts.ns.trim() === \"\") {\n throw new IdsError(\n \"invalid_namespace\",\n \"invalid namespace: ns must be a non-empty, non-whitespace string\",\n );\n }\n\n const hmacKey = getDigestKeyHmacKey(opts.key);\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createDigestLayoutOps(prefix, brand, opts.ns, hmacKey);\n\n return {\n digest: layout.digest,\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":";;;;AAKA,MAAM,UAAU,IAAI,YAAY;AAEhC,SAAS,WAAW,OAAe,QAAoB,QAAsB;CAC3E,OAAO,UAAW,UAAU,KAAM;CAClC,OAAO,SAAS,KAAM,UAAU,KAAM;CACtC,OAAO,SAAS,KAAM,UAAU,IAAK;CACrC,OAAO,SAAS,KAAK,QAAQ;AAC/B;AAEA,SAAS,aACP,YACA,SACA,UACY;CACZ,MAAM,SAAS,IAAI,WAAW,SAAS,IAAI,QAAQ,SAAS,SAAS;CACrE,MAAM,UAAU,IAAI,WAAW,MAAM;CACrC,IAAI,SAAS;CACb,WAAW,WAAW,QAAQ,SAAS,MAAM;CAC7C,UAAU;CACV,QAAQ,IAAI,YAAY,MAAM;CAC9B,UAAU,WAAW;CACrB,WAAW,QAAQ,QAAQ,SAAS,MAAM;CAC1C,UAAU;CACV,QAAQ,IAAI,SAAS,MAAM;CAC3B,UAAU,QAAQ;CAClB,QAAQ,IAAI,UAAU,MAAM;CAC5B,OAAO;AACT;AAEA,SAAgB,sBACd,QACA,OACA,IACA,SACA;CACA,MAAM,aAAa,QAAQ,OAAO,KAAK;CACvC,MAAM,UAAU,QAAQ,OAAO,EAAE;CAEjC,OAAO;EACL,QAAQ,OAAO,aAAsD;GAEnE,MAAM,UAAU,aAAa,YAAY,SADnB,OAAO,aAAa,WAAW,QAAQ,OAAO,QAAQ,IAAI,QACjB;GAK/D,OAAO,SAAS,QADA,IAHO,WACrB,MAAM,OAAO,OAAO,KAAK,QAAQ,SAAS,OAAkC,CAErD,CAAC,CAAC,SAAS,GAAA,EACN,CAAC;EACjC;EACA,qBAAiC,SAAS,IAAI,OAAO,mBAAmB;CAC1E;AACF;;;AC7CA,MAAM,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,wBAAwB;AA4BlE,MAAM,4BAAY,IAAI,QAAuC;;;;;;;;;;;;AAa7D,eAAsB,gBAAgB,OAAuC;CAC3E,iCAAiC,MAAM,QAAQ,QAAQ;CACvD,MAAM,UAAU,MAAM,cAAc,KAAK;CACzC,MAAM,MAAM,OAAO,OAAO,CAAC,CAAC;CAC5B,UAAU,IAAI,KAAK,EAAE,QAAQ,CAAC;CAC9B,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,gBAAgB,OAAmB,QAAiC;CAClF,OAAO,kBAAkB,OAAO,QAAQ,UAAU,QAAQ;AAC5D;;;;;;;;;;AAWA,SAAgB,gBAAgB,SAAiB,QAAqC;CACpF,OAAO,kBAAkB,SAAS,QAAQ,UAAU,QAAQ;AAC9D;;;;;;;AAQA,SAAgB,oBAAoB,KAAqC;CACvE,MAAM,eAAe,UAAU,IAAI,GAAG;;CAEtC,IAAI,iBAAiB,KAAA,GAAW,MAAM,IAAI,MAAM,oBAAoB;CACpE,OAAO,aAAa;AACtB;AAEA,eAAe,cAAc,OAAiD;CAC5E,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,MAAM,CACT;AACF;;;;;;;;;;;;;;;;;;;AC1BA,SAAgB,eACd,OACA,MACoB;CACpB,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAE7C,IAAI,OAAO,KAAK,OAAO,YAAY,KAAK,GAAG,KAAK,MAAM,IACpD,MAAM,IAAI,SACR,qBACA,kEACF;CAGF,MAAM,UAAU,oBAAoB,KAAK,GAAG;CAC5C,MAAM,SAAwB,GAAG,MAAM;CACvC,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,SAAS,sBAAsB,QAAQ,OAAO,KAAK,IAAI,OAAO;CAEpE,OAAO;EACL,QAAQ,OAAO;EACf,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,WAAW,KAAK;EAChB,oBAAoB,KAAK,aAAa,OAAO,OAAO,cAAc,CAAC;EACnE,aAAa,KAAK;CACpB;AACF"}
@@ -0,0 +1,124 @@
1
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
+ import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, t as Id } from "./types-g7CiQDyE.mjs";
3
+
4
+ //#region src/codecs/digest/key.d.ts
5
+ /** Wire encoding for digest operator key material (not Crockford base32). */
6
+ type DigestKeyFormat = "hex" | "base64url";
7
+ declare const digestKeyBrand: unique symbol;
8
+ /**
9
+ * Opaque imported handle for one operator Digest key.
10
+ *
11
+ * Holds a single HMAC-SHA-256 key derived via HKDF under the domain-separation
12
+ * label `@smonn/ids/digest/hmac`. The underlying `webcrypto.CryptoKey` is held internally and
13
+ * never exposed to callers. Obtain handles via {@link importDigestKey} and pass
14
+ * them to `createDigestId` as the `key` option.
15
+ *
16
+ * Unlike the other keyed codecs, the Digest codec holds exactly one key — there
17
+ * is no keyring. Re-keying is a deliberate, breaking operator action (every ID
18
+ * changes), never an in-band rotation.
19
+ *
20
+ * Distinct from the **Opaque key**, **Wrapping key**, and **Signing key** — the
21
+ * same raw bytes imported as a `DigestKey` are cryptographically independent of
22
+ * any other codec's key.
23
+ */
24
+ type DigestKey = {
25
+ readonly [digestKeyBrand]: "DigestKey";
26
+ };
27
+ /**
28
+ * Import raw operator key material into a {@link DigestKey} handle.
29
+ *
30
+ * Derives a single HMAC-SHA-256 key via HKDF under the domain-separation label
31
+ * `@smonn/ids/digest/hmac`. Accepts 16, 24, or 32 bytes. To store or transport key
32
+ * material, use {@link encodeDigestKey} / {@link decodeDigestKey}
33
+ * (`"hex"` or `"base64url"` — not Crockford base32).
34
+ *
35
+ * @param bytes - 16, 24, or 32 raw key bytes.
36
+ * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
37
+ */
38
+ declare function importDigestKey(bytes: Uint8Array): Promise<DigestKey>;
39
+ /**
40
+ * Encode raw digest operator key material for storage in env vars or secret managers.
41
+ *
42
+ * Supports `"hex"` (lowercase) and `"base64url"`. Output round-trips through
43
+ * {@link decodeDigestKey} back to the original bytes.
44
+ *
45
+ * @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
46
+ * @throws {IdsError} `invalid_key_length` if `bytes.length` is not 16, 24, or 32.
47
+ */
48
+ declare function encodeDigestKey(bytes: Uint8Array, format: DigestKeyFormat): string;
49
+ /**
50
+ * Decode key material emitted by {@link encodeDigestKey} back to raw bytes.
51
+ *
52
+ * The result can be passed directly to {@link importDigestKey}.
53
+ *
54
+ * @throws {IdsError} `invalid_key_format` if `format` is not `"hex"` or `"base64url"`.
55
+ * @throws {IdsError} `invalid_key_encoding` if the string is malformed for its format.
56
+ * @throws {IdsError} `invalid_key_length` if the decoded bytes are not 16, 24, or 32 bytes.
57
+ */
58
+ declare function decodeDigestKey(encoded: string, format: DigestKeyFormat): Uint8Array;
59
+ //#endregion
60
+ //#region src/codecs/digest/index.d.ts
61
+ /**
62
+ * Configuration options for a Digest codec instance.
63
+ */
64
+ type DigestOptions = {
65
+ /**
66
+ * Non-secret, required namespace. The same material under a different
67
+ * `ns` yields a different ID, so one key can serve multiple unlinkable namespaces.
68
+ * Must be non-empty and not whitespace-only.
69
+ */
70
+ ns: string;
71
+ /**
72
+ * Single operator digest key. The Digest codec holds exactly one key — there
73
+ * is no keyring. Re-keying is a deliberate, breaking operator action.
74
+ */
75
+ key: DigestKey; /** If true, silences the duplicate-brand warning in non-production environments. */
76
+ allowDuplicateBrand?: boolean;
77
+ };
78
+ /**
79
+ * Codec returned by {@link createDigestId}.
80
+ *
81
+ * Maps caller **material** to a stable public ID under one **Digest key**:
82
+ * the same material always yields the same ID, and the material cannot be
83
+ * recovered from the ID (**equality leakage** is the intended property).
84
+ *
85
+ * - `digest` is async (WebCrypto HMAC).
86
+ * - `is`, `parse`, `safeParse`, `toJsonSchema`, and `~standard` are synchronous
87
+ * and require no key material — they validate prefix and base32 shape only.
88
+ * - There is no reverse method (`unwrap`, `verify`, `extractTimestamp`) — the
89
+ * codec is one-way by definition.
90
+ */
91
+ type DigestCodec<Brand extends string> = {
92
+ /**
93
+ * Digest `material` into a stable canonical {@link Id}.
94
+ *
95
+ * The same `(brand, ns, key, material)` tuple always returns the same ID.
96
+ * Strings are UTF-8 encoded; byte arrays are used as-is.
97
+ */
98
+ digest(material: string | Uint8Array): Promise<Id<Brand>>; /** Strict type guard: `true` only for already-canonical `Id<Brand>` strings. */
99
+ is(value: unknown): value is Id<Brand>; /** Normalise to canonical form, or throw on parse failure. */
100
+ parse(value: unknown): Id<Brand>; /** Normalise to canonical form, or return `{ ok: false, error }`. */
101
+ safeParse(value: unknown): ParseResult<Brand>; /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */
102
+ toJsonSchema(): JsonSchema; /** Standard Schema validate entry point. */
103
+ readonly "~standard": StandardSchemaProps<Brand>;
104
+ };
105
+ /**
106
+ * Construct a {@link DigestCodec} for `brand`.
107
+ *
108
+ * `opts.ns` is the required namespace — the same material under a
109
+ * different `ns` yields a different ID. `opts.key` is the single operator
110
+ * Digest key; there is no keyring.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const key = await importDigestKey(new Uint8Array(32));
115
+ * const idk = createDigestId("idk", { ns: "checkout", key });
116
+ *
117
+ * const id = await idk.digest("order-123"); // Id<"idk">
118
+ * idk.is(id); // true
119
+ * ```
120
+ */
121
+ declare function createDigestId<Brand extends string>(brand: Brand, opts: DigestOptions): DigestCodec<Brand>;
122
+ //#endregion
123
+ export { DigestCodec, type DigestKey, type DigestKeyFormat, DigestOptions, IdsError, type IdsErrorCode, createDigestId, decodeDigestKey, encodeDigestKey, importDigestKey, isIdsError };
124
+ //# sourceMappingURL=digest.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.d.mts","names":[],"sources":["../src/codecs/digest/key.ts","../src/codecs/digest/index.ts"],"mappings":";;;;;KAQY,eAAA;AAAA,cAIE,cAAA;AAJd;;;;AAAY;AAA0B;;;;AAIxB;AAkBd;;;;AACY;AAoBZ;AA3CA,KAsBY,SAAA;EAAA,UACA,cAAA;AAAA;;;;;;;;;;AAoBsD;AAiBlE;iBAjBsB,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;;;;;;;;AAiBP;AAa3D;iBAbgB,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;;;;;AAagB;;iBAA3D,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;;AAzE/D;KCcA,aAAA;EDVE;;;AAAA;AAkBd;ECFE,EAAA;;;ADGU;AAoBZ;EClBE,GAAA,EAAK,SAAA;EAEL,mBAAA;AAAA;;;;;;;;;ADgBgE;AAiBlE;;;;KCjBY,WAAA;;;;;ADiB+C;AAa3D;ECvBE,MAAA,CAAO,QAAA,WAAmB,UAAA,GAAa,OAAA,CAAQ,EAAA,CAAG,KAAA;EAElD,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,GDiBkC;ECfzE,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;AA9C5C;;;;;;;;;AAaE;AAgBF;;;;;;AA7BA,iBAiEgB,cAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,EAAM,aAAA,GACL,WAAA,CAAY,KAAA"}
@@ -0,0 +1,3 @@
1
+ import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
+ import { i as importDigestKey, n as decodeDigestKey, r as encodeDigestKey, t as createDigestId } from "./digest-CknNw2wa.mjs";
3
+ export { IdsError, createDigestId, decodeDigestKey, encodeDigestKey, importDigestKey, isIdsError };
@@ -1,9 +1,9 @@
1
- import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
1
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
2
  import { t as Id } from "./types-g7CiQDyE.mjs";
3
- import { n as IdColumnCodec } from "./adapter-types-unUcmMXC.mjs";
3
+ import { n as IdColumnCodec } from "./adapter-types-CdYJM6Sf.mjs";
4
4
  import { ConvertCustomConfig, PgCustomColumnBuilder } from "drizzle-orm/pg-core";
5
5
 
6
- //#region src/drizzle.d.ts
6
+ //#region src/adapters/drizzle.d.ts
7
7
  /**
8
8
  * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.
9
9
  *
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle.d.mts","names":[],"sources":["../src/drizzle.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,qBAAA,CAAsB,mBAAA;EAA0B,IAAA,EAAM,EAAA,CAAG,KAAA;EAAQ,UAAA;AAAA"}
1
+ {"version":3,"file":"drizzle.d.mts","names":[],"sources":["../src/adapters/drizzle.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,qBAAA,CAAsB,mBAAA;EAA0B,IAAA,EAAM,EAAA,CAAG,KAAA;EAAQ,UAAA;AAAA"}
package/dist/drizzle.mjs CHANGED
@@ -1,7 +1,7 @@
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
+ import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
3
3
  import { customType } from "drizzle-orm/pg-core";
4
- //#region src/drizzle.ts
4
+ //#region src/adapters/drizzle.ts
5
5
  /**
6
6
  * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.
7
7
  *
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle.mjs","names":[],"sources":["../src/drizzle.ts"],"sourcesContent":["import {\n customType,\n type ConvertCustomConfig,\n type PgCustomColumnBuilder,\n} from \"drizzle-orm/pg-core\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"./error.js\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"./types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\n\nexport type { IdColumnCodec };\n\n/**\n * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.\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()`, not strict\n * `is()`. Data at rest should already be canonical per ADR-0003, but `safeParse`\n * is a safe boundary in case stale non-canonical values exist. Throws if the\n * value from the database does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/drizzle\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * export const users = pgTable(\"users\", { id: idColumn(usr).primaryKey() });\n * // users.id is Id<\"usr\"> end-to-end\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): PgCustomColumnBuilder<ConvertCustomConfig<\"\", { data: Id<Brand>; driverData: string }>> {\n return customType<{ data: Id<Brand>; driverData: string }>({\n dataType() {\n return \"text\";\n },\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":";;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,SACd,OACyF;CACzF,OAAO,WAAoD;EACzD,WAAW;GACT,OAAO;EACT;EACA,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF,CAAC,CAAC,CAAC;AACL"}
1
+ {"version":3,"file":"drizzle.mjs","names":[],"sources":["../src/adapters/drizzle.ts"],"sourcesContent":["import {\n customType,\n type ConvertCustomConfig,\n type PgCustomColumnBuilder,\n} from \"drizzle-orm/pg-core\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\n\nexport type { IdColumnCodec };\n\n/**\n * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.\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()`, not strict\n * `is()`. Data at rest should already be canonical per ADR-0003, but `safeParse`\n * is a safe boundary in case stale non-canonical values exist. Throws if the\n * value from the database does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/drizzle\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * export const users = pgTable(\"users\", { id: idColumn(usr).primaryKey() });\n * // users.id is Id<\"usr\"> end-to-end\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): PgCustomColumnBuilder<ConvertCustomConfig<\"\", { data: Id<Brand>; driverData: string }>> {\n return customType<{ data: Id<Brand>; driverData: string }>({\n dataType() {\n return \"text\";\n },\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":";;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,SACd,OACyF;CACzF,OAAO,WAAoD;EACzD,WAAW;GACT,OAAO;EACT;EACA,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF,CAAC,CAAC,CAAC;AACL"}
@@ -1 +1 @@
1
- {"version":3,"file":"error-Cp5qYZcv.mjs","names":[],"sources":["../src/error.ts"],"sourcesContent":["const BRAND = Symbol.for(\"@smonn/ids/IdsError\");\n\n/**\n * The stable machine-readable failure reason carried by `IdsError`.\n * Use `code` — not `message` — for programmatic branching; `message` is non-contractual.\n * Adding a new member is minor-additive; renaming or removing one is breaking.\n */\nexport type IdsErrorCode =\n | \"invalid_brand\"\n | \"invalid_key_format\"\n | \"invalid_key_encoding\"\n | \"invalid_key_length\"\n | \"invalid_kind\"\n | \"empty_keyring\"\n | \"duplicate_keyring_entry\"\n | \"invalid_lookup_key\"\n | \"verification_failed\"\n | \"invalid_id\";\n\n/**\n * The single error class thrown by caller-reachable public failures.\n * Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.\n * Recognized via `isIdsError()` — a branded guard that survives realm/dual-package duplication\n * where bare `instanceof` would silently fail.\n *\n * @example\n * ```ts\n * try {\n * usr.parse(rawInput);\n * } catch (err) {\n * if (isIdsError(err) && err.code === \"invalid_id\") return; // handle parse failure\n * }\n * ```\n */\nexport class IdsError extends Error {\n readonly code: IdsErrorCode;\n\n constructor(code: IdsErrorCode, message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"IdsError\";\n this.code = code;\n Object.defineProperty(this, BRAND, {\n value: true,\n enumerable: false,\n configurable: false,\n writable: false,\n });\n }\n}\n\n/**\n * Type guard for `IdsError`. Checks a non-enumerable brand rather than bare `instanceof`\n * so it survives realm/dual-package duplication (ESM + CJS dual package hazard).\n *\n * @example\n * ```ts\n * if (isIdsError(err)) {\n * switch (err.code) {\n * case \"verification_failed\": // ...\n * case \"invalid_id\": // ...\n * }\n * }\n * ```\n */\nexport function isIdsError(value: unknown): value is IdsError {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Record<symbol, unknown>)[BRAND] === true\n );\n}\n"],"mappings":";AAAA,MAAM,QAAQ,OAAO,IAAI,qBAAqB;;;;;;;;;;;;;;;;AAkC9C,IAAa,WAAb,cAA8B,MAAM;CAClC;CAEA,YAAY,MAAoB,SAAiB,SAAwB;EACvE,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,OAAO,eAAe,MAAM,OAAO;GACjC,OAAO;GACP,YAAY;GACZ,cAAc;GACd,UAAU;EACZ,CAAC;CACH;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,WAAW,OAAmC;CAC5D,OACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,WAAW;AAElD"}
1
+ {"version":3,"file":"error-Cp5qYZcv.mjs","names":[],"sources":["../src/error.ts"],"sourcesContent":["const BRAND = Symbol.for(\"@smonn/ids/IdsError\");\n\n/**\n * The stable machine-readable failure reason carried by `IdsError`.\n * Use `code` — not `message` — for programmatic branching; `message` is non-contractual.\n * Adding a new member is minor-additive; renaming or removing one is breaking.\n */\nexport type IdsErrorCode =\n | \"invalid_brand\"\n | \"invalid_key_format\"\n | \"invalid_key_encoding\"\n | \"invalid_key_length\"\n | \"invalid_kind\"\n | \"empty_keyring\"\n | \"duplicate_keyring_entry\"\n | \"invalid_lookup_key\"\n | \"verification_failed\"\n | \"invalid_id\"\n | \"invalid_namespace\";\n\n/**\n * The single error class thrown by caller-reachable public failures.\n * Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.\n * Recognized via `isIdsError()` — a branded guard that survives realm/dual-package duplication\n * where bare `instanceof` would silently fail.\n *\n * @example\n * ```ts\n * try {\n * usr.parse(rawInput);\n * } catch (err) {\n * if (isIdsError(err) && err.code === \"invalid_id\") return; // handle parse failure\n * }\n * ```\n */\nexport class IdsError extends Error {\n readonly code: IdsErrorCode;\n\n constructor(code: IdsErrorCode, message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"IdsError\";\n this.code = code;\n Object.defineProperty(this, BRAND, {\n value: true,\n enumerable: false,\n configurable: false,\n writable: false,\n });\n }\n}\n\n/**\n * Type guard for `IdsError`. Checks a non-enumerable brand rather than bare `instanceof`\n * so it survives realm/dual-package duplication (ESM + CJS dual package hazard).\n *\n * @example\n * ```ts\n * if (isIdsError(err)) {\n * switch (err.code) {\n * case \"verification_failed\": // ...\n * case \"invalid_id\": // ...\n * }\n * }\n * ```\n */\nexport function isIdsError(value: unknown): value is IdsError {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Record<symbol, unknown>)[BRAND] === true\n );\n}\n"],"mappings":";AAAA,MAAM,QAAQ,OAAO,IAAI,qBAAqB;;;;;;;;;;;;;;;;AAmC9C,IAAa,WAAb,cAA8B,MAAM;CAClC;CAEA,YAAY,MAAoB,SAAiB,SAAwB;EACvE,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,OAAO,eAAe,MAAM,OAAO;GACjC,OAAO;GACP,YAAY;GACZ,cAAc;GACd,UAAU;EACZ,CAAC;CACH;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,WAAW,OAAmC;CAC5D,OACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,WAAW;AAElD"}
@@ -4,7 +4,7 @@
4
4
  * Use `code` — not `message` — for programmatic branching; `message` is non-contractual.
5
5
  * Adding a new member is minor-additive; renaming or removing one is breaking.
6
6
  */
7
- type IdsErrorCode = "invalid_brand" | "invalid_key_format" | "invalid_key_encoding" | "invalid_key_length" | "invalid_kind" | "empty_keyring" | "duplicate_keyring_entry" | "invalid_lookup_key" | "verification_failed" | "invalid_id";
7
+ type IdsErrorCode = "invalid_brand" | "invalid_key_format" | "invalid_key_encoding" | "invalid_key_length" | "invalid_kind" | "empty_keyring" | "duplicate_keyring_entry" | "invalid_lookup_key" | "verification_failed" | "invalid_id" | "invalid_namespace";
8
8
  /**
9
9
  * The single error class thrown by caller-reachable public failures.
10
10
  * Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.
@@ -41,4 +41,4 @@ declare class IdsError extends Error {
41
41
  declare function isIdsError(value: unknown): value is IdsError;
42
42
  //#endregion
43
43
  export { IdsErrorCode as n, isIdsError as r, IdsError as t };
44
- //# sourceMappingURL=error-DTr4i6Ic.d.mts.map
44
+ //# sourceMappingURL=error-JIPylU_E.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-DTr4i6Ic.d.mts","names":[],"sources":["../src/error.ts"],"mappings":";;AAOA;;;;KAAY,YAAA;AA2BZ;;;;;;;;;;;;;;;AAAA,cAAa,QAAA,SAAiB,KAAA;EAAA,SACnB,IAAA,EAAM,YAAA;EAEf,WAAA,CAAY,IAAA,EAAM,YAAA,EAAc,OAAA,UAAiB,OAAA,GAAU,YAAA;AAAA;AAAA;AA2B7D;;;;;;;;AAAqD;;;;;AA3BQ,iBA2B7C,UAAA,CAAW,KAAA,YAAiB,KAAA,IAAS,QAAA"}
1
+ {"version":3,"file":"error-JIPylU_E.d.mts","names":[],"sources":["../src/error.ts"],"mappings":";;AAOA;;;;KAAY,YAAA;AA4BZ;;;;;;;;;;;;;;;AAAA,cAAa,QAAA,SAAiB,KAAA;EAAA,SACnB,IAAA,EAAM,YAAA;EAEf,WAAA,CAAY,IAAA,EAAM,YAAA,EAAc,OAAA,UAAiB,OAAA,GAAU,YAAA;AAAA;AAAA;AA2B7D;;;;;;;;AAAqD;;;;;AA3BQ,iBA2B7C,UAAA,CAAW,KAAA,YAAiB,KAAA,IAAS,QAAA"}
@@ -1,8 +1,8 @@
1
1
  import { t as Id } from "./types-g7CiQDyE.mjs";
2
- import { r as IdParamFailure, t as IdCodec } from "./adapter-types-unUcmMXC.mjs";
2
+ import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CdYJM6Sf.mjs";
3
3
  import { NextFunction, Request, Response } from "express";
4
4
 
5
- //#region src/express.d.ts
5
+ //#region src/adapters/express.d.ts
6
6
  /**
7
7
  * Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.
8
8
  * Inspect `err.reason` and `err.status` in error-handling middleware.
@@ -1 +1 @@
1
- {"version":3,"file":"express.d.mts","names":[],"sources":["../src/express.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,MAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,MAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA;;;;;EAKvE,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;AAAA;AAmDtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAI6E;iBAJ7D,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IACR,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,UAAkB,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA,KAAU,IAAA,EAAM,YAAA"}
1
+ {"version":3,"file":"express.d.mts","names":[],"sources":["../src/adapters/express.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,MAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,MAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA;;;;;EAKvE,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;AAAA;AAmDtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAI6E;iBAJ7D,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IACR,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,UAAkB,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA,KAAU,IAAA,EAAM,YAAA"}
package/dist/express.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { n as resolveIdParamFailure } from "./adapter-types-BY-wrYYB.mjs";
2
- //#region src/express.ts
1
+ import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
2
+ //#region src/adapters/express.ts
3
3
  /**
4
4
  * Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.
5
5
  * Inspect `err.reason` and `err.status` in error-handling middleware.
@@ -1 +1 @@
1
- {"version":3,"file":"express.mjs","names":[],"sources":["../src/express.ts"],"sourcesContent":["import type { NextFunction, Request, Response } from \"express\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"./types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.\n * Inspect `err.reason` and `err.status` in error-handling middleware.\n */\nexport class IdParamError extends Error {\n readonly status: number;\n readonly reason: \"brand_mismatch\" | \"malformed\";\n\n constructor(reason: \"brand_mismatch\" | \"malformed\", status: number) {\n super(`ID validation failed: ${reason}`);\n this.name = \"IdParamError\";\n this.reason = reason;\n this.status = status;\n }\n}\n\n/** Options for `idParam`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of forwarding to `next(err)` when provided. The hook owns the response\n * entirely — the adapter does not call `next(err)` itself.\n */\n onError?: (failure: IdParamFailure, req: Request, res: Response, next: NextFunction) => void;\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 * Express middleware that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and `reason`,\n * so the app's existing error-handling middleware controls rendering. The adapter does not write\n * a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter does\n * not call `next(err)`.\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 `res.locals` under `paramName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idParam, IdParamError } from \"@smonn/ids/express\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: forwards error to app error-handling middleware\n * app.get(\"/users/:id\", idParam(\"id\", usr), (req, res) => {\n * const id = res.locals.id; // Id<\"usr\">, canonical\n * });\n *\n * // Error-handling middleware receives the typed error\n * app.use((err, req, res, next) => {\n * if (err instanceof IdParamError) {\n * res.status(err.status).json({ error: err.reason });\n * return;\n * }\n * next(err);\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/orgs/:id\", idParam(\"id\", org, {\n * onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),\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): (req: Request, res: Response<unknown, Record<ParamKey, Id<Brand>>>, next: NextFunction) => void {\n return (req, res, next): void => {\n const raw = req.params[paramName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n options.onError(failure, req, res, next);\n return;\n }\n next(new IdParamError(failure.reason, failure.status));\n return;\n }\n (res.locals as Record<string, unknown>)[paramName] = result.id;\n next();\n };\n}\n"],"mappings":";;;;;;AAUA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,QAAwC,QAAgB;EAClE,MAAM,yBAAyB,QAAQ;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,SAAS;CAChB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,SAAgB,QACd,WACA,OACA,SACiG;CACjG,QAAQ,KAAK,KAAK,SAAe;EAC/B,MAAM,MAAM,IAAI,OAAO;EACvB,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,QAAQ,QAAQ,SAAS,KAAK,KAAK,IAAI;IACvC;GACF;GACA,KAAK,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM,CAAC;GACrD;EACF;EACA,IAAK,OAAmC,aAAa,OAAO;EAC5D,KAAK;CACP;AACF"}
1
+ {"version":3,"file":"express.mjs","names":[],"sources":["../src/adapters/express.ts"],"sourcesContent":["import type { NextFunction, Request, Response } from \"express\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.\n * Inspect `err.reason` and `err.status` in error-handling middleware.\n */\nexport class IdParamError extends Error {\n readonly status: number;\n readonly reason: \"brand_mismatch\" | \"malformed\";\n\n constructor(reason: \"brand_mismatch\" | \"malformed\", status: number) {\n super(`ID validation failed: ${reason}`);\n this.name = \"IdParamError\";\n this.reason = reason;\n this.status = status;\n }\n}\n\n/** Options for `idParam`. All fields are optional. */\nexport type IdParamOptions = {\n /**\n * Called instead of forwarding to `next(err)` when provided. The hook owns the response\n * entirely — the adapter does not call `next(err)` itself.\n */\n onError?: (failure: IdParamFailure, req: Request, res: Response, next: NextFunction) => void;\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 * Express middleware that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and `reason`,\n * so the app's existing error-handling middleware controls rendering. The adapter does not write\n * a response body itself.\n *\n * **`options.onError`:** when provided, the hook owns the response entirely — the adapter does\n * not call `next(err)`.\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 `res.locals` under `paramName`\n * and calls `next()`.\n *\n * @example\n * ```ts\n * import { idParam, IdParamError } from \"@smonn/ids/express\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: forwards error to app error-handling middleware\n * app.get(\"/users/:id\", idParam(\"id\", usr), (req, res) => {\n * const id = res.locals.id; // Id<\"usr\">, canonical\n * });\n *\n * // Error-handling middleware receives the typed error\n * app.use((err, req, res, next) => {\n * if (err instanceof IdParamError) {\n * res.status(err.status).json({ error: err.reason });\n * return;\n * }\n * next(err);\n * });\n *\n * // Override: consumer fully owns the response\n * app.get(\"/orgs/:id\", idParam(\"id\", org, {\n * onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),\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): (req: Request, res: Response<unknown, Record<ParamKey, Id<Brand>>>, next: NextFunction) => void {\n return (req, res, next): void => {\n const raw = req.params[paramName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n options.onError(failure, req, res, next);\n return;\n }\n next(new IdParamError(failure.reason, failure.status));\n return;\n }\n (res.locals as Record<string, unknown>)[paramName] = result.id;\n next();\n };\n}\n"],"mappings":";;;;;;AAUA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,QAAwC,QAAgB;EAClE,MAAM,yBAAyB,QAAQ;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,SAAS;CAChB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,SAAgB,QACd,WACA,OACA,SACiG;CACjG,QAAQ,KAAK,KAAK,SAAe;EAC/B,MAAM,MAAM,IAAI,OAAO;EACvB,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,QAAQ,QAAQ,SAAS,KAAK,KAAK,IAAI;IACvC;GACF;GACA,KAAK,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM,CAAC;GACrD;EACF;EACA,IAAK,OAAmC,aAAa,OAAO;EAC5D,KAAK;CACP;AACF"}
@@ -1,8 +1,8 @@
1
1
  import { t as Id } from "./types-g7CiQDyE.mjs";
2
- import { r as IdParamFailure, t as IdCodec } from "./adapter-types-unUcmMXC.mjs";
2
+ import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CdYJM6Sf.mjs";
3
3
  import { FastifyReply, FastifyRequest } from "fastify";
4
4
 
5
- //#region src/fastify.d.ts
5
+ //#region src/adapters/fastify.d.ts
6
6
  /**
7
7
  * Typed error thrown into Fastify's `setErrorHandler` on validation failure.
8
8
  * Inspect `err.reason` and `err.statusCode` in your error handler.
@@ -60,7 +60,7 @@ type IdParamOptions = {
60
60
  *
61
61
  * // Default: throws IdParamError → setErrorHandler renders it
62
62
  * fastify.get("/users/:id", { preHandler: idParam("id", usr) }, (request, reply) => {
63
- * const id = request.params.id; // Id<"usr">, canonical
63
+ * const id = request.params.id; // string (compile-time); Id<"usr"> at runtime after preHandler
64
64
  * });
65
65
  *
66
66
  * // Error handler receives the typed error
@@ -1 +1 @@
1
- {"version":3,"file":"fastify.d.mts","names":[],"sources":["../src/fastify.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,UAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,UAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IACE,OAAA,EAAS,cAAA,EACT,OAAA,EAAS,cAAA,EACT,KAAA,EAAO,YAAA,YACG,OAAA;;;;;EAKZ,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;AAAA;AA6DtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOK;;;;;;;;;;;;;;iBAPW,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IAEV,OAAA,EAAS,cAAA;EAAiB,MAAA,EAAQ,MAAA,SAAe,EAAA,CAAG,KAAA;AAAA,IACpD,KAAA,EAAO,YAAA,KACJ,OAAA"}
1
+ {"version":3,"file":"fastify.d.mts","names":[],"sources":["../src/adapters/fastify.ts"],"mappings":";;;;;;AAUA;;;cAAa,YAAA,SAAqB,KAAA;EAAA,SACvB,UAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,UAAA;AAAA;;KAS1C,cAAA;EAT0C;AAAA;AAStD;;EAKE,OAAA,IACE,OAAA,EAAS,cAAA,EACT,OAAA,EAAS,cAAA,EACT,KAAA,EAAO,YAAA,YACG,OAAA;;;;;EAKZ,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;AAAA;AA6DtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOK;;;;;;;;;;;;;;iBAPW,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IAEV,OAAA,EAAS,cAAA;EAAiB,MAAA,EAAQ,MAAA,SAAe,EAAA,CAAG,KAAA;AAAA,IACpD,KAAA,EAAO,YAAA,KACJ,OAAA"}
package/dist/fastify.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { n as resolveIdParamFailure } from "./adapter-types-BY-wrYYB.mjs";
2
- //#region src/fastify.ts
1
+ import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
2
+ //#region src/adapters/fastify.ts
3
3
  /**
4
4
  * Typed error thrown into Fastify's `setErrorHandler` on validation failure.
5
5
  * Inspect `err.reason` and `err.statusCode` in your error handler.
@@ -46,7 +46,7 @@ var IdParamError = class extends Error {
46
46
  *
47
47
  * // Default: throws IdParamError → setErrorHandler renders it
48
48
  * fastify.get("/users/:id", { preHandler: idParam("id", usr) }, (request, reply) => {
49
- * const id = request.params.id; // Id<"usr">, canonical
49
+ * const id = request.params.id; // string (compile-time); Id<"usr"> at runtime after preHandler
50
50
  * });
51
51
  *
52
52
  * // Error handler receives the typed error
@@ -1 +1 @@
1
- {"version":3,"file":"fastify.mjs","names":[],"sources":["../src/fastify.ts"],"sourcesContent":["import type { FastifyReply, FastifyRequest } from \"fastify\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"./types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Typed error thrown into Fastify's `setErrorHandler` on validation failure.\n * Inspect `err.reason` and `err.statusCode` in your error handler.\n */\nexport class IdParamError extends Error {\n readonly statusCode: number;\n readonly reason: \"brand_mismatch\" | \"malformed\";\n\n constructor(reason: \"brand_mismatch\" | \"malformed\", statusCode: number) {\n super(`ID validation failed: ${reason}`);\n this.name = \"IdParamError\";\n this.reason = reason;\n this.statusCode = statusCode;\n }\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 does not throw.\n */\n onError?: (\n failure: IdParamFailure,\n request: FastifyRequest,\n reply: FastifyReply,\n ) => void | Promise<void>;\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 * Fastify `preHandler` hook factory that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the app's\n * existing `setErrorHandler` controls rendering. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook calls `onError` and does not throw; the consumer\n * fully owns the response via `reply`.\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 `request.params` under `paramName`.\n *\n * **Return type note:** the returned hook is typed as\n * `(request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>, reply: FastifyReply) => Promise<void>`.\n * Assigning it to a Fastify `preHandler` slot is backward-compatible (method-signature bivariance applies).\n * However, a locally-annotated variable typed as the bare `(request: FastifyRequest, reply: FastifyReply) => Promise<void>`\n * will produce a TypeScript error under `--strictFunctionTypes` because function parameter types are contravariant.\n * Use `preHandler` assignment or let TypeScript infer the type to avoid this.\n *\n * @example\n * ```ts\n * import { idParam, IdParamError } from \"@smonn/ids/fastify\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws IdParamError → setErrorHandler renders it\n * fastify.get(\"/users/:id\", { preHandler: idParam(\"id\", usr) }, (request, reply) => {\n * const id = request.params.id; // Id<\"usr\">, canonical\n * });\n *\n * // Error handler receives the typed error\n * fastify.setErrorHandler((err, request, reply) => {\n * if (err instanceof IdParamError) {\n * reply.status(err.statusCode).send({ error: err.reason });\n * return;\n * }\n * reply.send(err);\n * });\n *\n * // Override: consumer fully owns the error response\n * fastify.get(\"/orgs/:id\", {\n * preHandler: idParam(\"id\", org, {\n * onError: (failure, request, reply) =>\n * reply.status(failure.status).send({ error: failure.reason }),\n * }),\n * }, handler);\n *\n * // Or a lightweight status remap without a full handler\n * fastify.get(\"/things/:id\", {\n * preHandler: idParam(\"id\", thing, { status: { brand_mismatch: 400 } }),\n * }, handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): (\n request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>,\n reply: FastifyReply,\n) => Promise<void> {\n return async (request, reply): Promise<void> => {\n const raw = request.params[paramName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n await options.onError(failure, request, reply);\n return;\n }\n throw new IdParamError(failure.reason, failure.status);\n }\n request.params[paramName] = result.id;\n };\n}\n"],"mappings":";;;;;;AAUA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,QAAwC,YAAoB;EACtE,MAAM,yBAAyB,QAAQ;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,aAAa;CACpB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,SAAgB,QACd,WACA,OACA,SAIiB;CACjB,OAAO,OAAO,SAAS,UAAyB;EAC9C,MAAM,MAAM,QAAQ,OAAO;EAC3B,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,MAAM,QAAQ,QAAQ,SAAS,SAAS,KAAK;IAC7C;GACF;GACA,MAAM,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM;EACvD;EACA,QAAQ,OAAO,aAAa,OAAO;CACrC;AACF"}
1
+ {"version":3,"file":"fastify.mjs","names":[],"sources":["../src/adapters/fastify.ts"],"sourcesContent":["import type { FastifyReply, FastifyRequest } from \"fastify\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Typed error thrown into Fastify's `setErrorHandler` on validation failure.\n * Inspect `err.reason` and `err.statusCode` in your error handler.\n */\nexport class IdParamError extends Error {\n readonly statusCode: number;\n readonly reason: \"brand_mismatch\" | \"malformed\";\n\n constructor(reason: \"brand_mismatch\" | \"malformed\", statusCode: number) {\n super(`ID validation failed: ${reason}`);\n this.name = \"IdParamError\";\n this.reason = reason;\n this.statusCode = statusCode;\n }\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 does not throw.\n */\n onError?: (\n failure: IdParamFailure,\n request: FastifyRequest,\n reply: FastifyReply,\n ) => void | Promise<void>;\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 * Fastify `preHandler` hook factory that validates a named route param against a codec via `safeParse`.\n *\n * **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the app's\n * existing `setErrorHandler` controls rendering. The adapter does not write a response body itself.\n *\n * **`options.onError`:** when provided, the hook calls `onError` and does not throw; the consumer\n * fully owns the response via `reply`.\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 `request.params` under `paramName`.\n *\n * **Return type note:** the returned hook is typed as\n * `(request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>, reply: FastifyReply) => Promise<void>`.\n * Assigning it to a Fastify `preHandler` slot is backward-compatible (method-signature bivariance applies).\n * However, a locally-annotated variable typed as the bare `(request: FastifyRequest, reply: FastifyReply) => Promise<void>`\n * will produce a TypeScript error under `--strictFunctionTypes` because function parameter types are contravariant.\n * Use `preHandler` assignment or let TypeScript infer the type to avoid this.\n *\n * @example\n * ```ts\n * import { idParam, IdParamError } from \"@smonn/ids/fastify\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * // Default: throws IdParamError → setErrorHandler renders it\n * fastify.get(\"/users/:id\", { preHandler: idParam(\"id\", usr) }, (request, reply) => {\n * const id = request.params.id; // string (compile-time); Id<\"usr\"> at runtime after preHandler\n * });\n *\n * // Error handler receives the typed error\n * fastify.setErrorHandler((err, request, reply) => {\n * if (err instanceof IdParamError) {\n * reply.status(err.statusCode).send({ error: err.reason });\n * return;\n * }\n * reply.send(err);\n * });\n *\n * // Override: consumer fully owns the error response\n * fastify.get(\"/orgs/:id\", {\n * preHandler: idParam(\"id\", org, {\n * onError: (failure, request, reply) =>\n * reply.status(failure.status).send({ error: failure.reason }),\n * }),\n * }, handler);\n *\n * // Or a lightweight status remap without a full handler\n * fastify.get(\"/things/:id\", {\n * preHandler: idParam(\"id\", thing, { status: { brand_mismatch: 400 } }),\n * }, handler);\n * ```\n */\nexport function idParam<ParamKey extends string, Brand extends string>(\n paramName: ParamKey,\n codec: IdCodec<Brand>,\n options?: IdParamOptions,\n): (\n request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>,\n reply: FastifyReply,\n) => Promise<void> {\n return async (request, reply): Promise<void> => {\n const raw = request.params[paramName];\n const result = codec.safeParse(raw);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, options);\n if (options?.onError) {\n await options.onError(failure, request, reply);\n return;\n }\n throw new IdParamError(failure.reason, failure.status);\n }\n request.params[paramName] = result.id;\n };\n}\n"],"mappings":";;;;;;AAUA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,QAAwC,YAAoB;EACtE,MAAM,yBAAyB,QAAQ;EACvC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,aAAa;CACpB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,SAAgB,QACd,WACA,OACA,SAIiB;CACjB,OAAO,OAAO,SAAS,UAAyB;EAC9C,MAAM,MAAM,QAAQ,OAAO;EAC3B,MAAM,SAAS,MAAM,UAAU,GAAG;EAClC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,OAAO;GAC3D,IAAI,SAAS,SAAS;IACpB,MAAM,QAAQ,QAAQ,SAAS,SAAS,KAAK;IAC7C;GACF;GACA,MAAM,IAAI,aAAa,QAAQ,QAAQ,QAAQ,MAAM;EACvD;EACA,QAAQ,OAAO,aAAa,OAAO;CACrC;AACF"}
package/dist/hono.d.mts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { t as Id } from "./types-g7CiQDyE.mjs";
2
- import { r as IdParamFailure, t as IdCodec } from "./adapter-types-unUcmMXC.mjs";
2
+ import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CdYJM6Sf.mjs";
3
+ import { ContentfulStatusCode } from "hono/utils/http-status";
3
4
  import { Context, MiddlewareHandler } from "hono";
4
5
 
5
- //#region src/hono.d.ts
6
+ //#region src/adapters/hono.d.ts
6
7
  /** Options for `idParam`. All fields are optional. */
7
8
  type IdParamOptions = {
8
9
  /**
@@ -15,8 +16,8 @@ type IdParamOptions = {
15
16
  * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.
16
17
  */
17
18
  status?: {
18
- brand_mismatch?: number;
19
- malformed?: number;
19
+ brand_mismatch?: ContentfulStatusCode;
20
+ malformed?: ContentfulStatusCode;
20
21
  };
21
22
  };
22
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"hono.d.mts","names":[],"sources":["../src/hono.ts"],"mappings":";;;;;;KAQY,cAAA;EAAA;;;;EAKV,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,CAAA,EAAG,OAAA,KAAY,QAAA,GAAW,OAAA,CAAQ,QAAA;;;;;EAKtE,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;AAAA;AAyCtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,GACT,iBAAA;EAAoB,SAAA,EAAW,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA;AAAA"}
1
+ {"version":3,"file":"hono.d.mts","names":[],"sources":["../src/adapters/hono.ts"],"mappings":";;;;;;;KASY,cAAA;EAAA;;;;EAKV,OAAA,IAAW,OAAA,EAAS,cAAA,EAAgB,CAAA,EAAG,OAAA,KAAY,QAAA,GAAW,OAAA,CAAQ,QAAA;;;;;EAKtE,MAAA;IAAW,cAAA,GAAiB,oBAAA;IAAsB,SAAA,GAAY,oBAAA;EAAA;AAAA;;;;;;;;;;;;AAAA;AAyChE;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,GACT,iBAAA;EAAoB,SAAA,EAAW,MAAA,CAAO,QAAA,EAAU,EAAA,CAAG,KAAA;AAAA"}
package/dist/hono.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { n as resolveIdParamFailure } from "./adapter-types-BY-wrYYB.mjs";
1
+ import { n as resolveIdParamFailure } from "./adapter-types-7wWdELSh.mjs";
2
2
  import { HTTPException } from "hono/http-exception";
3
- //#region src/hono.ts
3
+ //#region src/adapters/hono.ts
4
4
  /**
5
5
  * Hono middleware that validates a named route param against a codec via `safeParse`.
6
6
  *
@@ -46,7 +46,8 @@ function idParam(paramName, codec, options) {
46
46
  if (!result.ok) {
47
47
  const failure = resolveIdParamFailure(result.error, options);
48
48
  if (options?.onError) return options.onError(failure, c);
49
- throw new HTTPException(failure.status);
49
+ const defaultStatus = failure.reason === "brand_mismatch" ? 404 : 400;
50
+ throw new HTTPException(options?.status?.[failure.reason] ?? defaultStatus);
50
51
  }
51
52
  c.set(paramName, result.id);
52
53
  await next();
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 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"}
1
+ {"version":3,"file":"hono.mjs","names":[],"sources":["../src/adapters/hono.ts"],"sourcesContent":["import { HTTPException } from \"hono/http-exception\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\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?: ContentfulStatusCode; malformed?: ContentfulStatusCode };\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 const defaultStatus: ContentfulStatusCode = failure.reason === \"brand_mismatch\" ? 404 : 400;\n throw new HTTPException(options?.status?.[failure.reason] ?? defaultStatus);\n }\n c.set(paramName, result.id);\n await next();\n return;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,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,gBAAsC,QAAQ,WAAW,mBAAmB,MAAM;GACxF,MAAM,IAAI,cAAc,SAAS,SAAS,QAAQ,WAAW,aAAa;EAC5E;EACA,EAAE,IAAI,WAAW,OAAO,EAAE;EAC1B,MAAM,KAAK;CAEb;AACF"}
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
1
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
2
  import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, r as ParseError, t as Id } from "./types-g7CiQDyE.mjs";
3
3
 
4
- //#region src/timestamp.d.ts
4
+ //#region src/codecs/timestamp/index.d.ts
5
5
  /**
6
6
  * Configuration options for a codec instance.
7
7
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/timestamp.ts"],"mappings":";;;;;;;KASY,gBAAA;EAAA,6EAEV,GAAA,iBAEe;EAAf,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;;;AAAA;AAeF;;;;;;KAAY,cAAA;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;;;;EAI1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;EAIvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;EAEjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;iBAiD5B,iBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,GAAM,gBAAA,GACL,cAAA,CAAe,KAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/codecs/timestamp/index.ts"],"mappings":";;;;;;;KASY,gBAAA;EAAA,6EAEV,GAAA,iBAEe;EAAf,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;;;AAAA;AAeF;;;;;;KAAY,cAAA;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;;;;EAI1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;EAIvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;EAEjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;iBAiD5B,iBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,GAAM,gBAAA,GACL,cAAA,CAAe,KAAA"}