@smonn/ids 0.14.0 → 0.15.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.
@@ -1,5 +1,6 @@
1
1
  import { t as Id } from "./types-hGBnCpJj.mjs";
2
2
  import { t as IdCodec } from "./adapter-types-Bia_w9sg.mjs";
3
+ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-CifcKKOG.mjs";
3
4
  import { GraphQLScalarType } from "graphql";
4
5
 
5
6
  //#region src/adapters/graphql.d.ts
@@ -27,5 +28,5 @@ declare function idScalar<Brand extends string>(codec: IdCodec<Brand>, config: {
27
28
  description?: string;
28
29
  }): GraphQLScalarType<Id<Brand>, string>;
29
30
  //#endregion
30
- export { idScalar };
31
+ export { IdsError, type IdsErrorCode, idScalar, isIdsError };
31
32
  //# sourceMappingURL=graphql.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graphql.d.mts","names":[],"sources":["../src/adapters/graphql.ts"],"mappings":";;;;;;;AAwBA;;;;;;;;;;;;;;;;;iBAAgB,QAAA,uBACd,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,MAAA;EAAU,IAAA;EAAc,WAAA;AAAA,IACvB,iBAAA,CAAkB,EAAA,CAAG,KAAA"}
1
+ {"version":3,"file":"graphql.d.mts","names":[],"sources":["../src/adapters/graphql.ts"],"mappings":";;;;;;;AA2BA;;;;;;;;;;;;;;;;;;iBAAgB,QAAA,uBACd,KAAA,EAAO,OAAA,CAAQ,KAAA,GACf,MAAA;EAAU,IAAA;EAAc,WAAA;AAAA,IACvB,iBAAA,CAAkB,EAAA,CAAG,KAAA"}
package/dist/graphql.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
1
2
  import { GraphQLError, GraphQLScalarType, Kind } from "graphql";
2
3
  //#region src/adapters/graphql.ts
3
4
  /**
@@ -37,6 +38,6 @@ function idScalar(codec, config) {
37
38
  });
38
39
  }
39
40
  //#endregion
40
- export { idScalar };
41
+ export { IdsError, idScalar, isIdsError };
41
42
 
42
43
  //# sourceMappingURL=graphql.mjs.map
@@ -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` — 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"}
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/** {@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 * 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":";;;;;;;;;;;;;;;;;;;;;;AA2BA,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/prisma.d.mts CHANGED
@@ -4,6 +4,16 @@ import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-CifcK
4
4
 
5
5
  //#region src/adapters/prisma.d.ts
6
6
  /**
7
+ * Typed `$extends` result-component field definition produced by
8
+ * {@link IdTransform.computeField} — a `{ needs, compute }` pair whose `compute`
9
+ * return type is statically `Id<Brand>`, so the extended-client model field is
10
+ * typed correctly without a per-call-site cast.
11
+ */
12
+ type IdComputeField<Brand extends string> = {
13
+ needs: Record<string, boolean>;
14
+ compute: (model: Record<string, unknown>) => Id<Brand>;
15
+ };
16
+ /**
7
17
  * Read/write transform pair and `$extends` result-component factory for
8
18
  * integrating `Id<Brand>` with Prisma extensions.
9
19
  */
@@ -27,8 +37,8 @@ type IdTransform<Brand extends string> = {
27
37
  * `Id<Brand>` through Prisma's type machinery without a per-call-site cast.
28
38
  *
29
39
  * @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.
40
+ * @returns An {@link IdComputeField} whose `compute` return type is statically
41
+ * `Id<Brand>`, so the extended-client model field is typed correctly.
32
42
  *
33
43
  * @example
34
44
  * ```ts
@@ -40,10 +50,7 @@ type IdTransform<Brand extends string> = {
40
50
  * // xprisma.user.findUnique(…).id is typed as Id<"usr"> — no cast required
41
51
  * ```
42
52
  */
43
- computeField(fieldName: string): {
44
- needs: Record<string, boolean>;
45
- compute: (model: Record<string, unknown>) => Id<Brand>;
46
- };
53
+ computeField(fieldName: string): IdComputeField<Brand>;
47
54
  };
48
55
  /**
49
56
  * Creates a read/write transform pair for use with Prisma's `$extends` extension model.
@@ -72,5 +79,5 @@ type IdTransform<Brand extends string> = {
72
79
  */
