@smonn/ids 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +555 -18
  2. package/dist/adapter-types-oHCCSgOO.d.mts +12 -0
  3. package/dist/adapter-types-oHCCSgOO.d.mts.map +1 -0
  4. package/dist/cli.mjs +246 -63
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{codec-shell-dWpxoFmy.mjs → codec-shell-DH-UO4UR.mjs} +8 -8
  7. package/dist/codec-shell-DH-UO4UR.mjs.map +1 -0
  8. package/dist/drizzle-CeSni5PB.d.mts +44 -0
  9. package/dist/drizzle-CeSni5PB.d.mts.map +1 -0
  10. package/dist/drizzle.d.mts +3 -0
  11. package/dist/drizzle.mjs +43 -0
  12. package/dist/drizzle.mjs.map +1 -0
  13. package/dist/error-Cp5qYZcv.mjs +52 -0
  14. package/dist/error-Cp5qYZcv.mjs.map +1 -0
  15. package/dist/error-DTr4i6Ic.d.mts +44 -0
  16. package/dist/error-DTr4i6Ic.d.mts.map +1 -0
  17. package/dist/express.d.mts +85 -0
  18. package/dist/express.d.mts.map +1 -0
  19. package/dist/express.mjs +90 -0
  20. package/dist/express.mjs.map +1 -0
  21. package/dist/fastify.d.mts +88 -0
  22. package/dist/fastify.d.mts.map +1 -0
  23. package/dist/fastify.mjs +91 -0
  24. package/dist/fastify.mjs.map +1 -0
  25. package/dist/hono.d.mts +68 -0
  26. package/dist/hono.d.mts.map +1 -0
  27. package/dist/hono.mjs +63 -0
  28. package/dist/hono.mjs.map +1 -0
  29. package/dist/index.d.mts +2 -1
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +3 -2
  32. package/dist/kysely.d.mts +56 -0
  33. package/dist/kysely.d.mts.map +1 -0
  34. package/dist/kysely.mjs +43 -0
  35. package/dist/kysely.mjs.map +1 -0
  36. package/dist/{opaque-B4ps7Pqk.mjs → opaque-uvjOFY_0.mjs} +37 -20
  37. package/dist/opaque-uvjOFY_0.mjs.map +1 -0
  38. package/dist/opaque.d.mts +34 -9
  39. package/dist/opaque.d.mts.map +1 -1
  40. package/dist/opaque.mjs +3 -2
  41. package/dist/prisma.d.mts +85 -0
  42. package/dist/prisma.d.mts.map +1 -0
  43. package/dist/prisma.mjs +54 -0
  44. package/dist/prisma.mjs.map +1 -0
  45. package/dist/reverse-BgFU6JHw.mjs +87 -0
  46. package/dist/reverse-BgFU6JHw.mjs.map +1 -0
  47. package/dist/reverse.d.mts +77 -0
  48. package/dist/reverse.d.mts.map +1 -0
  49. package/dist/reverse.mjs +3 -0
  50. package/dist/signed.d.mts +56 -0
  51. package/dist/signed.d.mts.map +1 -0
  52. package/dist/signed.mjs +100 -0
  53. package/dist/signed.mjs.map +1 -0
  54. package/dist/{timestamp-Bgzxx8bE.mjs → timestamp-B5_UCzc6.mjs} +3 -3
  55. package/dist/{timestamp-Bgzxx8bE.mjs.map → timestamp-B5_UCzc6.mjs.map} +1 -1
  56. package/dist/{timestamp-bytes-B57RM7Ho.mjs → timestamp-bytes-BBY7JI33.mjs} +2 -2
  57. package/dist/{timestamp-bytes-B57RM7Ho.mjs.map → timestamp-bytes-BBY7JI33.mjs.map} +1 -1
  58. package/dist/wrapped-0vL72Nje.mjs +361 -0
  59. package/dist/wrapped-0vL72Nje.mjs.map +1 -0
  60. package/dist/wrapped.d.mts +89 -9
  61. package/dist/wrapped.d.mts.map +1 -1
  62. package/dist/wrapped.mjs +3 -336
  63. package/package.json +45 -3
  64. package/dist/codec-shell-dWpxoFmy.mjs.map +0 -1
  65. package/dist/opaque-B4ps7Pqk.mjs.map +0 -1
  66. package/dist/wrapped.mjs.map +0 -1
@@ -1,8 +1,9 @@
1
+ import { t as IdsError } from "./error-Cp5qYZcv.mjs";
1
2
  //#region src/brand.ts
2
3
  const brandPattern = /^[a-z]{3}$/;
3
4
  /** Validates a three-character lowercase brand. Throws on invalid input. */
4
5
  function validateBrand(brand) {
5
- if (!brandPattern.test(brand)) throw new Error("invalid brand, expected three lowercase a-z characters");
6
+ if (!brandPattern.test(brand)) throw new IdsError("invalid_brand", "invalid brand: expected three lowercase a-z characters");
6
7
  }
