@smonn/ids 0.12.3 → 0.13.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 (80) hide show
  1. package/README.md +32 -2
  2. package/dist/{adapter-types-CdYJM6Sf.d.mts → adapter-types-CIc-4O-P.d.mts} +2 -2
  3. package/dist/{adapter-types-CdYJM6Sf.d.mts.map → adapter-types-CIc-4O-P.d.mts.map} +1 -1
  4. package/dist/cli.mjs +235 -103
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/codec-shell-DvrTDa65.mjs.map +1 -1
  7. package/dist/{digest-CknNw2wa.mjs → digest-Drnof-l_.mjs} +8 -23
  8. package/dist/digest-Drnof-l_.mjs.map +1 -0
  9. package/dist/digest.d.mts +3 -3
  10. package/dist/digest.d.mts.map +1 -1
  11. package/dist/digest.mjs +1 -1
  12. package/dist/drizzle.d.mts +3 -3
  13. package/dist/drizzle.d.mts.map +1 -1
  14. package/dist/drizzle.mjs.map +1 -1
  15. package/dist/error-Cp5qYZcv.mjs.map +1 -1
  16. package/dist/{error-JIPylU_E.d.mts → error-Dqyho9vp.d.mts} +7 -2
  17. package/dist/error-Dqyho9vp.d.mts.map +1 -0
  18. package/dist/express.d.mts +2 -2
  19. package/dist/fastify.d.mts +2 -2
  20. package/dist/graphql.d.mts +3 -3
  21. package/dist/graphql.mjs +2 -2
  22. package/dist/graphql.mjs.map +1 -1
  23. package/dist/hono.d.mts +2 -2
  24. package/dist/hono.mjs +1 -2
  25. package/dist/hono.mjs.map +1 -1
  26. package/dist/index.d.mts +21 -5
  27. package/dist/index.d.mts.map +1 -1
  28. package/dist/index.mjs +1 -1
  29. package/dist/{key-material-f29JIyrz.mjs → key-material-DsukgnR5.mjs} +53 -2
  30. package/dist/key-material-DsukgnR5.mjs.map +1 -0
  31. package/dist/kysely.d.mts +3 -3
  32. package/dist/kysely.d.mts.map +1 -1
  33. package/dist/kysely.mjs.map +1 -1
  34. package/dist/mikro-orm.d.mts +3 -3
  35. package/dist/mikro-orm.d.mts.map +1 -1
  36. package/dist/mikro-orm.mjs.map +1 -1
  37. package/dist/nestjs.d.mts +2 -2
  38. package/dist/nestjs.mjs +1 -1
  39. package/dist/nestjs.mjs.map +1 -1
  40. package/dist/{opaque-BQVNoIIh.mjs → opaque-D7y5cgzT.mjs} +3 -26
  41. package/dist/opaque-D7y5cgzT.mjs.map +1 -0
  42. package/dist/opaque.d.mts +22 -4
  43. package/dist/opaque.d.mts.map +1 -1
  44. package/dist/opaque.mjs +1 -1
  45. package/dist/prisma.d.mts +32 -27
  46. package/dist/prisma.d.mts.map +1 -1
  47. package/dist/prisma.mjs +11 -15
  48. package/dist/prisma.mjs.map +1 -1
  49. package/dist/{reverse-DsPd7Lco.mjs → reverse-DrAofYWV.mjs} +10 -3
  50. package/dist/reverse-DrAofYWV.mjs.map +1 -0
  51. package/dist/reverse.d.mts +20 -4
  52. package/dist/reverse.d.mts.map +1 -1
  53. package/dist/reverse.mjs +1 -1
  54. package/dist/{signed-4h2BnlWx.mjs → signed-B2Aa3zMg.mjs} +10 -31
  55. package/dist/signed-B2Aa3zMg.mjs.map +1 -0
  56. package/dist/signed.d.mts +13 -4
  57. package/dist/signed.d.mts.map +1 -1
  58. package/dist/signed.mjs +1 -1
  59. package/dist/{timestamp-Cg9nRfnK.mjs → timestamp-YPd58344.mjs} +10 -3
  60. package/dist/timestamp-YPd58344.mjs.map +1 -0
  61. package/dist/typeorm.d.mts +2 -2
  62. package/dist/typeorm.d.mts.map +1 -1
  63. package/dist/typeorm.mjs.map +1 -1
  64. package/dist/{types-g7CiQDyE.d.mts → types-wplmOgOK.d.mts} +20 -3
  65. package/dist/types-wplmOgOK.d.mts.map +1 -0
  66. package/dist/{wrapped-BQ-lNECo.mjs → wrapped-BjmVzuYc.mjs} +17 -75
  67. package/dist/wrapped-BjmVzuYc.mjs.map +1 -0
  68. package/dist/wrapped.d.mts +31 -5
  69. package/dist/wrapped.d.mts.map +1 -1
  70. package/dist/wrapped.mjs +1 -1
  71. package/package.json +80 -27
  72. package/dist/digest-CknNw2wa.mjs.map +0 -1
  73. package/dist/error-JIPylU_E.d.mts.map +0 -1
  74. package/dist/key-material-f29JIyrz.mjs.map +0 -1
  75. package/dist/opaque-BQVNoIIh.mjs.map +0 -1
  76. package/dist/reverse-DsPd7Lco.mjs.map +0 -1
  77. package/dist/signed-4h2BnlWx.mjs.map +0 -1
  78. package/dist/timestamp-Cg9nRfnK.mjs.map +0 -1
  79. package/dist/types-g7CiQDyE.d.mts.map +0 -1
  80. package/dist/wrapped-BQ-lNECo.mjs.map +0 -1