73
80
  declare function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand>;
74
81
  //#endregion
75
- export { type IdColumnCodec, IdTransform, IdsError, type IdsErrorCode, idField, isIdsError };
82
+ export { type IdColumnCodec, IdComputeField, IdTransform, IdsError, type IdsErrorCode, idField, isIdsError };
76
83
  //# sourceMappingURL=prisma.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/adapters/prisma.ts"],"mappings":";;;;;AAYA;;;;AAAA,KAAY,WAAA;;;;;;EAMV,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;EA6BsB;;;;;;;EArB/C,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;;;;;;;;;;;;;;AAqBkC;AA6BpD;;;;EA/BE,YAAA,CAAa,SAAA;IACX,KAAA,EAAO,MAAA;IACP,OAAA,GAAU,KAAA,EAAO,MAAA,sBAA4B,EAAA,CAAG,KAAA;EAAA;AAAA;;;;;;;;AA6BoC;;;;;;;;;;;;;;;;;;iBAAxE,OAAA,uBAA8B,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,WAAA,CAAY,KAAA"}
1
+ {"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/adapters/prisma.ts"],"mappings":";;;;;AAcA;;;;;;AAAA,KAAY,cAAA;EACV,KAAA,EAAO,MAAA;EACP,OAAA,GAAU,KAAA,EAAO,MAAA,sBAA4B,EAAA,CAAG,KAAA;AAAA;;;;;KAOtC,WAAA;;;;AAPsC;AAOlD;EAME,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;;;;;EAQzB,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;EAmBiB;;;;;;;;;;;;;;;AAAe;AA4BlD;;EA5BE,YAAA,CAAa,SAAA,WAAoB,cAAA,CAAe,KAAA;AAAA;;;;;;;;;;;;;AA4BsC;;;;;;;;;;;;;iBAAxE,OAAA,uBAA8B,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,WAAA,CAAY,KAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.mjs","names":[],"sources":["../src/adapters/prisma.ts"],"sourcesContent":["import { 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 * Read/write transform pair and `$extends` result-component factory for\n * integrating `Id<Brand>` with Prisma extensions.\n */\nexport type IdTransform<Brand extends string> = {\n /**\n * Read transform: validates the raw database value via `safeParse` and returns\n * `Id<Brand>`. Throws if the value is missing, malformed, or belongs to a\n * different brand.\n */\n read(value: unknown): Id<Brand>;\n /**\n * Write transform: passes `Id<Brand>` through as its canonical string form.\n * `Id<Brand>` is already the canonical string, so this is an identity function\n * at runtime.\n *\n * Use in a Prisma `$extends` query component or explicit `data` mapping.\n */\n write(value: Id<Brand>): string;\n /**\n * Creates a typed `$extends` result-component field definition that carries\n * `Id<Brand>` through Prisma's type machinery without a per-call-site cast.\n *\n * @param fieldName - The model field to read from (e.g. `\"id\"`).\n * @returns A `{ needs, compute }` object whose `compute` return type is\n * statically `Id<Brand>`, so the extended-client model field is typed correctly.\n *\n * @example\n * ```ts\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\n computeField(fieldName: string): {\n needs: Record<string, boolean>;\n compute: (model: Record<string, unknown>) => Id<Brand>;\n };\n};\n\n/**\n * Creates a read/write transform pair for use with Prisma's `$extends` extension model.\n *\n * Works with any codec variant exposing `safeParse`.\n *\n * Use `computeField(fieldName)` to produce a typed `$extends` result-component\n * field definition — the brand is carried through Prisma's type machinery\n * automatically and no per-call-site cast is required.\n *\n * @example\n * ```ts\n * import { idField } from \"@smonn/ids/prisma\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const userIdField = idField(usr);\n *\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\nexport function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand> {\n return {\n read(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n computeField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n // Prisma's $extends types `compute` as returning `any` in its constraint\n // type (DynamicResultExtensionArgs). Returning a pre-built object with an\n // explicit Id<Brand> return type on `compute` causes TypeScript to infer\n // the brand through the `& R` intersection in $extends — encapsulating\n // the single necessary cast here rather than pushing it to every call site.\n compute: (model: Record<string, unknown>): Id<Brand> =>\n readIdColumn(codec, model[fieldName]),\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,SAAgB,QAA8B,OAAiD;CAC7F,OAAO;EACL,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;EACA,aAAa,WAAmB;GAC9B,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAM3B,UAAU,UACR,aAAa,OAAO,MAAM,UAAU;GACxC;EACF;CACF;AACF"}
1
+ {"version":3,"file":"prisma.mjs","names":[],"sources":["../src/adapters/prisma.ts"],"sourcesContent":["import { 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 * Typed `$extends` result-component field definition produced by\n * {@link IdTransform.computeField} — a `{ needs, compute }` pair whose `compute`\n * return type is statically `Id<Brand>`, so the extended-client model field is\n * typed correctly without a per-call-site cast.\n */\nexport type IdComputeField<Brand extends string> = {\n needs: Record<string, boolean>;\n compute: (model: Record<string, unknown>) => Id<Brand>;\n};\n\n/**\n * Read/write transform pair and `$extends` result-component factory for\n * integrating `Id<Brand>` with Prisma extensions.\n */\nexport type IdTransform<Brand extends string> = {\n /**\n * Read transform: validates the raw database value via `safeParse` and returns\n * `Id<Brand>`. Throws if the value is missing, malformed, or belongs to a\n * different brand.\n */\n read(value: unknown): Id<Brand>;\n /**\n * Write transform: passes `Id<Brand>` through as its canonical string form.\n * `Id<Brand>` is already the canonical string, so this is an identity function\n * at runtime.\n *\n * Use in a Prisma `$extends` query component or explicit `data` mapping.\n */\n write(value: Id<Brand>): string;\n /**\n * Creates a typed `$extends` result-component field definition that carries\n * `Id<Brand>` through Prisma's type machinery without a per-call-site cast.\n *\n * @param fieldName - The model field to read from (e.g. `\"id\"`).\n * @returns An {@link IdComputeField} whose `compute` return type is statically\n * `Id<Brand>`, so the extended-client model field is typed correctly.\n *\n * @example\n * ```ts\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\n computeField(fieldName: string): IdComputeField<Brand>;\n};\n\n/**\n * Creates a read/write transform pair for use with Prisma's `$extends` extension model.\n *\n * Works with any codec variant exposing `safeParse`.\n *\n * Use `computeField(fieldName)` to produce a typed `$extends` result-component\n * field definition — the brand is carried through Prisma's type machinery\n * automatically and no per-call-site cast is required.\n *\n * @example\n * ```ts\n * import { idField } from \"@smonn/ids/prisma\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const userIdField = idField(usr);\n *\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\nexport function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand> {\n return {\n read(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n computeField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n // Prisma's $extends types `compute` as returning `any` in its constraint\n // type (DynamicResultExtensionArgs). Returning a pre-built object with an\n // explicit Id<Brand> return type on `compute` causes TypeScript to infer\n // the brand through the `& R` intersection in $extends — encapsulating\n // the single necessary cast here rather than pushing it to every call site.\n compute: (model: Record<string, unknown>): Id<Brand> =>\n readIdColumn(codec, model[fieldName]),\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,SAAgB,QAA8B,OAAiD;CAC7F,OAAO;EACL,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;EACA,aAAa,WAAmB;GAC9B,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAM3B,UAAU,UACR,aAAa,OAAO,MAAM,UAAU;GACxC;EACF;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smonn/ids",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "license": "MIT",
5
5
  "author": "Simon Ingeson (https://github.com/smonn)",
6
6
  "repository": {
@@ -11,7 +11,8 @@
11
11
  "ids": "./dist/cli.mjs"
12
12
  },
13
13
  "files": [
14
- "dist"
14
+ "dist",
15
+ "spec/vectors.json"
15
16
  ],
16
17
  "type": "module",
17
18
  "types": "./dist/index.d.mts",
@@ -0,0 +1,97 @@
1
+ {
2
+ "version": 1,
3
+ "vectors": [
4
+ {
5
+ "name": "timestamp_extract_running_example",
6
+ "category": "codec:timestamp",
7
+ "operation": "extract",
8
+ "input": "usr_06f80z92d2dbsqqg28t5cy4tqg",
9
+ "expected": 1780272145000
10
+ },
11
+ {
12
+ "name": "timestamp_generate_running_example",
13
+ "category": "codec:timestamp",
14
+ "operation": "generate",
15
+ "input": { "timestamp": 1780272145000, "rng": "9abcdef0123456789abc" },
16
+ "expected": "usr_06f80z92d2dbsqqg28t5cy4tqg"
17
+ },
18
+ {
19
+ "name": "reverse_extract_running_example",
20
+ "description": "the 6 timestamp bytes are bitwise-inverted before encoding; extract re-inverts them",
21
+ "category": "codec:reverse",
22
+ "operation": "extract",
23
+ "input": "usr_zsgqz0pxjydbsqqg28t5cy4tqg",
24
+ "expected": 1780272145000
25
+ },
26
+ {
27
+ "name": "reverse_generate_running_example",
28
+ "category": "codec:reverse",
29
+ "operation": "generate",
30
+ "input": { "timestamp": 1780272145000, "rng": "9abcdef0123456789abc" },
31
+ "expected": "usr_zsgqz0pxjydbsqqg28t5cy4tqg"
32
+ },
33
+ {
34
+ "name": "to_uuid_running_example",
35
+ "description": "the 16 payload bytes written verbatim as an RFC 9562 lowercase hyphenated UUID",
36
+ "category": "wire",
37
+ "operation": "to_uuid",
38
+ "input": "usr_06f80z92d2dbsqqg28t5cy4tqg",
39
+ "expected": "019e807d-2268-9abc-def0-123456789abc"
40
+ },
41
+ {
42
+ "name": "from_uuid_running_example",
43
+ "category": "wire",
44
+ "operation": "from_uuid",
45
+ "input": "019e807d-2268-9abc-def0-123456789abc",
46
+ "expected": { "ok": true, "id": "usr_06f80z92d2dbsqqg28t5cy4tqg" }
47
+ },
48
+ {
49
+ "name": "from_uuid_uppercase_accept",
50
+ "description": "RFC 9562 parsers accept uppercase hex; output is the same canonical id",
51
+ "category": "wire",
52
+ "operation": "from_uuid",
53
+ "input": "019E807D-2268-9ABC-DEF0-123456789ABC",
54
+ "expected": { "ok": true, "id": "usr_06f80z92d2dbsqqg28t5cy4tqg" }
55
+ },
56
+ {
57
+ "name": "from_uuid_reject_braces",
58
+ "description": "braced {…} form is rejected; only the hyphenated 8-4-4-4-12 form is accepted",
59
+ "category": "wire",
60
+ "operation": "from_uuid",
61
+ "input": "{019e807d-2268-9abc-def0-123456789abc}",
62
+ "expected": { "ok": false, "layer": "uuid" }
63
+ },
64
+ {
65
+ "name": "canonicalize_uppercase",
66
+ "description": "ASCII A-Z folds to lowercase",
67
+ "category": "wire",
68
+ "operation": "canonicalize",
69
+ "input": "USR_06F80Z92D2DBSQQG28T5CY4TQG",
70
+ "expected": { "ok": true, "id": "usr_06f80z92d2dbsqqg28t5cy4tqg" }
71
+ },
72
+ {
73
+ "name": "canonicalize_alias_o_to_zero",
74
+ "description": "Crockford visual alias o/O resolves to 0 before validation",
75
+ "category": "wire",
76
+ "operation": "canonicalize",
77
+ "input": "usr_o6f8oz92d2dbsqqg28t5cy4tqg",
78
+ "expected": { "ok": true, "id": "usr_06f80z92d2dbsqqg28t5cy4tqg" }
79
+ },
80
+ {
81
+ "name": "canonicalize_reject_padding_bit",
82
+ "description": "final base32 char '1' has non-zero low 2 bits (not in [048cgmrw])",
83
+ "category": "wire",
84
+ "operation": "canonicalize",
85
+ "input": "usr_00000000000000000000000001",
86
+ "expected": { "ok": false, "layer": "base32" }
87
+ },
88
+ {
89
+ "name": "canonicalize_reject_wrong_prefix",
90
+ "description": "a well-formed id of a different brand is rejected at the prefix layer (vectors are authored against brand usr)",
91
+ "category": "wire",
92
+ "operation": "canonicalize",
93
+ "input": "org_06f80z92d2dbsqqg28t5cy4tqg",
94
+ "expected": { "ok": false, "layer": "prefix" }
95
+ }
96
+ ]
97
+ }