7
8
  //#endregion
8
9
  //#region src/base32.ts
@@ -103,11 +104,6 @@ function safeParse(prefix, value) {
103
104
  id: prefix + base32
104
105
  };
105
106
  }
106
- function parse(prefix, value) {
107
- const result = safeParse(prefix, value);
108
- if (result.ok) return result.id;
109
- throw new Error(`Invalid ID: ${result.error}`);
110
- }
111
107
  function is(prefix, value) {
112
108
  if (typeof value !== "string") return false;
113
109
  if (!value.startsWith(prefix)) return false;
@@ -131,7 +127,11 @@ function standardValidate(prefix, value) {
131
127
  function wireMethods(prefix) {
132
128
  return {
133
129
  is: (value) => is(prefix, value),
134
- parse: (value) => parse(prefix, value),
130
+ parse: (value) => {
131
+ const result = safeParse(prefix, value);
132
+ if (result.ok) return result.id;
133
+ throw new IdsError("invalid_id", `invalid ID: ${result.error}`, { cause: result.error });
134
+ },
135
135
  safeParse: (value) => safeParse(prefix, value),
136
136
  toJsonSchema: (brand, example) => ({
137
137
  type: "string",
@@ -149,4 +149,4 @@ function wireMethods(prefix) {
149
149
  //#endregion
150
150
  export { toWireId as a, payloadBytesFromId as i, registerBrand as n, decodeBase32 as o, payloadBase32Length as r, validateBrand as s, wireMethods as t };
151
151
 
152
- //# sourceMappingURL=codec-shell-dWpxoFmy.mjs.map
152
+ //# sourceMappingURL=codec-shell-DH-UO4UR.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec-shell-DH-UO4UR.mjs","names":[],"sources":["../src/brand.ts","../src/base32.ts","../src/wire/envelope.ts","../src/wire/invariants.ts","../src/registry.ts","../src/wire/parse.ts","../src/wire/codec-shell.ts"],"sourcesContent":["import { IdsError } from \"./error.js\";\n\nconst brandPattern = /^[a-z]{3}$/;\n\n/** Validates a three-character lowercase brand. Throws on invalid input. */\nexport function validateBrand(brand: string): void {\n if (!brandPattern.test(brand)) {\n throw new IdsError(\"invalid_brand\", \"invalid brand: expected three lowercase a-z characters\");\n }\n}\n","/*\n This is based on Crockford's Base32 spec: https://www.crockford.com/base32.html\n One difference is that it uses lowercase instead of uppercase when encoding.\n\n These functions are internal: codec constructors guarantee that input is a\n 16-byte buffer for encode, or a string of characters drawn from the alphabet\n for decode. Invalid input produces silent garbage rather than a thrown error,\n consistent with the trust-the-type rule in ADR-0003.\n*/\n\nexport const alphabet = \"0123456789abcdefghjkmnpqrstvwxyz\";\n\n// 0–31 → ASCII char code, for write-into-codes-then-fromCharCode encoding.\nconst valueToCharCode = new Uint8Array(32);\nfor (let i = 0; i < 32; i++) valueToCharCode[i] = alphabet.charCodeAt(i);\n\n// charCode → 0–31 value. Canonical lowercase only; upstream resolves case and\n// o/i/l aliases before any string reaches decodeBase32.\nconst INVALID = 0xff;\nconst charCodeToValue = new Uint8Array(256).fill(INVALID);\nfor (let i = 0; i < alphabet.length; i++) charCodeToValue[alphabet.charCodeAt(i)] = i;\n\nexport function encodeBase32(bytes: Uint8Array): string {\n // Build an Array<number> of char codes and pass it to fromCharCode.apply.\n // Faster than `result += char` (avoids cons-string overhead) and than\n // Uint8Array variants (apply has a fast path for plain Arrays).\n // oxlint-disable-next-line no-new-array\n const codes = new Array<number>(Math.floor((bytes.length * 8) / 5) + 1);\n let chi = 0;\n let bits = 0;\n let value = 0;\n\n for (let i = 0; i < bytes.length; i++) {\n value = (value << 8) | bytes[i]!;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n codes[chi++] = valueToCharCode[(value >>> bits) & 0x1f]!;\n }\n }\n codes[chi] = valueToCharCode[(value << (5 - bits)) & 0x1f]!;\n return String.fromCharCode.apply(null, codes);\n}\n\nexport function decodeBase32(str: string): Uint8Array {\n const result = new Uint8Array(Math.floor((str.length * 5) / 8));\n let bits = 0;\n let value = 0;\n let index = 0;\n\n for (let i = 0; i < str.length; i++) {\n const v = charCodeToValue[str.charCodeAt(i)]!;\n value = (value << 5) | v;\n bits += 5;\n if (bits >= 8) {\n bits -= 8;\n result[index++] = (value >>> bits) & 0xff;\n }\n }\n return result;\n}\n","import { decodeBase32, encodeBase32 } from \"../base32.js\";\nimport type { Id, Prefix } from \"../types.js\";\n\n/** Encodes a 16-byte payload as lowercase Crockford base32 (26 chars). */\nfunction encodePayload(bytes: Uint8Array): string {\n return encodeBase32(bytes);\n}\n\n/** Decodes a 26-char base32 payload suffix to 16 bytes. Trust-the-type. */\nfunction decodePayload(base32: string): Uint8Array {\n return decodeBase32(base32);\n}\n\n/** Composes a canonical wire ID from a prefix and 16-byte payload. */\nexport function toWireId<Brand extends string>(\n prefix: Prefix<Brand>,\n payload: Uint8Array,\n): Id<Brand> {\n return (prefix + encodePayload(payload)) as Id<Brand>;\n}\n\n/** Decodes the full 16-byte payload from a trusted wire ID. */\nexport function payloadBytesFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n id: Id<Brand>,\n): Uint8Array {\n return decodePayload(id.slice(prefix.length));\n}\n","// Payload is always 16 bytes on the wire (every codec). 16 bytes → 26 Crockford\n// base32 chars. ADR-0002 codifies this as the shared wire-format invariant.\nexport const payloadByteLength: number = 16;\nexport const payloadBase32Length: number = Math.ceil((payloadByteLength * 8) / 5);\n\n// Compact regex character class for the canonical lowercase Crockford alphabet\n// (`0123456789abcdefghjkmnpqrstvwxyz` — excludes i, l, o, u). Used in the JSON\n// Schema `pattern`, which describes the canonical wire form only (ADR-0003).\nexport const base32CharClass: string = \"[0-9a-hjkmnp-tv-z]\";\n","const registeredBrands = new Set<string>();\nconst warnedBrands = new Set<string>();\n\nexport function registerBrand(brand: string, allowDuplicateBrand: boolean | undefined): void {\n if (\n typeof process === \"undefined\" ||\n process.env.NODE_ENV === \"production\" ||\n allowDuplicateBrand\n ) {\n return;\n }\n\n if (registeredBrands.has(brand)) {\n if (!warnedBrands.has(brand)) {\n console.warn(\n `[@smonn/ids] brand \"${brand}\" was registered more than once — this usually indicates a bundling or import bug, or that more than one codec variant is using the same brand. Pass { allowDuplicateBrand: true } to silence.`,\n );\n warnedBrands.add(brand);\n }\n } else {\n registeredBrands.add(brand);\n }\n}\n","import { alphabet } from \"../base32.js\";\nimport type { Id, ParseError, ParseResult, Prefix } from \"../types.js\";\nimport { payloadBase32Length } from \"./invariants.js\";\n\nconst replacePattern = /[ilo]/g;\nconst aliasTestPattern = /[ilo]/;\nconst replacer = (match: string): string => (match === \"o\" ? \"0\" : \"1\");\nconst base32Pattern = new RegExp(`^[${alphabet}]{${payloadBase32Length}}$`);\n\nexport function safeParse<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n): ParseResult<Brand> {\n if (typeof value !== \"string\") return { ok: false, error: \"not_string\" };\n const lowercase = value.toLowerCase();\n if (!lowercase.startsWith(prefix)) return { ok: false, error: \"invalid_prefix\" };\n\n const sliced = lowercase.slice(prefix.length);\n const base32 = aliasTestPattern.test(sliced)\n ? sliced.replaceAll(replacePattern, replacer)\n : sliced;\n\n if (!base32Pattern.test(base32)) return { ok: false, error: \"invalid_base32\" };\n\n const id = (prefix + base32) as Id<Brand>;\n return { ok: true, id };\n}\n\nexport function is<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n): value is Id<Brand> {\n if (typeof value !== \"string\") return false;\n if (!value.startsWith(prefix)) return false;\n return base32Pattern.test(value.slice(prefix.length));\n}\n\nfunction errorMessage<Brand extends string>(prefix: Prefix<Brand>, error: ParseError): string {\n switch (error) {\n case \"not_string\":\n return \"expected string\";\n case \"invalid_prefix\":\n return `expected prefix '${prefix}'`;\n case \"invalid_base32\":\n return \"invalid base32 payload\";\n }\n}\n\nexport function standardValidate<Brand extends string>(\n prefix: Prefix<Brand>,\n value: unknown,\n):\n | { readonly value: Id<Brand>; readonly issues?: undefined }\n | { readonly issues: ReadonlyArray<{ readonly message: string }> } {\n const result = safeParse(prefix, value);\n if (result.ok) return { value: result.id };\n return { issues: [{ message: errorMessage(prefix, result.error) }] };\n}\n","import { IdsError } from \"../error.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"../types.js\";\nimport { base32CharClass, payloadBase32Length } from \"./invariants.js\";\nimport { is, safeParse, standardValidate } from \"./parse.js\";\n\ntype WireMethods<Brand extends string> = {\n is: (value: unknown) => value is Id<Brand>;\n parse: (value: unknown) => Id<Brand>;\n safeParse: (value: unknown) => ParseResult<Brand>;\n toJsonSchema: (brand: Brand, example: string) => JsonSchema;\n \"~standard\": StandardSchemaProps<Brand>;\n};\n\n/** Wire-only methods shared by every codec variant for a fixed prefix. */\nexport function wireMethods<Brand extends string>(prefix: Prefix<Brand>): WireMethods<Brand> {\n const standard: StandardSchemaProps<Brand> = {\n version: 1,\n vendor: \"@smonn/ids\",\n validate: (value: unknown) => standardValidate(prefix, value),\n };\n return {\n is: (value: unknown): value is Id<Brand> => is(prefix, value),\n parse: (value: unknown): Id<Brand> => {\n const result = safeParse(prefix, value);\n if (result.ok) return result.id;\n throw new IdsError(\"invalid_id\", `invalid ID: ${result.error}`, { cause: result.error });\n },\n safeParse: (value: unknown): ParseResult<Brand> => safeParse(prefix, value),\n toJsonSchema: (brand: Brand, example: string): JsonSchema => ({\n type: \"string\",\n pattern: `^${prefix}${base32CharClass}{${payloadBase32Length}}$`,\n description: `Branded ID for '${brand}'`,\n example,\n }),\n \"~standard\": standard,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { i as ParseResult, t as Id } from "./types-g7CiQDyE.mjs";
2
+ import { ConvertCustomConfig, PgCustomColumnBuilder } from "drizzle-orm/pg-core";
3
+
4
+ //#region src/drizzle.d.ts
5
+ /**
6
+ * Minimum codec interface required by the Drizzle adapter.
7
+ *
8
+ * Any codec variant satisfies this type — TimestampCodec, OpaqueTimestampCodec,
9
+ * and WrappedKeyCodec all expose `safeParse`. The adapter never calls
10
+ * `extractTimestamp`, `wrap`/`unwrap`, or any key-dependent method.
11
+ *
12
+ * Kysely and Prisma adapter issues should use this same codec contract shape.
13
+ */
14
+ type IdColumnCodec<Brand extends string> = {
15
+ safeParse(value: unknown): ParseResult<Brand>;
16
+ };
17
+ /**
18
+ * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.
19
+ *
20
+ * **Write path:** passes the `Id<Brand>` directly to the driver — it is already
21
+ * the canonical string form.
22
+ *
23
+ * **Read path:** normalises the raw DB string via `codec.safeParse()`, not strict
24
+ * `is()`. Data at rest should already be canonical per ADR-0003, but `safeParse`
25
+ * is a safe boundary in case stale non-canonical values exist. Throws if the
26
+ * value from the database does not parse as a valid `Id<Brand>`.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { idColumn } from "@smonn/ids/drizzle";
31
+ * import { createTimestampId } from "@smonn/ids";
32
+ *
33
+ * const usr = createTimestampId("usr");
34
+ * export const users = pgTable("users", { id: idColumn(usr).primaryKey() });
35
+ * // users.id is Id<"usr"> end-to-end
36
+ * ```
37
+ */
38
+ declare function idColumn<Brand extends string>(codec: IdColumnCodec<Brand>): PgCustomColumnBuilder<ConvertCustomConfig<"", {
39
+ data: Id<Brand>;
40
+ driverData: string;
41
+ }>>;
42
+ //#endregion
43
+ export { idColumn as n, IdColumnCodec as t };
44
+ //# sourceMappingURL=drizzle-CeSni5PB.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drizzle-CeSni5PB.d.mts","names":[],"sources":["../src/drizzle.ts"],"mappings":";;;;;;;;;;;;;KAoBY,aAAA;EACV,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;AA0B2B;iBAFpD,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,qBAAA,CAAsB,mBAAA;EAA0B,IAAA,EAAM,EAAA,CAAG,KAAA;EAAQ,UAAA;AAAA"}
@@ -0,0 +1,3 @@
1
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-DTr4i6Ic.mjs";
2
+ import { n as idColumn, t as IdColumnCodec } from "./drizzle-CeSni5PB.mjs";
3
+ export { IdColumnCodec, IdsError, type IdsErrorCode, idColumn, isIdsError };
@@ -0,0 +1,43 @@
1
+ import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
+ import { customType } from "drizzle-orm/pg-core";
3
+ //#region src/drizzle.ts
4
+ /**
5
+ * Drizzle custom column type that stores an `Id<Brand>` as a canonical `text` value.
6
+ *
7
+ * **Write path:** passes the `Id<Brand>` directly to the driver — it is already
8
+ * the canonical string form.
9
+ *
10
+ * **Read path:** normalises the raw DB string via `codec.safeParse()`, not strict
11
+ * `is()`. Data at rest should already be canonical per ADR-0003, but `safeParse`
12
+ * is a safe boundary in case stale non-canonical values exist. Throws if the
13
+ * value from the database does not parse as a valid `Id<Brand>`.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { idColumn } from "@smonn/ids/drizzle";
18
+ * import { createTimestampId } from "@smonn/ids";
19
+ *
20
+ * const usr = createTimestampId("usr");
21
+ * export const users = pgTable("users", { id: idColumn(usr).primaryKey() });
22
+ * // users.id is Id<"usr"> end-to-end
23
+ * ```
24
+ */
25
+ function idColumn(codec) {
26
+ return customType({
27
+ dataType() {
28
+ return "text";
29
+ },
30
+ toDriver(value) {
31
+ return value;
32
+ },
33
+ 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;
37
+ }
38
+ })();
39
+ }
40
+ //#endregion
41
+ export { IdsError, idColumn, isIdsError };
42
+
43
+ //# sourceMappingURL=drizzle.mjs.map
@@ -0,0 +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 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 Drizzle adapter.\n *\n * Any codec variant satisfies this type — TimestampCodec, OpaqueTimestampCodec,\n * and WrappedKeyCodec all expose `safeParse`. The adapter never calls\n * `extractTimestamp`, `wrap`/`unwrap`, or any key-dependent method.\n *\n * Kysely and Prisma adapter issues should use this same codec contract shape.\n */\nexport type IdColumnCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\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 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":";;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,SACd,OACyF;CACzF,OAAO,WAAoD;EACzD,WAAW;GACT,OAAO;EACT;EACA,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,CAAC,CAAC,CAAC;AACL"}
@@ -0,0 +1,52 @@
1
+ //#region src/error.ts
2
+ const BRAND = Symbol.for("@smonn/ids/IdsError");
3
+ /**
4
+ * The single error class thrown by caller-reachable public failures.
5
+ * Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.
6
+ * Recognized via `isIdsError()` — a branded guard that survives realm/dual-package duplication
7
+ * where bare `instanceof` would silently fail.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * try {
12
+ * usr.parse(rawInput);
13
+ * } catch (err) {
14
+ * if (isIdsError(err) && err.code === "invalid_id") return; // handle parse failure
15
+ * }
16
+ * ```
17
+ */
18
+ var IdsError = class extends Error {
19
+ code;
20
+ constructor(code, message, options) {
21
+ super(message, options);
22
+ this.name = "IdsError";
23
+ this.code = code;
24
+ Object.defineProperty(this, BRAND, {
25
+ value: true,
26
+ enumerable: false,
27
+ configurable: false,
28
+ writable: false
29
+ });
30
+ }
31
+ };
32
+ /**
33
+ * Type guard for `IdsError`. Checks a non-enumerable brand rather than bare `instanceof`
34
+ * so it survives realm/dual-package duplication (ESM + CJS dual package hazard).
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * if (isIdsError(err)) {
39
+ * switch (err.code) {
40
+ * case "verification_failed": // ...
41
+ * case "invalid_id": // ...
42
+ * }
43
+ * }
44
+ * ```
45
+ */
46
+ function isIdsError(value) {
47
+ return typeof value === "object" && value !== null && value[BRAND] === true;
48
+ }
49
+ //#endregion
50
+ export { isIdsError as n, IdsError as t };
51
+
52
+ //# sourceMappingURL=error-Cp5qYZcv.mjs.map
@@ -0,0 +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"}
@@ -0,0 +1,44 @@
1
+ //#region src/error.d.ts
2
+ /**
3
+ * The stable machine-readable failure reason carried by `IdsError`.
4
+ * Use `code` — not `message` — for programmatic branching; `message` is non-contractual.
5
+ * Adding a new member is minor-additive; renaming or removing one is breaking.
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";
8
+ /**
9
+ * The single error class thrown by caller-reachable public failures.
10
+ * Carries a stable `readonly code: IdsErrorCode` for programmatic discrimination.
11
+ * Recognized via `isIdsError()` — a branded guard that survives realm/dual-package duplication
12
+ * where bare `instanceof` would silently fail.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * try {
17
+ * usr.parse(rawInput);
18
+ * } catch (err) {
19
+ * if (isIdsError(err) && err.code === "invalid_id") return; // handle parse failure
20
+ * }
21
+ * ```
22
+ */
23
+ declare class IdsError extends Error {
24
+ readonly code: IdsErrorCode;
25
+ constructor(code: IdsErrorCode, message: string, options?: ErrorOptions);
26
+ }
27
+ /**
28
+ * Type guard for `IdsError`. Checks a non-enumerable brand rather than bare `instanceof`
29
+ * so it survives realm/dual-package duplication (ESM + CJS dual package hazard).
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * if (isIdsError(err)) {
34
+ * switch (err.code) {
35
+ * case "verification_failed": // ...
36
+ * case "invalid_id": // ...
37
+ * }
38
+ * }
39
+ * ```
40
+ */
41
+ declare function isIdsError(value: unknown): value is IdsError;
42
+ //#endregion
43
+ export { IdsErrorCode as n, isIdsError as r, IdsError as t };
44
+ //# sourceMappingURL=error-DTr4i6Ic.d.mts.map
@@ -0,0 +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"}
@@ -0,0 +1,85 @@
1
+ import { i as ParseResult, t as Id } from "./types-g7CiQDyE.mjs";
2
+ import { t as IdParamFailure } from "./adapter-types-oHCCSgOO.mjs";
3
+ import { NextFunction, Request, Response } from "express";
4
+
5
+ //#region src/express.d.ts
6
+ type IdCodec<Brand extends string> = {
7
+ safeParse(value: unknown): ParseResult<Brand>;
8
+ };
9
+ /**
10
+ * Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.
11
+ * Inspect `err.reason` and `err.status` in error-handling middleware.
12
+ */
13
+ declare class IdParamError extends Error {
14
+ readonly status: number;
15
+ readonly reason: "brand_mismatch" | "malformed";
16
+ constructor(reason: "brand_mismatch" | "malformed", status: number);
17
+ }
18
+ /** Options for `idParam`. All fields are optional. */
19
+ type IdParamOptions = {
20
+ /**
21
+ * Called instead of forwarding to `next(err)` when provided. The hook owns the response
22
+ * entirely — the adapter does not call `next(err)` itself.
23
+ */
24
+ onError?: (failure: IdParamFailure, req: Request, res: Response, next: NextFunction) => void;
25
+ /**
26
+ * Remap the default HTTP status for a failure reason without a full handler.
27
+ * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.
28
+ */
29
+ status?: {
30
+ brand_mismatch?: number;
31
+ malformed?: number;
32
+ };
33
+ };
34
+ /**
35
+ * Express middleware that validates a named route param against a codec via `safeParse`.
36
+ *
37
+ * **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and `reason`,
38
+ * so the app's existing error-handling middleware controls rendering. The adapter does not write
39
+ * a response body itself.
40
+ *
41
+ * **`options.onError`:** when provided, the hook owns the response entirely — the adapter does
42
+ * not call `next(err)`.
43
+ *
44
+ * **`options.status`:** remaps the default HTTP status for a reason without a full handler.
45
+ *
46
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
47
+ * - **Malformed or missing ID → `reason: "malformed"`, default 400**
48
+ *
49
+ * On success, stores the canonical `Id<Brand>` in `res.locals` under `paramName`
50
+ * and calls `next()`.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * import { idParam, IdParamError } from "@smonn/ids/express";
55
+ * import { createTimestampId } from "@smonn/ids";
56
+ *
57
+ * const usr = createTimestampId("usr");
58
+ *
59
+ * // Default: forwards error to app error-handling middleware
60
+ * app.get("/users/:id", idParam("id", usr), (req, res) => {
61
+ * const id = res.locals.id; // Id<"usr">, canonical
62
+ * });
63
+ *
64
+ * // Error-handling middleware receives the typed error
65
+ * app.use((err, req, res, next) => {
66
+ * if (err instanceof IdParamError) {
67
+ * res.status(err.status).json({ error: err.reason });
68
+ * return;
69
+ * }
70
+ * next(err);
71
+ * });
72
+ *
73
+ * // Override: consumer fully owns the response
74
+ * app.get("/orgs/:id", idParam("id", org, {
75
+ * onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),
76
+ * }), handler);
77
+ *
78
+ * // Or a lightweight status remap without a full handler
79
+ * app.get("/things/:id", idParam("id", thing, { status: { brand_mismatch: 400 } }), handler);
80
+ * ```
81
+ */
82
+ declare function idParam<ParamKey extends string, Brand extends string>(paramName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): (req: Request, res: Response<unknown, Record<ParamKey, Id<Brand>>>, next: NextFunction) => void;
83
+ //#endregion
84
+ export { IdParamError, type IdParamFailure, IdParamOptions, idParam };
85
+ //# sourceMappingURL=express.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.mts","names":[],"sources":["../src/express.ts"],"mappings":";;;;;KAMK,OAAA;EACH,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;cAO5B,YAAA,SAAqB,KAAA;EAAA,SACvB,MAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,MAAA;AAAA;AAJtD;AAAA,KAaY,cAAA;;;;;EAKV,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,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"}
@@ -0,0 +1,90 @@
1
+ //#region src/express.ts
2
+ /**
3
+ * Typed error forwarded to Express's error pipeline (`next(err)`) on validation failure.
4
+ * Inspect `err.reason` and `err.status` in error-handling middleware.
5
+ */
6
+ var IdParamError = class extends Error {
7
+ status;
8
+ reason;
9
+ constructor(reason, status) {
10
+ super(`ID validation failed: ${reason}`);
11
+ this.name = "IdParamError";
12
+ this.reason = reason;
13
+ this.status = status;
14
+ }
15
+ };
16
+ /**
17
+ * Express middleware that validates a named route param against a codec via `safeParse`.
18
+ *
19
+ * **Default (no options):** calls `next(err)` with an `IdParamError` carrying `status` and `reason`,
20
+ * so the app's existing error-handling middleware controls rendering. The adapter does not write
21
+ * a response body itself.
22
+ *
23
+ * **`options.onError`:** when provided, the hook owns the response entirely — the adapter does
24
+ * not call `next(err)`.
25
+ *
26
+ * **`options.status`:** remaps the default HTTP status for a reason without a full handler.
27
+ *
28
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
29
+ * - **Malformed or missing ID → `reason: "malformed"`, default 400**
30
+ *
31
+ * On success, stores the canonical `Id<Brand>` in `res.locals` under `paramName`
32
+ * and calls `next()`.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { idParam, IdParamError } from "@smonn/ids/express";
37
+ * import { createTimestampId } from "@smonn/ids";
38
+ *
39
+ * const usr = createTimestampId("usr");
40
+ *
41
+ * // Default: forwards error to app error-handling middleware
42
+ * app.get("/users/:id", idParam("id", usr), (req, res) => {
43
+ * const id = res.locals.id; // Id<"usr">, canonical
44
+ * });
45
+ *
46
+ * // Error-handling middleware receives the typed error
47
+ * app.use((err, req, res, next) => {
48
+ * if (err instanceof IdParamError) {
49
+ * res.status(err.status).json({ error: err.reason });
50
+ * return;
51
+ * }
52
+ * next(err);
53
+ * });
54
+ *
55
+ * // Override: consumer fully owns the response
56
+ * app.get("/orgs/:id", idParam("id", org, {
57
+ * onError: (failure, req, res) => res.status(failure.status).json({ error: failure.reason }),
58
+ * }), handler);
59
+ *
60
+ * // Or a lightweight status remap without a full handler
61
+ * app.get("/things/:id", idParam("id", thing, { status: { brand_mismatch: 400 } }), handler);
62
+ * ```
63
+ */
64
+ function idParam(paramName, codec, options) {
65
+ return (req, res, next) => {
66
+ const raw = req.params[paramName];
67
+ const result = codec.safeParse(raw);
68
+ if (!result.ok) {
69
+ const reason = result.error === "invalid_prefix" ? "brand_mismatch" : "malformed";
70
+ const defaultStatus = reason === "brand_mismatch" ? 404 : 400;
71
+ const status = options?.status?.[reason] ?? defaultStatus;
72
+ const failure = {
73
+ reason,
74
+ status
75
+ };
76
+ if (options?.onError) {
77
+ options.onError(failure, req, res, next);
78
+ return;
79
+ }
80
+ next(new IdParamError(reason, status));
81
+ return;
82
+ }
83
+ res.locals[paramName] = result.id;
84
+ next();
85
+ };
86
+ }
87
+ //#endregion
88
+ export { IdParamError, idParam };
89
+
90
+ //# sourceMappingURL=express.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.mjs","names":[],"sources":["../src/express.ts"],"sourcesContent":["import type { NextFunction, Request, Response } from \"express\";\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/**\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 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 options.onError(failure, req, res, next);\n return;\n }\n next(new IdParamError(reason, status));\n return;\n }\n (res.locals as Record<string, unknown>)[paramName] = result.id;\n next();\n };\n}\n"],"mappings":";;;;;AAcA,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,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,SAAS;IACpB,QAAQ,QAAQ,SAAS,KAAK,KAAK,IAAI;IACvC;GACF;GACA,KAAK,IAAI,aAAa,QAAQ,MAAM,CAAC;GACrC;EACF;EACA,IAAK,OAAmC,aAAa,OAAO;EAC5D,KAAK;CACP;AACF"}
@@ -0,0 +1,88 @@
1
+ import { i as ParseResult } from "./types-g7CiQDyE.mjs";
2
+ import { t as IdParamFailure } from "./adapter-types-oHCCSgOO.mjs";
3
+ import { FastifyReply, FastifyRequest } from "fastify";
4
+
5
+ //#region src/fastify.d.ts
6
+ type IdCodec<Brand extends string> = {
7
+ safeParse(value: unknown): ParseResult<Brand>;
8
+ };
9
+ /**
10
+ * Typed error thrown into Fastify's `setErrorHandler` on validation failure.
11
+ * Inspect `err.reason` and `err.statusCode` in your error handler.
12
+ */
13
+ declare class IdParamError extends Error {
14
+ readonly statusCode: number;
15
+ readonly reason: "brand_mismatch" | "malformed";
16
+ constructor(reason: "brand_mismatch" | "malformed", statusCode: number);
17
+ }
18
+ /** Options for `idParam`. All fields are optional. */
19
+ type IdParamOptions = {
20
+ /**
21
+ * Called instead of throwing when provided. The hook owns the response entirely —
22
+ * the adapter does not throw.
23
+ */
24
+ onError?: (failure: IdParamFailure, request: FastifyRequest, reply: FastifyReply) => void | Promise<void>;
25
+ /**
26
+ * Remap the default HTTP status for a failure reason without a full handler.
27
+ * e.g. `{ brand_mismatch: 400 }` treats both failure kinds as 400.
28
+ */
29
+ status?: {
30
+ brand_mismatch?: number;
31
+ malformed?: number;
32
+ };
33
+ };
34
+ /**
35
+ * Fastify `preHandler` hook factory that validates a named route param against a codec via `safeParse`.
36
+ *
37
+ * **Default (no options):** throws `IdParamError` carrying `statusCode` and `reason` so the app's
38
+ * existing `setErrorHandler` controls rendering. The adapter does not write a response body itself.
39
+ *
40
+ * **`options.onError`:** when provided, the hook calls `onError` and does not throw; the consumer
41
+ * fully owns the response via `reply`.
42
+ *
43
+ * **`options.status`:** remaps the default HTTP status for a reason without a full handler.
44
+ *
45
+ * - **Brand mismatch (`invalid_prefix`) → `reason: "brand_mismatch"`, default 404**
46
+ * - **Malformed or missing ID → `reason: "malformed"`, default 400**
47
+ *
48
+ * On success, stores the canonical `Id<Brand>` in `request.params` under `paramName`.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { idParam, IdParamError } from "@smonn/ids/fastify";
53
+ * import { createTimestampId } from "@smonn/ids";
54
+ *
55
+ * const usr = createTimestampId("usr");
56
+ *
57
+ * // Default: throws IdParamError → setErrorHandler renders it
58
+ * fastify.get("/users/:id", { preHandler: idParam("id", usr) }, (request, reply) => {
59
+ * const id = request.params.id; // Id<"usr">, canonical
60
+ * });
61
+ *
62
+ * // Error handler receives the typed error
63
+ * fastify.setErrorHandler((err, request, reply) => {
64
+ * if (err instanceof IdParamError) {
65
+ * reply.status(err.statusCode).send({ error: err.reason });
66
+ * return;
67
+ * }
68
+ * reply.send(err);
69
+ * });
70
+ *
71
+ * // Override: consumer fully owns the error response
72
+ * fastify.get("/orgs/:id", {
73
+ * preHandler: idParam("id", org, {
74
+ * onError: (failure, request, reply) =>
75
+ * reply.status(failure.status).send({ error: failure.reason }),
76
+ * }),
77
+ * }, handler);
78
+ *
79
+ * // Or a lightweight status remap without a full handler
80
+ * fastify.get("/things/:id", {
81
+ * preHandler: idParam("id", thing, { status: { brand_mismatch: 400 } }),
82
+ * }, handler);
83
+ * ```
84
+ */
85
+ declare function idParam<ParamKey extends string, Brand extends string>(paramName: ParamKey, codec: IdCodec<Brand>, options?: IdParamOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
86
+ //#endregion
87
+ export { IdParamError, type IdParamFailure, IdParamOptions, idParam };
88
+ //# sourceMappingURL=fastify.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.d.mts","names":[],"sources":["../src/fastify.ts"],"mappings":";;;;;KAMK,OAAA;EACH,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;cAO5B,YAAA,SAAqB,KAAA;EAAA,SACvB,UAAA;EAAA,SACA,MAAA;EAET,WAAA,CAAY,MAAA,kCAAwC,UAAA;AAAA;AAJtD;AAAA,KAaY,cAAA;;;;;EAKV,OAAA,IACE,OAAA,EAAS,cAAA,EACT,OAAA,EAAS,cAAA,EACT,KAAA,EAAO,YAAA,YACG,OAAA;;;;;EAKZ,MAAA;IAAW,cAAA;IAAyB,SAAA;EAAA;AAAA;;;;;;;;;;;;;;;;;;AAAA;AAsDtC;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIqD;;;;;;iBAJrC,OAAA,gDACd,SAAA,EAAW,QAAA,EACX,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,OAAA,GAAU,cAAA,IACR,OAAA,EAAS,cAAA,EAAgB,KAAA,EAAO,YAAA,KAAiB,OAAA"}