package/dist/graphql.mjs CHANGED
@@ -3,7 +3,7 @@ import { GraphQLError, GraphQLScalarType, Kind } from "graphql";
3
3
  /**
4
4
  * Builds a `GraphQLScalarType` for the given codec and brand.
5
5
  *
6
- * - `serialize` — identity pass-through; an `Id<Brand>` is already the canonical wire string.
6
+ * - `serialize` — validates via `codec.safeParse`; throws `GraphQLError` on a non-conforming value.
7
7
  * - `parseValue` — validates variables via `codec.safeParse`; throws `GraphQLError` on failure.
8
8
  * - `parseLiteral` — validates inline `Kind.STRING` literals; throws `GraphQLError` for any
9
9
  * other AST kind or on a failed `safeParse`.
@@ -28,7 +28,7 @@ function idScalar(codec, config) {
28
28
  return new GraphQLScalarType({
29
29
  name: config.name,
30
30
  description: config.description,
31
- serialize: (value) => value,
31
+ serialize: parse,
32
32
  parseValue: parse,
33
33
  parseLiteral: (ast) => {
34
34
  if (ast.kind !== Kind.STRING) throw new GraphQLError(`${config.name} must be a string literal, got ${ast.kind}`);
@@ -1 +1 @@
1
- {"version":3,"file":"graphql.mjs","names":[],"sources":["../src/adapters/graphql.ts"],"sourcesContent":["import { GraphQLError, GraphQLScalarType, Kind } from \"graphql\";\nimport type { ValueNode } from \"graphql\";\nimport type { IdCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/**\n * Builds a `GraphQLScalarType` for the given codec and brand.\n *\n * - `serialize` — identity pass-through; an `Id<Brand>` is already the canonical wire string.\n * - `parseValue` — validates variables via `codec.safeParse`; throws `GraphQLError` on failure.\n * - `parseLiteral` — validates inline `Kind.STRING` literals; throws `GraphQLError` for any\n * other AST kind or on a failed `safeParse`.\n *\n * `graphql` must be installed as a peer dependency.\n *\n * @example\n * ```ts\n * import { idScalar } from \"@smonn/ids/graphql\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const UserIdScalar = idScalar(usr, { name: \"UserId\", description: \"A branded user ID.\" });\n * ```\n */\nexport function idScalar<Brand extends string>(\n codec: IdCodec<Brand>,\n config: { name: string; description?: string },\n): GraphQLScalarType<Id<Brand>, string> {\n const parse = (value: unknown): Id<Brand> => {\n const result = codec.safeParse(value);\n if (!result.ok) {\n throw new GraphQLError(`invalid ${config.name}: ${result.error}`);\n }\n return result.id;\n };\n return new GraphQLScalarType<Id<Brand>, string>({\n name: config.name,\n description: config.description,\n serialize: (value) => value as string,\n parseValue: parse,\n parseLiteral: (ast: ValueNode) => {\n if (ast.kind !== Kind.STRING) {\n throw new GraphQLError(`${config.name} must be a string literal, got ${ast.kind}`);\n }\n return parse(ast.value);\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,SACd,OACA,QACsC;CACtC,MAAM,SAAS,UAA8B;EAC3C,MAAM,SAAS,MAAM,UAAU,KAAK;EACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,aAAa,WAAW,OAAO,KAAK,IAAI,OAAO,OAAO;EAElE,OAAO,OAAO;CAChB;CACA,OAAO,IAAI,kBAAqC;EAC9C,MAAM,OAAO;EACb,aAAa,OAAO;EACpB,YAAY,UAAU;EACtB,YAAY;EACZ,eAAe,QAAmB;GAChC,IAAI,IAAI,SAAS,KAAK,QACpB,MAAM,IAAI,aAAa,GAAG,OAAO,KAAK,iCAAiC,IAAI,MAAM;GAEnF,OAAO,MAAM,IAAI,KAAK;EACxB;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"graphql.mjs","names":[],"sources":["../src/adapters/graphql.ts"],"sourcesContent":["import { GraphQLError, GraphQLScalarType, Kind } from \"graphql\";\nimport type { ValueNode } from \"graphql\";\nimport type { IdCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/**\n * Builds a `GraphQLScalarType` for the given codec and brand.\n *\n * - `serialize` — validates via `codec.safeParse`; throws `GraphQLError` on a non-conforming value.\n * - `parseValue` — validates variables via `codec.safeParse`; throws `GraphQLError` on failure.\n * - `parseLiteral` — validates inline `Kind.STRING` literals; throws `GraphQLError` for any\n * other AST kind or on a failed `safeParse`.\n *\n * `graphql` must be installed as a peer dependency.\n *\n * @example\n * ```ts\n * import { idScalar } from \"@smonn/ids/graphql\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const UserIdScalar = idScalar(usr, { name: \"UserId\", description: \"A branded user ID.\" });\n * ```\n */\nexport function idScalar<Brand extends string>(\n codec: IdCodec<Brand>,\n config: { name: string; description?: string },\n): GraphQLScalarType<Id<Brand>, string> {\n const parse = (value: unknown): Id<Brand> => {\n const result = codec.safeParse(value);\n if (!result.ok) {\n throw new GraphQLError(`invalid ${config.name}: ${result.error}`);\n }\n return result.id;\n };\n return new GraphQLScalarType<Id<Brand>, string>({\n name: config.name,\n description: config.description,\n serialize: parse,\n parseValue: parse,\n parseLiteral: (ast: ValueNode) => {\n if (ast.kind !== Kind.STRING) {\n throw new GraphQLError(`${config.name} must be a string literal, got ${ast.kind}`);\n }\n return parse(ast.value);\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,SACd,OACA,QACsC;CACtC,MAAM,SAAS,UAA8B;EAC3C,MAAM,SAAS,MAAM,UAAU,KAAK;EACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,aAAa,WAAW,OAAO,KAAK,IAAI,OAAO,OAAO;EAElE,OAAO,OAAO;CAChB;CACA,OAAO,IAAI,kBAAqC;EAC9C,MAAM,OAAO;EACb,aAAa,OAAO;EACpB,WAAW;EACX,YAAY;EACZ,eAAe,QAAmB;GAChC,IAAI,IAAI,SAAS,KAAK,QACpB,MAAM,IAAI,aAAa,GAAG,OAAO,KAAK,iCAAiC,IAAI,MAAM;GAEnF,OAAO,MAAM,IAAI,KAAK;EACxB;CACF,CAAC;AACH"}
package/dist/hono.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { t as Id } from "./types-g7CiQDyE.mjs";
2
- import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CdYJM6Sf.mjs";
1
+ import { t as Id } from "./types-wplmOgOK.mjs";
2
+ import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CIc-4O-P.mjs";
3
3
  import { ContentfulStatusCode } from "hono/utils/http-status";
4
4
  import { Context, MiddlewareHandler } from "hono";
5
5
 
package/dist/hono.mjs CHANGED
@@ -46,8 +46,7 @@ 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
- const defaultStatus = failure.reason === "brand_mismatch" ? 404 : 400;
50
- throw new HTTPException(options?.status?.[failure.reason] ?? defaultStatus);
49
+ throw new HTTPException(failure.status);
51
50
  }
52
51
  c.set(paramName, result.id);
53
52
  await next();
package/dist/hono.mjs.map CHANGED
@@ -1 +1 @@
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"}
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 throw new HTTPException(failure.status as ContentfulStatusCode);\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,IAAI,cAAc,QAAQ,MAA8B;EAChE;EACA,EAAE,IAAI,WAAW,OAAO,EAAE;EAC1B,MAAM,KAAK;CAEb;AACF"}
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
- import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, r as ParseError, t as Id } from "./types-g7CiQDyE.mjs";
1
+ import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, o as ValidBrand, r as ParseError, t as Id } from "./types-wplmOgOK.mjs";
2
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-Dqyho9vp.mjs";
3
3
 
4
4
  //#region src/codecs/timestamp/index.d.ts
5
5
  /**
@@ -37,10 +37,19 @@ type TimestampCodec<Brand extends string> = {
37
37
  safeParse(value: unknown): ParseResult<Brand>;
38
38
  /**
39
39
  * Decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.
40
+ *
41
+ * Best-effort: decodes the timestamp bytes in the payload without any additional
42
+ * verification. An ID that bypassed `safeParse()` (e.g. via a type assertion)
43
+ * may return a plausible-looking but incorrect `Date`.
40
44
  */
41
45
  extractTimestamp(id: Id<Brand>): Date; /** Tight lower bound for any ID generated at `date` (random portion `0x00`). Throws on invalid dates. */
42
46
  minIdForTime(date: Date): Id<Brand>; /** Tight upper bound for any ID generated at `date` (random portion `0xff`). Throws on invalid dates. */
43
- maxIdForTime(date: Date): Id<Brand>; /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */
47
+ maxIdForTime(date: Date): Id<Brand>;
48
+ /**
49
+ * JSON Schema for the canonical wire form. The `pattern` matches the canonical stored
50
+ * form only and is deliberately stricter than `parse()`/`safeParse()`, which accept
51
+ * uppercase letters and Crockford aliases (`o`/`i`/`l`) before normalising. See ADR-0003.
52
+ */
44
53
  toJsonSchema(): JsonSchema; /** Standard Schema validate entry point. */
45
54
  readonly "~standard": StandardSchemaProps<Brand>;
46
55
  };
@@ -49,8 +58,15 @@ type TimestampCodec<Brand extends string> = {
49
58
  *
50
59
  * @param brand - Entity type brand validated once at construction.
51
60
  * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.
61
+ * @example
62
+ * ```ts
63
+ * const users = createTimestampId("usr");
64
+ *
65
+ * const id = users.generate(); // Id<"usr">
66
+ * users.extractTimestamp(id); // Date
67
+ * ```
52
68
  */
53
- declare function createTimestampId<Brand extends string>(brand: Brand, opts?: TimestampOptions): TimestampCodec<Brand>;
69
+ declare function createTimestampId<Brand extends string>(brand: Brand & ValidBrand<Brand>, opts?: TimestampOptions): TimestampCodec<Brand>;
54
70
  //#endregion
55
- export { type Id, IdsError, type IdsErrorCode, type JsonSchema, type ParseError, type ParseResult, type TimestampCodec, type TimestampOptions, createTimestampId, isIdsError };
71
+ export { type Id, IdsError, type IdsErrorCode, type JsonSchema, type ParseError, type ParseResult, type TimestampCodec, type TimestampOptions, type ValidBrand, createTimestampId, isIdsError };
56
72
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/codecs/timestamp/index.ts"],"mappings":";;;;;;;KAUY,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;;;;;;;iBAiB5B,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":";;;;;;;KAiBY,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;;;;;;;;EAQvC,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;;;;;;EAM7B,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;;;;;;;;iBAwB5B,iBAAA,uBACd,KAAA,EAAO,KAAA,GAAQ,UAAA,CAAW,KAAA,GAC1B,IAAA,GAAM,gBAAA,GACL,cAAA,CAAe,KAAA"}
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { t as createTimestampId } from "./timestamp-Cg9nRfnK.mjs";
2
+ import { t as createTimestampId } from "./timestamp-YPd58344.mjs";
3
3
  export { IdsError, createTimestampId, isIdsError };
@@ -1,4 +1,5 @@
1
1
  import { t as IdsError } from "./error-Cp5qYZcv.mjs";
2
+ import "./codec-shell-DvrTDa65.mjs";
2
3
  //#region src/codecs/_kernel/bytes.ts
3
4
  const hexDigits = "0123456789abcdef";
4
5
  const invalidNibble = 255;
@@ -39,6 +40,13 @@ function encodeBase64Url(bytes) {
39
40
  for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
40
41
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
41
42
  }
43
+ /** Writes a 32-bit integer as four big-endian bytes into target[offset..offset+3]. */
44
+ function writeLen32(value, target, offset) {
45
+ target[offset] = value >>> 24 & 255;
46
+ target[offset + 1] = value >>> 16 & 255;
47
+ target[offset + 2] = value >>> 8 & 255;
48
+ target[offset + 3] = value & 255;
49
+ }
42
50
  /** Decodes a base64url string to raw bytes. Throws on invalid input. */
43
51
  function decodeBase64Url(encoded) {
44
52
  const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
@@ -49,6 +57,49 @@ function decodeBase64Url(encoded) {
49
57
  return out;
50
58
  }
51
59
  //#endregion
60
+ //#region src/codecs/_kernel/crypto.ts
61
+ const zeroIv = /* @__PURE__ */ new Uint8Array(16);
62
+ const pkcsPad = 16;
63
+ function timingSafeEqual(a, b) {
64
+ if (a.length !== b.length) return false;
65
+ let diff = 0;
66
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
67
+ return diff === 0;
68
+ }
69
+ async function encryptPayload(key, plaintext) {
70
+ return new Uint8Array(await crypto.subtle.encrypt({
71
+ name: "AES-CBC",
72
+ iv: zeroIv
73
+ }, key, plaintext)).subarray(0, 16);
74
+ }
75
+ async function decryptPayload(key, c1) {
76
+ const c2Input = /* @__PURE__ */ new Uint8Array(16);
77
+ for (let i = 0; i < 16; i++) c2Input[i] = pkcsPad ^ c1[i];
78
+ const c2Encrypted = new Uint8Array(await crypto.subtle.encrypt({
79
+ name: "AES-CBC",
80
+ iv: zeroIv
81
+ }, key, c2Input));
82
+ const ciphertext = /* @__PURE__ */ new Uint8Array(32);
83
+ ciphertext.set(c1, 0);
84
+ ciphertext.set(c2Encrypted.subarray(0, 16), 16);
85
+ return new Uint8Array(await crypto.subtle.decrypt({
86
+ name: "AES-CBC",
87
+ iv: zeroIv
88
+ }, key, ciphertext));
89
+ }
90
+ /**
91
+ * @param info - codec-specific HKDF domain-separation label; see ADR-0019.
92
+ */
93
+ async function deriveKey(bytes, info, keySpec, keyUsages) {
94
+ const base = await crypto.subtle.importKey("raw", bytes, "HKDF", false, ["deriveKey"]);
95
+ return crypto.subtle.deriveKey({
96
+ name: "HKDF",
97
+ hash: "SHA-256",
98
+ salt: /* @__PURE__ */ new Uint8Array(),
99
+ info
100
+ }, base, keySpec, false, keyUsages);
101
+ }
102
+ //#endregion
52
103
  //#region src/codecs/_kernel/key-material.ts
53
104
  const validByteLengths = /* @__PURE__ */ new Set([
54
105
  16,
@@ -132,6 +183,6 @@ function decodeKeyMaterial(encoded, format, formatNoun, lengthNoun) {
132
183
  return bytes;
133
184
  }
134
185
  //#endregion
135
- export { encodeKeyMaterial as i, assertValidKeyring as n, decodeKeyMaterial as r, assertValidKeyMaterialByteLength as t };
186
+ export { decryptPayload as a, timingSafeEqual as c, encodeKeyMaterial as i, writeLen32 as l, assertValidKeyring as n, deriveKey as o, decodeKeyMaterial as r, encryptPayload as s, assertValidKeyMaterialByteLength as t };
136
187
 
137
- //# sourceMappingURL=key-material-f29JIyrz.mjs.map
188
+ //# sourceMappingURL=key-material-DsukgnR5.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-material-DsukgnR5.mjs","names":[],"sources":["../src/codecs/_kernel/bytes.ts","../src/codecs/_kernel/crypto.ts","../src/codecs/_kernel/key-material.ts"],"sourcesContent":["const hexDigits = \"0123456789abcdef\";\n\nconst invalidNibble = 0xff;\nconst hexCharCodeToNibble = new Uint8Array(128).fill(invalidNibble);\nfor (let i = 0; i < 10; i++) hexCharCodeToNibble[48 + i] = i;\nfor (let i = 0; i < 6; i++) {\n hexCharCodeToNibble[97 + i] = 10 + i;\n hexCharCodeToNibble[65 + i] = 10 + i;\n}\n\n/** Lowercase hex encoding of raw bytes. */\nexport function encodeHex(bytes: Uint8Array): string {\n // oxlint-disable-next-line no-new-array\n const codes = new Array<number>(bytes.length * 2);\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i]!;\n codes[i * 2] = hexDigits.charCodeAt(b >>> 4);\n codes[i * 2 + 1] = hexDigits.charCodeAt(b & 0x0f);\n }\n return String.fromCharCode(...codes);\n}\n\n/** Decodes a hex string to raw bytes. Throws on non-hex input. */\nexport function decodeHex(encoded: string): Uint8Array {\n if (encoded.length % 2 !== 0) throw new Error(\"invalid hex\");\n const out = new Uint8Array(encoded.length / 2);\n for (let i = 0; i < out.length; i++) {\n const hiCode = encoded.charCodeAt(i * 2);\n const loCode = encoded.charCodeAt(i * 2 + 1);\n if (hiCode >= hexCharCodeToNibble.length || loCode >= hexCharCodeToNibble.length) {\n throw new Error(\"invalid hex\");\n }\n const hi = hexCharCodeToNibble[hiCode]!;\n const lo = hexCharCodeToNibble[loCode]!;\n if (hi === invalidNibble || lo === invalidNibble) {\n throw new Error(\"invalid hex\");\n }\n out[i] = (hi << 4) | lo;\n }\n return out;\n}\n\n/** Base64url encoding without padding. */\nexport function encodeBase64Url(bytes: Uint8Array): string {\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** Writes a 32-bit integer as four big-endian bytes into target[offset..offset+3]. */\nexport function 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\n/** Decodes a base64url string to raw bytes. Throws on invalid input. */\nexport function decodeBase64Url(encoded: string): Uint8Array {\n const base64 = encoded.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const pad = (4 - (base64.length % 4)) % 4;\n const binary = atob(base64 + \"=\".repeat(pad));\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n}\n","import type { webcrypto } from \"node:crypto\";\nimport { payloadByteLength } from \"../../wire/invariants.js\";\n\nconst zeroIv = new Uint8Array(payloadByteLength);\nconst pkcsPad = 0x10;\n\nexport function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;\n return diff === 0;\n}\n\nexport async function encryptPayload(\n key: webcrypto.CryptoKey,\n plaintext: Uint8Array,\n): Promise<Uint8Array> {\n const encrypted = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n plaintext as Uint8Array<ArrayBuffer>,\n ),\n );\n return encrypted.subarray(0, payloadByteLength);\n}\n\n// AES-CBC strip-and-reconstruct decrypt (ADR-0004). The wire carries only C1\n// (16 bytes); C2 = AES_K(P2 XOR C1) where P2 is the PKCS#7 pad block (0x10×16).\n// Recompute C2 via CBC encrypt of (P2 XOR C1) with IV=0, then decrypt C1‖C2.\nexport async function decryptPayload(\n key: webcrypto.CryptoKey,\n c1: Uint8Array,\n): Promise<Uint8Array> {\n const c2Input = new Uint8Array(payloadByteLength);\n for (let i = 0; i < payloadByteLength; i++) c2Input[i] = pkcsPad ^ c1[i]!;\n const c2Encrypted = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n c2Input as Uint8Array<ArrayBuffer>,\n ),\n );\n const ciphertext = new Uint8Array(payloadByteLength * 2);\n ciphertext.set(c1, 0);\n ciphertext.set(c2Encrypted.subarray(0, payloadByteLength), payloadByteLength);\n return new Uint8Array(\n await crypto.subtle.decrypt(\n { name: \"AES-CBC\", iv: zeroIv },\n key,\n ciphertext as Uint8Array<ArrayBuffer>,\n ),\n );\n}\n\nexport { writeLen32 } from \"./bytes.js\";\n\n/**\n * @param info - codec-specific HKDF domain-separation label; see ADR-0019.\n */\nexport async function deriveKey(\n bytes: Uint8Array,\n info: Uint8Array,\n keySpec: webcrypto.AesDerivedKeyParams | webcrypto.HmacImportParams,\n keyUsages: webcrypto.KeyUsage[],\n): 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 {\n name: \"HKDF\",\n hash: \"SHA-256\",\n salt: new Uint8Array(), // empty salt: IKM is already uniform random; see ADR-0019\n info: info as Uint8Array<ArrayBuffer>,\n },\n base,\n keySpec,\n false,\n keyUsages,\n );\n}\n","import { decodeBase64Url, decodeHex, encodeBase64Url, encodeHex } from \"./bytes.js\";\nimport { IdsError } from \"../../error.js\";\n\ntype KeyMaterialFormat = \"hex\" | \"base64url\";\n\nconst validByteLengths = new Set([16, 24, 32]);\n\nfunction formatForError(value: unknown): string {\n try {\n return String(value);\n } catch {\n return \"[unprintable]\";\n }\n}\n\nfunction assertKeyMaterialFormat(\n format: unknown,\n noun: string,\n): asserts format is KeyMaterialFormat {\n if (format !== \"hex\" && format !== \"base64url\") {\n throw new IdsError(\n \"invalid_key_format\",\n `invalid ${noun} key format: expected hex or base64url, got '${formatForError(format)}'`,\n );\n }\n}\n\n/**\n * Throws `empty_keyring` when `keys` is empty.\n * `noun` appears in the message (e.g. `\"signing\"` → \"signing keyring must contain at least one key\").\n */\nfunction assertNonEmptyKeyring<K = unknown>(keys: readonly K[], noun: string): void {\n if (keys.length === 0) {\n throw new IdsError(\"empty_keyring\", `${noun} keyring must contain at least one key`);\n }\n}\n\n/**\n * Throws `duplicate_keyring_entry` when any two entries in `keys` compare equal.\n * Uses the caller-supplied constant-time `keysEqual` comparator.\n */\nfunction assertNoDuplicateKeyringEntries<K>(\n keys: readonly K[],\n keysEqual: (a: K, b: K) => boolean,\n noun: string,\n): void {\n for (let i = 0; i < keys.length; i++) {\n for (let j = i + 1; j < keys.length; j++) {\n if (keysEqual(keys[i]!, keys[j]!)) {\n throw new IdsError(\"duplicate_keyring_entry\", `duplicate ${noun} key in keyring`);\n }\n }\n }\n}\n\n/**\n * Asserts that `keys` is non-empty and contains no pairwise duplicates.\n *\n * Combines {@link assertNonEmptyKeyring} and {@link assertNoDuplicateKeyringEntries}\n * into a single call for codec constructors that validate a keyring at construction.\n *\n * @param keys - The keyring to validate.\n * @param keysEqual - Constant-time comparator (e.g. `wrappingKeysEqual`, `signingKeysEqual`).\n * @param noun - Noun used in error messages (e.g. `\"wrapping\"`, `\"signing\"`).\n */\nexport function assertValidKeyring<K>(\n keys: readonly K[],\n keysEqual: (a: K, b: K) => boolean,\n noun: string,\n): void {\n assertNonEmptyKeyring(keys, noun);\n assertNoDuplicateKeyringEntries(keys, keysEqual, noun);\n}\n\n/** Throws `invalid_key_length` when `byteLength` is not 16, 24, or 32. */\nexport function assertValidKeyMaterialByteLength(byteLength: number, noun: string): void {\n if (!validByteLengths.has(byteLength)) {\n throw new IdsError(\n \"invalid_key_length\",\n `invalid ${noun} key length: expected 16, 24, or 32 bytes, got ${byteLength}`,\n );\n }\n}\n\n/**\n * Encodes raw key bytes as hex or base64url.\n *\n * `formatNoun` appears in format error messages; `lengthNoun` in length error messages.\n * For most key types both are the same (e.g. `\"wrapping\"`, `\"signing\"`). For the\n * Opaque key, they differ (`\"opaque\"` and `\"AES\"` respectively) to preserve the\n * original human-readable messages.\n */\nexport function encodeKeyMaterial(\n bytes: Uint8Array,\n format: KeyMaterialFormat,\n formatNoun: string,\n lengthNoun: string,\n): string {\n assertKeyMaterialFormat(format, formatNoun);\n assertValidKeyMaterialByteLength(bytes.length, lengthNoun);\n if (format === \"hex\") return encodeHex(bytes);\n return encodeBase64Url(bytes);\n}\n\n/**\n * Decodes a hex or base64url-encoded key string back to raw bytes.\n *\n * `formatNoun` appears in format error messages; `lengthNoun` in length error messages.\n */\nexport function decodeKeyMaterial(\n encoded: string,\n format: KeyMaterialFormat,\n formatNoun: string,\n lengthNoun: string,\n): Uint8Array {\n assertKeyMaterialFormat(format, formatNoun);\n let bytes: Uint8Array;\n if (format === \"hex\") {\n if (encoded.length === 0 || encoded.length % 2 !== 0) {\n throw new IdsError(\n \"invalid_key_encoding\",\n \"invalid hex key: length must be a positive even number of characters\",\n );\n }\n if (!/^[0-9a-fA-F]+$/.test(encoded)) {\n throw new IdsError(\"invalid_key_encoding\", \"invalid hex key: expected [0-9a-fA-F] only\");\n }\n bytes = decodeHex(encoded);\n } else {\n try {\n bytes = decodeBase64Url(encoded);\n } catch {\n throw new IdsError(\"invalid_key_encoding\", \"invalid base64url key\");\n }\n }\n assertValidKeyMaterialByteLength(bytes.length, lengthNoun);\n return bytes;\n}\n"],"mappings":"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"}
package/dist/kysely.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
- import { t as Id } from "./types-g7CiQDyE.mjs";
3
- import { n as IdColumnCodec } from "./adapter-types-CdYJM6Sf.mjs";
1
+ import { t as Id } from "./types-wplmOgOK.mjs";
2
+ import { n as IdColumnCodec } from "./adapter-types-CIc-4O-P.mjs";
3
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-Dqyho9vp.mjs";
4
4
  import { ColumnType } from "kysely";
5
5
 
6
6
  //#region src/adapters/kysely.d.ts
@@ -1 +1 @@
1
- {"version":3,"file":"kysely.d.mts","names":[],"sources":["../src/adapters/kysely.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;KA0BY,YAAA,yBAAqC,UAAA,CAAW,EAAA,CAAG,KAAA,GAAQ,EAAA,CAAG,KAAA,GAAQ,EAAA,CAAG,KAAA;;;AAAA;AA2BrF;;;;;;;;;;;;;;;;;;;;;;iBAAgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA;EAErB,QAAA,CAAS,KAAA,EAAO,EAAA,CAAG,KAAA;EACnB,UAAA,CAAW,KAAA,WAAgB,EAAA,CAAG,KAAA;AAAA"}
1
+ {"version":3,"file":"kysely.d.mts","names":[],"sources":["../src/adapters/kysely.ts"],"mappings":";;;;;;AAyBA;;;;;;;;;;;;;;;;;AAAA,KAAY,YAAA,yBAAqC,UAAA,CAAW,EAAA,CAAG,KAAA,GAAQ,EAAA,CAAG,KAAA,GAAQ,EAAA,CAAG,KAAA;;;;AAAA;AA2BrF;;;;;;;;;;;;;;;;;;;;;iBAAgB,QAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA;EAErB,QAAA,CAAS,KAAA,EAAO,EAAA,CAAG,KAAA;EACnB,UAAA,CAAW,KAAA,WAAgB,EAAA,CAAG,KAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"kysely.mjs","names":[],"sources":["../src/adapters/kysely.ts"],"sourcesContent":["import type { ColumnType } from \"kysely\";\nimport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdColumnCodec } from \"./adapter-types.js\";\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode };\n\n/**\n * Kysely column type mapping for `Id<Brand>`.\n *\n * Use this in your Kysely `Database` interface to type a column as `Id<Brand>` at\n * the TypeScript level. Pair it with `idColumn(codec)` for runtime read/write\n * transformation.\n *\n * @example\n * ```ts\n * import type { IdColumnType } from \"@smonn/ids/kysely\";\n * import type { Id } from \"@smonn/ids\";\n *\n * interface Database {\n * users: { id: IdColumnType<\"usr\"> };\n * }\n * ```\n */\nexport type IdColumnType<Brand extends string> = ColumnType<Id<Brand>, Id<Brand>, Id<Brand>>;\n\n/**\n * Kysely column adapter bound to a codec.\n *\n * Returns an object with `fromDriver` / `toDriver` helpers that mirror the read/write\n * contract of the Drizzle adapter — same error message, same strictness (safeParse on\n * read, identity on write).\n *\n * **Write path:** passes the `Id<Brand>` directly to the driver — it is already\n * the canonical string form.\n *\n * **Read path:** normalises the raw DB string via `codec.safeParse()`. Throws if\n * the value does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/kysely\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const usrCol = idColumn(usr);\n *\n * // In a query result handler:\n * const id = usrCol.fromDriver(row.id);\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): {\n toDriver(value: Id<Brand>): string;\n fromDriver(value: string): Id<Brand>;\n} {\n return {\n toDriver(value: Id<Brand>): string {\n return value;\n },\n fromDriver(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgB,SACd,OAIA;CACA,OAAO;EACL,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF"}
1
+ {"version":3,"file":"kysely.mjs","names":[],"sources":["../src/adapters/kysely.ts"],"sourcesContent":["import type { ColumnType } from \"kysely\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdColumnCodec } from \"./adapter-types.js\";\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\n\n/**\n * Kysely column type mapping for `Id<Brand>`.\n *\n * Use this in your Kysely `Database` interface to type a column as `Id<Brand>` at\n * the TypeScript level. Pair it with `idColumn(codec)` for runtime read/write\n * transformation.\n *\n * @example\n * ```ts\n * import type { IdColumnType } from \"@smonn/ids/kysely\";\n * import type { Id } from \"@smonn/ids\";\n *\n * interface Database {\n * users: { id: IdColumnType<\"usr\"> };\n * }\n * ```\n */\nexport type IdColumnType<Brand extends string> = ColumnType<Id<Brand>, Id<Brand>, Id<Brand>>;\n\n/**\n * Kysely column adapter bound to a codec.\n *\n * Returns an object with `fromDriver` / `toDriver` helpers that mirror the read/write\n * contract of the Drizzle adapter — same error message, same strictness (safeParse on\n * read, identity on write).\n *\n * **Write path:** passes the `Id<Brand>` directly to the driver — it is already\n * the canonical string form.\n *\n * **Read path:** normalises the raw DB string via `codec.safeParse()`. Throws if\n * the value does not parse as a valid `Id<Brand>`.\n *\n * @example\n * ```ts\n * import { idColumn } from \"@smonn/ids/kysely\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const usrCol = idColumn(usr);\n *\n * // In a query result handler:\n * const id = usrCol.fromDriver(row.id);\n * ```\n */\nexport function idColumn<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): {\n toDriver(value: Id<Brand>): string;\n fromDriver(value: string): Id<Brand>;\n} {\n return {\n toDriver(value: Id<Brand>): string {\n return value;\n },\n fromDriver(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,SACd,OAIA;CACA,OAAO;EACL,SAAS,OAA0B;GACjC,OAAO;EACT;EACA,WAAW,OAA0B;GACnC,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF"}
@@ -1,6 +1,6 @@
1
- import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
- import { t as Id } from "./types-g7CiQDyE.mjs";
3
- import { n as IdColumnCodec } from "./adapter-types-CdYJM6Sf.mjs";
1
+ import { t as Id } from "./types-wplmOgOK.mjs";
2
+ import { n as IdColumnCodec } from "./adapter-types-CIc-4O-P.mjs";
3
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-Dqyho9vp.mjs";
4
4
  import { Type } from "@mikro-orm/core";
5
5
 
6
6
  //#region src/adapters/mikro-orm.d.ts
@@ -1 +1 @@
1
- {"version":3,"file":"mikro-orm.d.mts","names":[],"sources":["../src/adapters/mikro-orm.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuCqB;;;;;;;;;;iBAFL,MAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,cACV,IAAA,CAAK,EAAA,CAAG,KAAA"}
1
+ {"version":3,"file":"mikro-orm.d.mts","names":[],"sources":["../src/adapters/mikro-orm.ts"],"mappings":";;;;;;AAoCA;;;;;;;;;;;;;;;;;;AAEqB;;;;;;;;;AAFrB,iBAAgB,MAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,cACV,IAAA,CAAK,EAAA,CAAG,KAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"mikro-orm.mjs","names":[],"sources":["../src/adapters/mikro-orm.ts"],"sourcesContent":["import { Type } from \"@mikro-orm/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 * Factory that returns a MikroORM `Type` subclass bound to a codec.\n *\n * **Write path** (`convertToDatabaseValue`): passes the `Id<Brand>` through\n * unchanged — it is already the canonical string form.\n *\n * **Read path** (`convertToJSValue`): normalises the raw DB value via\n * `codec.safeParse()`. Throws `IdsError(\"invalid_id\")` if the stored value\n * does not parse as a valid `Id<Brand>`.\n *\n * **Column type** (`getColumnType`): returns `\"text\"`.\n *\n * @example\n * ```ts\n * import { PrimaryKey } from \"@mikro-orm/core\";\n * import { idType } from \"@smonn/ids/mikro-orm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * class User {\n * @PrimaryKey({ type: idType(usr) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idType<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): new () => Type<Id<Brand>, string> {\n return class extends Type<Id<Brand>, string> {\n override convertToDatabaseValue(value: Id<Brand>): string {\n return value;\n }\n override convertToJSValue(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n }\n override getColumnType(): string {\n return \"text\";\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,OACd,OACmC;CACnC,OAAO,cAAc,KAAwB;EAC3C,uBAAgC,OAA0B;GACxD,OAAO;EACT;EACA,iBAA0B,OAA0B;GAClD,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,gBAAiC;GAC/B,OAAO;EACT;CACF;AACF"}
1
+ {"version":3,"file":"mikro-orm.mjs","names":[],"sources":["../src/adapters/mikro-orm.ts"],"sourcesContent":["import { Type } from \"@mikro-orm/core\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * Factory that returns a MikroORM `Type` subclass bound to a codec.\n *\n * **Write path** (`convertToDatabaseValue`): passes the `Id<Brand>` through\n * unchanged — it is already the canonical string form.\n *\n * **Read path** (`convertToJSValue`): normalises the raw DB value via\n * `codec.safeParse()`. Throws `IdsError(\"invalid_id\")` if the stored value\n * does not parse as a valid `Id<Brand>`.\n *\n * **Column type** (`getColumnType`): returns `\"text\"`.\n *\n * @example\n * ```ts\n * import { PrimaryKey } from \"@mikro-orm/core\";\n * import { idType } from \"@smonn/ids/mikro-orm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * class User {\n * @PrimaryKey({ type: idType(usr) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idType<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): new () => Type<Id<Brand>, string> {\n return class extends Type<Id<Brand>, string> {\n override convertToDatabaseValue(value: Id<Brand>): string {\n return value;\n }\n override convertToJSValue(value: string): Id<Brand> {\n return readIdColumn(codec, value);\n }\n override getColumnType(): string {\n return \"text\";\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,OACd,OACmC;CACnC,OAAO,cAAc,KAAwB;EAC3C,uBAAgC,OAA0B;GACxD,OAAO;EACT;EACA,iBAA0B,OAA0B;GAClD,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,gBAAiC;GAC/B,OAAO;EACT;CACF;AACF"}
package/dist/nestjs.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { t as Id } from "./types-g7CiQDyE.mjs";
2
- import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CdYJM6Sf.mjs";
1
+ import { t as Id } from "./types-wplmOgOK.mjs";
2
+ import { r as IdParamFailure, t as IdCodec } from "./adapter-types-CIc-4O-P.mjs";
3
3
  import { ArgumentMetadata, PipeTransform } from "@nestjs/common";
4
4
 
5
5
  //#region src/adapters/nestjs.d.ts
package/dist/nestjs.mjs CHANGED
@@ -46,7 +46,7 @@ var ParseIdPipe = class {
46
46
  const result = this.codec.safeParse(value);
47
47
  if (!result.ok) {
48
48
  const failure = resolveIdParamFailure(result.error, this.options);
49
- if (this.options?.onError) this.options.onError(failure);
49
+ if (this.options?.onError) return this.options.onError(failure);
50
50
  if (failure.reason === "brand_mismatch" && failure.status === 404) throw new NotFoundException();
51
51
  if (failure.reason === "malformed" && failure.status === 400) throw new BadRequestException();
52
52
  throw new HttpException(failure.reason, failure.status);
@@ -1 +1 @@
1
- {"version":3,"file":"nestjs.mjs","names":[],"sources":["../src/adapters/nestjs.ts"],"sourcesContent":["import { BadRequestException, HttpException, Injectable, NotFoundException } from \"@nestjs/common\";\nimport type { ArgumentMetadata, PipeTransform } from \"@nestjs/common\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Options for `ParseIdPipe`. All fields are optional.\n *\n * **`onError` constraint:** NestJS `transform()` receives only `value` and `ArgumentMetadata`\n * — there is no HTTP context object. The `onError` hook must throw (or re-throw); it cannot\n * write a response inline the way Hono/Express hooks can.\n */\nexport type IdParamOptions = {\n /**\n * Called instead of throwing when provided. The hook **must** throw or re-throw — it cannot\n * return a response because `PipeTransform.transform` has no HTTP context.\n */\n onError?: (failure: IdParamFailure) => never;\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 * NestJS pipe that validates an untrusted route param against a codec via `safeParse`.\n *\n * Marked `@Injectable()` via `Injectable()(ParseIdPipe)` at module load time, making it\n * available for NestJS DI.\n *\n * **Default (no options):** throws `NotFoundException` (404) for brand mismatches and\n * `BadRequestException` (400) for malformed IDs.\n *\n * **`options.status`:** remaps the default HTTP status for a reason; when the resolved status\n * differs from the default, the pipe throws `HttpException(reason, status)`.\n *\n * **`options.onError`:** escape hatch for custom error handling. The hook must throw — it\n * cannot return a response because `PipeTransform.transform` has no HTTP context.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing ID → `reason: \"malformed\"`, default 400**\n *\n * @example\n * ```ts\n * import { ParseIdPipe } from \"@smonn/ids/nestjs\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Controller(\"users\")\n * class UsersController {\n * @Get(\":id\")\n * findOne(@Param(\"id\", new ParseIdPipe(usr)) id: Id<\"usr\">) {\n * return { id }; // Id<\"usr\">, canonical\n * }\n * }\n * ```\n */\nexport class ParseIdPipe<Brand extends string> implements PipeTransform<unknown, Id<Brand>> {\n private readonly codec: IdCodec<Brand>;\n private readonly options: IdParamOptions | undefined;\n\n constructor(codec: IdCodec<Brand>, options?: IdParamOptions) {\n this.codec = codec;\n this.options = options;\n }\n\n transform(value: unknown, _metadata: ArgumentMetadata): Id<Brand> {\n const result = this.codec.safeParse(value);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, this.options);\n if (this.options?.onError) {\n this.options.onError(failure);\n }\n if (failure.reason === \"brand_mismatch\" && failure.status === 404) {\n throw new NotFoundException();\n }\n if (failure.reason === \"malformed\" && failure.status === 400) {\n throw new BadRequestException();\n }\n throw new HttpException(failure.reason, failure.status);\n }\n return result.id;\n }\n}\n\n// Apply @Injectable() metadata so ParseIdPipe participates in NestJS DI when provided as a class.\n// Using a call instead of the @Injectable() decorator syntax to remain compatible with\n// TypeScript projects that do not enable experimentalDecorators.\nInjectable()(ParseIdPipe as unknown as new (...args: unknown[]) => unknown);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,IAAa,cAAb,MAA4F;CAC1F;CACA;CAEA,YAAY,OAAuB,SAA0B;EAC3D,KAAK,QAAQ;EACb,KAAK,UAAU;CACjB;CAEA,UAAU,OAAgB,WAAwC;EAChE,MAAM,SAAS,KAAK,MAAM,UAAU,KAAK;EACzC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,KAAK,OAAO;GAChE,IAAI,KAAK,SAAS,SAChB,KAAK,QAAQ,QAAQ,OAAO;GAE9B,IAAI,QAAQ,WAAW,oBAAoB,QAAQ,WAAW,KAC5D,MAAM,IAAI,kBAAkB;GAE9B,IAAI,QAAQ,WAAW,eAAe,QAAQ,WAAW,KACvD,MAAM,IAAI,oBAAoB;GAEhC,MAAM,IAAI,cAAc,QAAQ,QAAQ,QAAQ,MAAM;EACxD;EACA,OAAO,OAAO;CAChB;AACF;AAKA,WAAW,CAAC,CAAC,WAA6D"}
1
+ {"version":3,"file":"nestjs.mjs","names":[],"sources":["../src/adapters/nestjs.ts"],"sourcesContent":["import { BadRequestException, HttpException, Injectable, NotFoundException } from \"@nestjs/common\";\nimport type { ArgumentMetadata, PipeTransform } from \"@nestjs/common\";\nimport { type IdCodec, type IdParamFailure, resolveIdParamFailure } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\nexport type { IdParamFailure };\n\n/**\n * Options for `ParseIdPipe`. All fields are optional.\n *\n * **`onError` constraint:** NestJS `transform()` receives only `value` and `ArgumentMetadata`\n * — there is no HTTP context object. The `onError` hook must throw (or re-throw); it cannot\n * write a response inline the way Hono/Express hooks can.\n */\nexport type IdParamOptions = {\n /**\n * Called instead of throwing when provided. The hook **must** throw or re-throw — it cannot\n * return a response because `PipeTransform.transform` has no HTTP context.\n */\n onError?: (failure: IdParamFailure) => never;\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 * NestJS pipe that validates an untrusted route param against a codec via `safeParse`.\n *\n * Marked `@Injectable()` via `Injectable()(ParseIdPipe)` at module load time, making it\n * available for NestJS DI.\n *\n * **Default (no options):** throws `NotFoundException` (404) for brand mismatches and\n * `BadRequestException` (400) for malformed IDs.\n *\n * **`options.status`:** remaps the default HTTP status for a reason; when the resolved status\n * differs from the default, the pipe throws `HttpException(reason, status)`.\n *\n * **`options.onError`:** escape hatch for custom error handling. The hook must throw — it\n * cannot return a response because `PipeTransform.transform` has no HTTP context.\n *\n * - **Brand mismatch (`invalid_prefix`) → `reason: \"brand_mismatch\"`, default 404**\n * - **Malformed or missing ID → `reason: \"malformed\"`, default 400**\n *\n * @example\n * ```ts\n * import { ParseIdPipe } from \"@smonn/ids/nestjs\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Controller(\"users\")\n * class UsersController {\n * @Get(\":id\")\n * findOne(@Param(\"id\", new ParseIdPipe(usr)) id: Id<\"usr\">) {\n * return { id }; // Id<\"usr\">, canonical\n * }\n * }\n * ```\n */\nexport class ParseIdPipe<Brand extends string> implements PipeTransform<unknown, Id<Brand>> {\n private readonly codec: IdCodec<Brand>;\n private readonly options: IdParamOptions | undefined;\n\n constructor(codec: IdCodec<Brand>, options?: IdParamOptions) {\n this.codec = codec;\n this.options = options;\n }\n\n transform(value: unknown, _metadata: ArgumentMetadata): Id<Brand> {\n const result = this.codec.safeParse(value);\n if (!result.ok) {\n const failure = resolveIdParamFailure(result.error, this.options);\n if (this.options?.onError) {\n return this.options.onError(failure);\n }\n if (failure.reason === \"brand_mismatch\" && failure.status === 404) {\n throw new NotFoundException();\n }\n if (failure.reason === \"malformed\" && failure.status === 400) {\n throw new BadRequestException();\n }\n throw new HttpException(failure.reason, failure.status);\n }\n return result.id;\n }\n}\n\n// Apply @Injectable() metadata so ParseIdPipe participates in NestJS DI when provided as a class.\n// Using a call instead of the @Injectable() decorator syntax to remain compatible with\n// TypeScript projects that do not enable experimentalDecorators.\nInjectable()(ParseIdPipe as unknown as new (...args: unknown[]) => unknown);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,IAAa,cAAb,MAA4F;CAC1F;CACA;CAEA,YAAY,OAAuB,SAA0B;EAC3D,KAAK,QAAQ;EACb,KAAK,UAAU;CACjB;CAEA,UAAU,OAAgB,WAAwC;EAChE,MAAM,SAAS,KAAK,MAAM,UAAU,KAAK;EACzC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,UAAU,sBAAsB,OAAO,OAAO,KAAK,OAAO;GAChE,IAAI,KAAK,SAAS,SAChB,OAAO,KAAK,QAAQ,QAAQ,OAAO;GAErC,IAAI,QAAQ,WAAW,oBAAoB,QAAQ,WAAW,KAC5D,MAAM,IAAI,kBAAkB;GAE9B,IAAI,QAAQ,WAAW,eAAe,QAAQ,WAAW,KACvD,MAAM,IAAI,oBAAoB;GAEhC,MAAM,IAAI,cAAc,QAAQ,QAAQ,QAAQ,MAAM;EACxD;EACA,OAAO,OAAO;CAChB;AACF;AAKA,WAAW,CAAC,CAAC,WAA6D"}
@@ -1,36 +1,13 @@
1
1
  import { a as toWireId, i as payloadBytesFromId, n as registerBrand, r as payloadBase32Length, s as validateBrand, t as wireMethods } from "./codec-shell-DvrTDa65.mjs";
2
2
  import { a as writeTimestamp, r as readTimestampMs, t as defaultRng } from "./rng-Clos6uC0.mjs";
3
- import { i as encodeKeyMaterial, r as decodeKeyMaterial, t as assertValidKeyMaterialByteLength } from "./key-material-f29JIyrz.mjs";
3
+ import { a as decryptPayload, i as encodeKeyMaterial, r as decodeKeyMaterial, s as encryptPayload, t as assertValidKeyMaterialByteLength } from "./key-material-DsukgnR5.mjs";
4
4
  //#region src/codecs/opaque/layout.ts
5
- const zeroIv = /* @__PURE__ */ new Uint8Array(16);
6
- const pkcsPad = 16;
7
5
  function buildPlaintext(ms, rng) {
8
6
  const plaintext = /* @__PURE__ */ new Uint8Array(16);
9
7
  writeTimestamp(ms, plaintext);
10
8
  rng(plaintext.subarray(6, 16));
11
9
  return plaintext;
12
10
  }
13
- async function encryptPayload(key, plaintext) {
14
- return new Uint8Array(await crypto.subtle.encrypt({
15
- name: "AES-CBC",
16
- iv: zeroIv
17
- }, key, plaintext)).subarray(0, 16);
18
- }
19
- async function decryptPayload(key, c1) {
20
- const c2Input = /* @__PURE__ */ new Uint8Array(16);
21
- for (let i = 0; i < 16; i++) c2Input[i] = pkcsPad ^ c1[i];
22
- const c2Encrypted = new Uint8Array(await crypto.subtle.encrypt({
23
- name: "AES-CBC",
24
- iv: zeroIv
25
- }, key, c2Input));
26
- const ciphertext = /* @__PURE__ */ new Uint8Array(32);
27
- ciphertext.set(c1, 0);
28
- ciphertext.set(c2Encrypted.subarray(0, 16), 16);
29
- return new Uint8Array(await crypto.subtle.decrypt({
30
- name: "AES-CBC",
31
- iv: zeroIv
32
- }, key, ciphertext));
33
- }
34
11
  async function extractTimestampFromId(prefix, key, id) {
35
12
  const plaintext = await decryptPayload(key, payloadBytesFromId(prefix, id));
36
13
  return new Date(readTimestampMs(plaintext));
@@ -49,7 +26,7 @@ function createOpaqueLayoutOps(prefix, key, rng) {
49
26
  return {
50
27
  generateAt: (ms) => generateWireId(prefix, key, rng, ms),
51
28
  extractTimestamp: (id) => extractTimestampFromId(prefix, key, id),
52
- exampleWireId: () => schemaExample(prefix)
29
+ exampleWireId: (_ms) => schemaExample(prefix)
53
30
  };
54
31
  }
55
32
  //#endregion
@@ -127,4 +104,4 @@ function createOpaqueTimestampId(brand, opts) {
127
104
  //#endregion
128
105
  export { importOpaqueKey as i, decodeOpaqueKey as n, encodeOpaqueKey as r, createOpaqueTimestampId as t };
129
106
 
130
- //# sourceMappingURL=opaque-BQVNoIIh.mjs.map
107
+ //# sourceMappingURL=opaque-D7y5cgzT.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opaque-D7y5cgzT.mjs","names":[],"sources":["../src/codecs/opaque/layout.ts","../src/codecs/opaque/key.ts","../src/codecs/opaque/index.ts"],"sourcesContent":["import type { webcrypto } from \"node:crypto\";\nimport type { Id, LayoutOps, Prefix } from \"../../types.js\";\nimport { decryptPayload, encryptPayload } from \"../_kernel/crypto.js\";\nimport { payloadBytesFromId, toWireId } from \"../../wire/envelope.js\";\nimport { payloadBase32Length, payloadByteLength } from \"../../wire/invariants.js\";\nimport {\n readTimestampMs,\n timestampByteLength,\n writeTimestamp,\n} from \"../../wire/timestamp-bytes.js\";\n\nfunction buildPlaintext(ms: number, rng: (target: Uint8Array) => void): Uint8Array {\n const plaintext = new Uint8Array(payloadByteLength);\n writeTimestamp(ms, plaintext);\n rng(plaintext.subarray(timestampByteLength, payloadByteLength));\n return plaintext;\n}\n\nasync function extractTimestampFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n key: webcrypto.CryptoKey,\n id: Id<Brand>,\n): Promise<Date> {\n const plaintext = await decryptPayload(key, payloadBytesFromId(prefix, id));\n return new Date(readTimestampMs(plaintext));\n}\n\n/** Produces a canonical encrypted wire ID. Per-call plaintext/ciphertext buffers —\n * subtle dominates this path; reuse would be safe but not worth pinning to spec detail. */\nasync function generateWireId<Brand extends string>(\n prefix: Prefix<Brand>,\n key: webcrypto.CryptoKey,\n rng: (target: Uint8Array) => void,\n ms: number,\n): Promise<Id<Brand>> {\n const plaintext = buildPlaintext(ms, rng);\n const encrypted = await encryptPayload(key, plaintext);\n return toWireId(prefix, encrypted);\n}\n\n/** Structural placeholder for JSON Schema (encrypt is async). */\nfunction schemaExample<Brand extends string>(prefix: Prefix<Brand>): string {\n return prefix + \"0\".repeat(payloadBase32Length);\n}\n\n/** Layout ops binder for the Opaque Timestamp variant. `extractTimestampFromId` is module-private; the binder exposes `extractTimestamp` for the codec constructor. */\nexport function createOpaqueLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n key: webcrypto.CryptoKey,\n rng: (target: Uint8Array) => void,\n): LayoutOps<Brand> & {\n generateAt(ms: number): Promise<Id<Brand>>;\n extractTimestamp(id: Id<Brand>): Promise<Date>;\n} {\n return {\n generateAt: (ms: number): Promise<Id<Brand>> => generateWireId(prefix, key, rng, ms),\n extractTimestamp: (id: Id<Brand>): Promise<Date> => extractTimestampFromId(prefix, key, id),\n exampleWireId: (_ms?: number): Id<Brand> => schemaExample(prefix) 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 opaque AES key material (not Crockford base32). */\nexport type OpaqueKeyFormat = \"hex\" | \"base64url\";\n\ndeclare const opaqueKeyBrand: unique symbol;\n\n/**\n * Opaque imported handle for one AES key used by the Opaque Timestamp codec.\n *\n * Holds the underlying `webcrypto.CryptoKey` internally; callers never access it directly.\n * Obtain handles via {@link importOpaqueKey} and pass them to\n * `createOpaqueTimestampId` as the `key` option.\n *\n * Distinct from the `WrappingKey` used by `@smonn/ids/wrapped` — one raw\n * secret must not silently serve both codecs without an explicit import.\n */\nexport type OpaqueKey = {\n readonly [opaqueKeyBrand]: \"OpaqueKey\";\n};\n\nconst opaqueKeyInternals = new WeakMap<OpaqueKey, webcrypto.CryptoKey>();\n\n/**\n * Imports raw AES key bytes into an {@link OpaqueKey} handle for the Opaque\n * Timestamp codec.\n *\n * Accepts 16, 24, or 32 bytes (AES-128 / AES-192 / AES-256 strength).\n * To store or transport key material, use {@link encodeOpaqueKey} /\n * {@link decodeOpaqueKey} (`\"hex\"` or `\"base64url\"` — not Crockford base32).\n *\n * @param bytes - 16, 24, or 32 raw key bytes.\n */\nexport async function importOpaqueKey(bytes: Uint8Array): Promise<OpaqueKey> {\n assertValidKeyMaterialByteLength(bytes.length, \"AES\");\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n bytes as Uint8Array<ArrayBuffer>,\n \"AES-CBC\",\n false,\n [\"encrypt\", \"decrypt\"],\n );\n const key = Object.freeze({}) as OpaqueKey;\n opaqueKeyInternals.set(key, cryptoKey);\n return key;\n}\n\nexport function getOpaqueKeyCryptoKey(key: OpaqueKey): webcrypto.CryptoKey {\n const cryptoKey = opaqueKeyInternals.get(key);\n if (cryptoKey === undefined) {\n throw new Error(\"invalid opaque key\");\n }\n return cryptoKey;\n}\n\n/**\n * Encodes raw AES key bytes for storage in env vars or secret managers.\n *\n * @param bytes - 16, 24, or 32 raw key bytes (AES-128/192/256).\n * @param format - `hex` (lowercase) or `base64url`.\n */\nexport function encodeOpaqueKey(bytes: Uint8Array, format: OpaqueKeyFormat): string {\n return encodeKeyMaterial(bytes, format, \"opaque\", \"AES\");\n}\n\n/**\n * Decodes key material emitted by `encodeOpaqueKey` (or `ids keygen`) back to raw bytes.\n *\n * @param encoded - Hex or base64url string.\n * @param format - Must match how the string was encoded.\n */\nexport function decodeOpaqueKey(encoded: string, format: OpaqueKeyFormat): Uint8Array {\n return decodeKeyMaterial(encoded, format, \"opaque\", \"AES\");\n}\n","import { validateBrand } from \"../_kernel/brand.js\";\nimport { createOpaqueLayoutOps } from \"./layout.js\";\nimport { getOpaqueKeyCryptoKey, type OpaqueKey } from \"./key.js\";\nimport { registerBrand } from \"../_kernel/registry.js\";\nimport { defaultRng } from \"../_kernel/rng.js\";\nimport type {\n Id,\n JsonSchema,\n ParseResult,\n Prefix,\n StandardSchemaProps,\n ValidBrand,\n} from \"../../types.js\";\nimport { wireMethods } from \"../../wire/codec-shell.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../../error.js\";\nexport {\n decodeOpaqueKey,\n encodeOpaqueKey,\n importOpaqueKey,\n type OpaqueKey,\n type OpaqueKeyFormat,\n} from \"./key.js\";\n\n/**\n * Configuration options for an Opaque Timestamp codec instance.\n */\nexport type OpaqueTimestampOptions = {\n /**\n * {@link OpaqueKey} handle for AES-CBC encryption and decryption.\n * Obtain via {@link importOpaqueKey}.\n *\n * A single key, not a ring: rotation is forward-only and caller-tracked —\n * hold one codec per key epoch and select it from your own records. The\n * library cannot trial keys (the payload is unauthenticated). See ADR-0013.\n */\n key: OpaqueKey;\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * A brand-scoped codec for generating and validating Opaque Timestamp IDs.\n *\n * Same wire shape as the Timestamp codec (`{brand}_` + 26 base32 chars) but the\n * payload is AES-CBC encrypted. `generate`, `generateAt`, and `extractTimestamp`\n * are async; parsing methods are sync. No `minIdForTime` / `maxIdForTime` —\n * encrypted payloads do not sort by creation time.\n *\n * @remarks\n * **Security properties (unauthenticated, deterministic, and malleable by design):**\n *\n * - The payload is AES-CBC encrypted but **unauthenticated** — there is no\n * integrity tag. A tampered or wrong-key payload decrypts to garbage bytes\n * without throwing.\n * - Opaque IDs must be treated as **opaque handles**, not as trusted or\n * authenticated tokens.\n * - `extractTimestamp` is best-effort on untrusted input: a wrong or tampered\n * key returns a plausible-looking `Date` without error, not a verification\n * failure. Do not treat the returned timestamp as proof of origin.\n */\nexport type OpaqueTimestampCodec<Brand extends string> = {\n /** Produces a new canonical encrypted ID using the codec's `now` and `rng`. */\n generate(): Promise<Id<Brand>>;\n /** Produces a new canonical encrypted ID with timestamp bytes from `date`. Throws on invalid dates. */\n generateAt(date: Date): Promise<Id<Brand>>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decrypts and decodes the creation `Date` from an `Id<Brand>`. Trusts the type — use `safeParse()` at boundaries first. See ADR-0002.\n *\n * Requires the same key used at generation; a wrong key returns a plausible\n * but wrong `Date`, never an error. With rotation, select the codec for the\n * ID's key epoch from your own records — the library cannot. See ADR-0013.\n */\n extractTimestamp(id: Id<Brand>): Promise<Date>;\n /**\n * JSON Schema for the canonical wire form. The `pattern` matches the canonical stored\n * form only and is deliberately stricter than `parse()`/`safeParse()`, which accept\n * uppercase letters and Crockford aliases (`o`/`i`/`l`) before normalising. See ADR-0003.\n * The `example` is a structural placeholder (generated at construction time).\n */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\n/**\n * Creates an Opaque Timestamp codec for `brand` (three lowercase a–z characters).\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Required `key` (an {@link OpaqueKey} from {@link importOpaqueKey}) plus\n * optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createOpaqueTimestampId<Brand extends string>(\n brand: Brand & ValidBrand<Brand>,\n opts: OpaqueTimestampOptions,\n): OpaqueTimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const cryptoKey = getOpaqueKeyCryptoKey(opts.key);\n const now = opts.now ?? Date.now;\n const rng = opts.rng ?? defaultRng;\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createOpaqueLayoutOps(prefix, cryptoKey, rng);\n\n return {\n generate: () => layout.generateAt(now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId()),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":";;;;AAWA,SAAS,eAAe,IAAY,KAA+C;CACjF,MAAM,4BAAY,IAAI,WAAA,EAA4B;CAClD,eAAe,IAAI,SAAS;CAC5B,IAAI,UAAU,SAAA,GAAA,EAA+C,CAAC;CAC9D,OAAO;AACT;AAEA,eAAe,uBACb,QACA,KACA,IACe;CACf,MAAM,YAAY,MAAM,eAAe,KAAK,mBAAmB,QAAQ,EAAE,CAAC;CAC1E,OAAO,IAAI,KAAK,gBAAgB,SAAS,CAAC;AAC5C;;;AAIA,eAAe,eACb,QACA,KACA,KACA,IACoB;CAGpB,OAAO,SAAS,QAAQ,MADA,eAAe,KADrB,eAAe,IAAI,GACe,CAAC,CACpB;AACnC;;AAGA,SAAS,cAAoC,QAA+B;CAC1E,OAAO,SAAS,IAAI,OAAO,mBAAmB;AAChD;;AAGA,SAAgB,sBACd,QACA,KACA,KAIA;CACA,OAAO;EACL,aAAa,OAAmC,eAAe,QAAQ,KAAK,KAAK,EAAE;EACnF,mBAAmB,OAAiC,uBAAuB,QAAQ,KAAK,EAAE;EAC1F,gBAAgB,QAA4B,cAAc,MAAM;CAClE;AACF;;;ACjCA,MAAM,qCAAqB,IAAI,QAAwC;;;;;;;;;;;AAYvE,eAAsB,gBAAgB,OAAuC;CAC3E,iCAAiC,MAAM,QAAQ,KAAK;CACpD,MAAM,YAAY,MAAM,OAAO,OAAO,UACpC,OACA,OACA,WACA,OACA,CAAC,WAAW,SAAS,CACvB;CACA,MAAM,MAAM,OAAO,OAAO,CAAC,CAAC;CAC5B,mBAAmB,IAAI,KAAK,SAAS;CACrC,OAAO;AACT;AAEA,SAAgB,sBAAsB,KAAqC;CACzE,MAAM,YAAY,mBAAmB,IAAI,GAAG;CAC5C,IAAI,cAAc,KAAA,GAChB,MAAM,IAAI,MAAM,oBAAoB;CAEtC,OAAO;AACT;;;;;;;AAQA,SAAgB,gBAAgB,OAAmB,QAAiC;CAClF,OAAO,kBAAkB,OAAO,QAAQ,UAAU,KAAK;AACzD;;;;;;;AAQA,SAAgB,gBAAgB,SAAiB,QAAqC;CACpF,OAAO,kBAAkB,SAAS,QAAQ,UAAU,KAAK;AAC3D;;;;;;;;;;ACgCA,SAAgB,wBACd,OACA,MAC6B;CAC7B,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAE7C,MAAM,YAAY,sBAAsB,KAAK,GAAG;CAChD,MAAM,MAAM,KAAK,OAAO,KAAK;CAC7B,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,SAAwB,GAAG,MAAM;CACvC,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,SAAS,sBAAsB,QAAQ,WAAW,GAAG;CAE3D,OAAO;EACL,gBAAgB,OAAO,WAAW,IAAI,CAAC;EACvC,aAAa,SAAe,OAAO,WAAW,KAAK,QAAQ,CAAC;EAC5D,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,WAAW,KAAK;EAChB,kBAAkB,OAAO;EACzB,oBAAoB,KAAK,aAAa,OAAO,OAAO,cAAc,CAAC;EACnE,aAAa,KAAK;CACpB;AACF"}
package/dist/opaque.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
- import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, t as Id } from "./types-g7CiQDyE.mjs";
1
+ import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, o as ValidBrand, t as Id } from "./types-wplmOgOK.mjs";
2
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-Dqyho9vp.mjs";
3
3
 
4
4
  //#region src/codecs/opaque/key.d.ts
5
5
  /** Wire encoding for opaque AES key material (not Crockford base32). */
@@ -69,6 +69,18 @@ type OpaqueTimestampOptions = {
69
69
  * payload is AES-CBC encrypted. `generate`, `generateAt`, and `extractTimestamp`
70
70
  * are async; parsing methods are sync. No `minIdForTime` / `maxIdForTime` —
71
71
  * encrypted payloads do not sort by creation time.
72
+ *
73
+ * @remarks
74
+ * **Security properties (unauthenticated, deterministic, and malleable by design):**
75
+ *
76
+ * - The payload is AES-CBC encrypted but **unauthenticated** — there is no
77
+ * integrity tag. A tampered or wrong-key payload decrypts to garbage bytes
78
+ * without throwing.
79
+ * - Opaque IDs must be treated as **opaque handles**, not as trusted or
80
+ * authenticated tokens.
81
+ * - `extractTimestamp` is best-effort on untrusted input: a wrong or tampered
82
+ * key returns a plausible-looking `Date` without error, not a verification
83
+ * failure. Do not treat the returned timestamp as proof of origin.
72
84
  */
73
85
  type OpaqueTimestampCodec<Brand extends string> = {
74
86
  /** Produces a new canonical encrypted ID using the codec's `now` and `rng`. */generate(): Promise<Id<Brand>>; /** Produces a new canonical encrypted ID with timestamp bytes from `date`. Throws on invalid dates. */
@@ -93,7 +105,13 @@ type OpaqueTimestampCodec<Brand extends string> = {
93
105
  * but wrong `Date`, never an error. With rotation, select the codec for the
94
106
  * ID's key epoch from your own records — the library cannot. See ADR-0013.
95
107
  */
96
- extractTimestamp(id: Id<Brand>): Promise<Date>; /** JSON Schema for the canonical wire form (`example` is a structural placeholder). */
108
+ extractTimestamp(id: Id<Brand>): Promise<Date>;
109
+ /**
110
+ * JSON Schema for the canonical wire form. The `pattern` matches the canonical stored
111
+ * form only and is deliberately stricter than `parse()`/`safeParse()`, which accept
112
+ * uppercase letters and Crockford aliases (`o`/`i`/`l`) before normalising. See ADR-0003.
113
+ * The `example` is a structural placeholder (generated at construction time).
114
+ */
97
115
  toJsonSchema(): JsonSchema; /** Standard Schema validate entry point. */
98
116
  readonly "~standard": StandardSchemaProps<Brand>;
99
117
  };
@@ -104,7 +122,7 @@ type OpaqueTimestampCodec<Brand extends string> = {
104
122
  * @param opts - Required `key` (an {@link OpaqueKey} from {@link importOpaqueKey}) plus
105
123
  * optional `now`, `rng`, and `allowDuplicateBrand` overrides.
106
124
  */
107
- declare function createOpaqueTimestampId<Brand extends string>(brand: Brand, opts: OpaqueTimestampOptions): OpaqueTimestampCodec<Brand>;
125
+ declare function createOpaqueTimestampId<Brand extends string>(brand: Brand & ValidBrand<Brand>, opts: OpaqueTimestampOptions): OpaqueTimestampCodec<Brand>;
108
126
  //#endregion
109
127
  export { IdsError, type IdsErrorCode, type OpaqueKey, type OpaqueKeyFormat, OpaqueTimestampCodec, OpaqueTimestampOptions, createOpaqueTimestampId, decodeOpaqueKey, encodeOpaqueKey, importOpaqueKey, isIdsError };
110
128
  //# sourceMappingURL=opaque.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/codecs/opaque/key.ts","../src/codecs/opaque/index.ts"],"mappings":";;;;;KAQY,eAAA;AAAA,cAEE,cAAA;AAFd;;;;AAAY;AAA0B;;;;AAExB;AAFd,KAcY,SAAA;EAAA,UACA,cAAA;AAAA;;AAAA;AAeZ;;;;;;;;iBAAsB,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;;;;AAAA;AA4BlE;;iBAAgB,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;;;;iBAU3C,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;;;KCvD/D,sBAAA;EDb0B;;;;AAExB;AAYd;;;ECQE,GAAA,EAAK,SAAA,EDPK;ECSV,GAAA,iBDMoB;ECJpB,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;;;;;;;;ADEgE;KCStD,oBAAA;EDmBI,+ECjBd,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA,IDiBkC;ECfzD,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;;;;;EAKnC,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;EDoBlC;;;EChBE,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;EAI1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;EDYkC;AAAA;;;;ACvD3E;;EAmDE,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA,CAAQ,IAAA,GAtC1B;EAwCf,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;AAxC1C;AAWF;;iBAuCgB,uBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
1
+ {"version":3,"file":"opaque.d.mts","names":[],"sources":["../src/codecs/opaque/key.ts","../src/codecs/opaque/index.ts"],"mappings":";;;;;KAQY,eAAA;AAAA,cAEE,cAAA;AAFd;;;;AAAY;AAA0B;;;;AAExB;AAFd,KAcY,SAAA;EAAA,UACA,cAAA;AAAA;;AAAA;AAeZ;;;;;;;;iBAAsB,eAAA,CAAgB,KAAA,EAAO,UAAA,GAAa,OAAA,CAAQ,SAAA;;;;AAAA;AA4BlE;;iBAAgB,eAAA,CAAgB,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,eAAA;;;;;;;iBAU3C,eAAA,CAAgB,OAAA,UAAiB,MAAA,EAAQ,eAAA,GAAkB,UAAA;;;;;;KChD/D,sBAAA;EDpB0B;;;;AAExB;AAYd;;;ECeE,GAAA,EAAK,SAAA,EDdK;ECgBV,GAAA,iBDDoB;ECGpB,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;;;;;;;;ADLgE;AA4BlE;;;;;;;;;AAA2D;AAU3D;;KCVY,oBAAA;EDU+D,+ECRzE,QAAA,IAAY,OAAA,CAAQ,EAAA,CAAG,KAAA;EAEvB,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,EAAA,CAAG,KAAA;;;ADMsC;;ECDzE,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;AA/ClC;;EAmDE,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;EAtCX;;;EA0Cf,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;;;AAxCvC;AAuBF;EAyBE,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,OAAA,CAAQ,IAAA;;;;;;;EAOzC,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;;iBAU5B,uBAAA,uBACd,KAAA,EAAO,KAAA,GAAQ,UAAA,CAAW,KAAA,GAC1B,IAAA,EAAM,sBAAA,GACL,oBAAA,CAAqB,KAAA"}
package/dist/opaque.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-BQVNoIIh.mjs";
2
+ import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-D7y5cgzT.mjs";
3
3
  export { IdsError, createOpaqueTimestampId, decodeOpaqueKey, encodeOpaqueKey, importOpaqueKey, isIdsError };
package/dist/prisma.d.mts CHANGED
@@ -1,24 +1,17 @@
1
- import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-JIPylU_E.mjs";
2
- import { t as Id } from "./types-g7CiQDyE.mjs";
3
- import { n as IdColumnCodec } from "./adapter-types-CdYJM6Sf.mjs";
1
+ import { t as Id } from "./types-wplmOgOK.mjs";
2
+ import { n as IdColumnCodec } from "./adapter-types-CIc-4O-P.mjs";
3
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-Dqyho9vp.mjs";
4
4
 
5
5
  //#region src/adapters/prisma.d.ts
6
6
  /**
7
- * Read/write transform pair for integrating `Id<Brand>` with Prisma extensions.
8
- *
9
- * **Prisma casting caveat:** Prisma cannot fully brand a generated model field
10
- * type at the schema level. The `read` function asserts `Id<Brand>` at the
11
- * TypeScript level, but Prisma's generated types for the model field will not
12
- * reflect this branding. Callers consuming the validated value from a Prisma
13
- * result component may need an explicit `as Id<Brand>` cast at the call site.
7
+ * Read/write transform pair and `$extends` result-component factory for
8
+ * integrating `Id<Brand>` with Prisma extensions.
14
9
  */
15
10
  type IdTransform<Brand extends string> = {
16
11
  /**
17
12
  * Read transform: validates the raw database value via `safeParse` and returns
18
13
  * `Id<Brand>`. Throws if the value is missing, malformed, or belongs to a
19
14
  * different brand.
20
- *
21
- * Use in a Prisma `$extends` result component's `compute` function.
22
15
  */
23
16
  read(value: unknown): Id<Brand>;
24
17
  /**
@@ -29,18 +22,37 @@ type IdTransform<Brand extends string> = {
29
22
  * Use in a Prisma `$extends` query component or explicit `data` mapping.
30
23
  */
31
24
  write(value: Id<Brand>): string;
25
+ /**
26
+ * Creates a typed `$extends` result-component field definition that carries
27
+ * `Id<Brand>` through Prisma's type machinery without a per-call-site cast.
28
+ *
29
+ * @param fieldName - The model field to read from (e.g. `"id"`).
30
+ * @returns A `{ needs, compute }` object whose `compute` return type is
31
+ * statically `Id<Brand>`, so the extended-client model field is typed correctly.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const xprisma = prisma.$extends({
36
+ * result: {
37
+ * user: { id: userIdField.computeField("id") },
38
+ * },
39
+ * });
40
+ * // xprisma.user.findUnique(…).id is typed as Id<"usr"> — no cast required
41
+ * ```
42
+ */
43
+ computeField(fieldName: string): {
44
+ needs: Record<string, boolean>;
45
+ compute: (model: Record<string, unknown>) => Id<Brand>;
46
+ };
32
47
  };
33
48
  /**
34
49
  * Creates a read/write transform pair for use with Prisma's `$extends` extension model.
35
50
  *
36
51
  * Works with any codec variant exposing `safeParse`.
37
52
  *
38
- * **Prisma casting caveat:** Prisma's `$extends` result component can add
39
- * typed computed accessors to model instances, but cannot retroactively
40
- * re-type an existing schema field at the Prisma Client level. The `read`
41
- * function asserts `Id<Brand>`, but callers will need an explicit
42
- * `as Id<Brand>` cast at consumption sites where Prisma's generated types
43
- * are expected.
53
+ * Use `computeField(fieldName)` to produce a typed `$extends` result-component
54
+ * field definition the brand is carried through Prisma's type machinery
55
+ * automatically and no per-call-site cast is required.
44
56
  *
45
57
  * @example
46
58
  * ```ts
@@ -52,17 +64,10 @@ type IdTransform<Brand extends string> = {
52
64
  *
53
65
  * const xprisma = prisma.$extends({
54
66
  * result: {
55
- * user: {
56
- * id: {
57
- * needs: { id: true },
58
- * compute(user) {
59
- * // Cast required: Prisma cannot brand the generated type at schema level
60
- * return userIdField.read(user.id) as Id<"usr">;
61
- * },
62
- * },
63
- * },
67
+ * user: { id: userIdField.computeField("id") },
64
68
  * },
65
69
  * });
70
+ * // xprisma.user.findUnique(…).id is typed as Id<"usr"> — no cast required
66
71
  * ```
67
72
  */
68
73
  declare function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand>;