prisma-guard 1.31.0 → 1.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1121,7 +1121,7 @@ The return value is:
1121
1121
  | `shape` | The concrete resolved guard shape after caller matching and dynamic shape execution |
1122
1122
  | `body` | The normalized request body. Omitted input becomes `{}` |
1123
1123
  | `effectiveReadBody` | The body used for read planning. If the body has no `select` or `include`, the shape's default read projection is applied |
1124
- | `matchedKey` | The selected named-shape key, such as `default`, `admin`, or `/user/me` |
1124
+ | `matchedKey` | The selected declared key, such as `default`, `admin`, or `/user/:id`. For parameterized callers this is the pattern key, not the concrete caller value |
1125
1125
  | `wasDynamic` | `true` when the matched shape was a function and was executed with guard context |
1126
1126
 
1127
1127
  `resolve()` uses the same guard extension context and caller selection path as `.findMany()`, `.findFirst()`, and the other guarded methods. If the shape is context-dependent, the shape function is called with the current guard context.
@@ -1915,22 +1915,40 @@ The `default` fallback works consistently across both `.guard()` and `guard.quer
1915
1915
 
1916
1916
  ### Note for generated route configs
1917
1917
 
1918
- The direct `.guard({ where: ... })` form is still valid in the runtime API. Generated route configs from `prisma-generator-express`, however, type operation `shape` as a named shape map. For a single generated-route shape, put it under `default`:
1918
+ The direct runtime API accepts all three guard input forms:
1919
+
1920
+ - one direct guard shape
1921
+ - one context-dependent shape function
1922
+ - a named shape map
1923
+
1924
+ `prisma-generator-express` mirrors those forms under an operation's `shape` property. It also provides a `variants` descriptor API when each named shape needs its own route hooks:
1919
1925
 
1920
1926
  ```ts
1921
1927
  const projectConfig = {
1922
1928
  findMany: {
1923
- shape: {
1929
+ before: [authenticate],
1930
+ variants: {
1931
+ admin: {
1932
+ shape: {
1933
+ where: { title: { contains: true }, status: { equals: true } },
1934
+ take: { max: 100 },
1935
+ },
1936
+ before: [requireAdmin],
1937
+ },
1924
1938
  default: {
1925
- where: { title: { contains: true } },
1926
- take: { max: 50, default: 20 },
1939
+ shape: {
1940
+ where: { title: { contains: true } },
1941
+ take: { max: 20, default: 10 },
1942
+ },
1927
1943
  },
1928
1944
  },
1929
1945
  },
1930
1946
  }
1931
1947
  ```
1932
1948
 
1933
- Additional keys such as `admin`, `public`, or path patterns are caller-routed variants. Reserved shape keys such as `where`, `data`, `include`, and `select` cannot be used as caller keys.
1949
+ Within one generated operation, `shape` and `variants` cannot both be defined. Both may be omitted when the operation only needs operation-wide hooks or pagination. Each `variants[key].shape` is one direct shape or one context-dependent shape function; it is not another nested named map.
1950
+
1951
+ Use `shape` when only guard routing is needed. Use `variants` when hooks need to differ by the matched named shape.
1934
1952
 
1935
1953
  ### Caller resolution order
1936
1954
 
@@ -1959,16 +1977,50 @@ await prisma.project.guard({ where: { ... } }).findMany(req.body)
1959
1977
  The `caller` key in the context object is not used for scope injection — it is only used for shape routing. Scope roots are identified by matching context keys against `@scope-root` model names.
1960
1978
 
1961
1979
  ### Parameterized caller patterns
1980
+
1962
1981
  ```text
1963
1982
  /org/:orgId/users
1964
1983
  /org/:orgId/users/:userId
1965
1984
  ```
1966
1985
 
1967
- Matching is case-sensitive. Exact matches are checked first. If no exact match is found, parameterized patterns are evaluated. Parameters are routing-only and are not extracted into context.
1986
+ Matching is case-sensitive. A parameter segment starts with `:` and matches exactly one path segment. Segment counts must be equal. Parameters are routing-only and are not extracted into context.
1987
+
1988
+ Caller routing uses this precedence:
1989
+
1990
+ 1. Reject any named key that collides with a reserved guard shape key.
1991
+ 2. If caller is not a string, use `default` when present; otherwise throw the missing-caller `CallerError`.
1992
+ 3. If caller is blank or whitespace-only, skip exact and parameterized matching. Use `default` when present; otherwise throw unknown-caller `CallerError`.
1993
+ 4. A non-blank exact key match wins immediately.
1994
+ 5. If there is no exact match, evaluate parameterized patterns.
1995
+ 6. One matching pattern selects that declared pattern key.
1996
+ 7. Multiple matching patterns throw an ambiguous-caller `CallerError` listing every match.
1997
+ 8. With no match, use `default` when present; otherwise throw unknown-caller `CallerError`.
1998
+
1999
+ Exact matching therefore wins over a pattern that would also match:
2000
+
2001
+ ```ts
2002
+ const shapes = {
2003
+ 'customer/123': exactShape,
2004
+ 'customer/:id': parameterizedShape,
2005
+ }
2006
+
2007
+ // Selects "customer/123", not "customer/:id".
2008
+ await prisma.order.guard(shapes, 'customer/123').findMany()
2009
+ ```
2010
+
2011
+ For a parameterized caller, `matchedKey` is the declared pattern key. A caller of `customer/123` matched by `customer/:id` produces `matchedKey === 'customer/:id'`. This is the key used for shape memoization and returned by `resolve()`.
1968
2012
 
1969
2013
  ### Fail-closed behavior
1970
2014
 
1971
- If `caller` is missing or doesn't match any pattern and no `default` key exists, the request is rejected with a `CallerError`. If a caller matches multiple parameterized patterns, it is also rejected with a `CallerError`.
2015
+ Named routing fails closed:
2016
+
2017
+ - A non-string caller with no `default` throws the missing-caller `CallerError`.
2018
+ - A blank or whitespace-only caller never matches a blank key or a parameterized key. It uses `default` or throws unknown-caller `CallerError`.
2019
+ - An unmatched caller uses `default` or throws unknown-caller `CallerError`.
2020
+ - Multiple matching parameterized patterns throw ambiguous-caller `CallerError`.
2021
+ - A caller key that collides with a reserved shape key throws `ShapeError`.
2022
+
2023
+ The same precedence and error behavior apply to `.guard()` and `guard.query().parse()`.
1972
2024
 
1973
2025
  If a request body contains a `caller` field when using named shapes, it is rejected with a `CallerError` that directs the developer to use the second argument to `.guard()` or the context function instead.
1974
2026
 
@@ -2738,6 +2790,4 @@ Possible future improvements:
2738
2790
 
2739
2791
  ## License
2740
2792
 
2741
- MIT
2742
-
2743
-
2793
+ MIT
@@ -975,40 +975,75 @@ function unsupported() {
975
975
  return marker;
976
976
  }
977
977
 
978
- // src/shared/match-caller.ts
979
- function matchCallerPattern(patterns, caller) {
980
- if (typeof caller !== "string" || caller.trim().length === 0)
981
- return null;
982
- if (patterns.includes(caller))
983
- return caller;
978
+ // src/shared/guard-variant-routing.ts
979
+ function matchVariantPatterns(keys, caller) {
980
+ const callerParts = caller.split("/");
984
981
  const matches = [];
985
- for (const pattern of patterns) {
982
+ for (const pattern of keys) {
986
983
  if (!pattern.includes(":"))
987
984
  continue;
988
985
  const patternParts = pattern.split("/");
989
- const callerParts = caller.split("/");
990
986
  if (patternParts.length !== callerParts.length)
991
987
  continue;
992
- let ok = true;
993
- for (let i = 0; i < patternParts.length; i++) {
994
- if (patternParts[i].startsWith(":"))
988
+ let matchesCaller = true;
989
+ for (let index = 0; index < patternParts.length; index++) {
990
+ if (patternParts[index].startsWith(":"))
995
991
  continue;
996
- if (patternParts[i] !== callerParts[i]) {
997
- ok = false;
992
+ if (patternParts[index] !== callerParts[index]) {
993
+ matchesCaller = false;
998
994
  break;
999
995
  }
1000
996
  }
1001
- if (ok)
997
+ if (matchesCaller)
1002
998
  matches.push(pattern);
1003
999
  }
1004
- if (matches.length === 0)
1005
- return null;
1000
+ return matches;
1001
+ }
1002
+ function resolveGuardVariantKey(input) {
1003
+ if (input.kind === "single") {
1004
+ return { ok: true, key: "_default" };
1005
+ }
1006
+ const { keys, caller, reservedKeys } = input;
1007
+ for (const key of keys) {
1008
+ if (reservedKeys.has(key)) {
1009
+ return {
1010
+ ok: false,
1011
+ code: "reserved-key",
1012
+ key,
1013
+ keys
1014
+ };
1015
+ }
1016
+ }
1017
+ const hasDefault = keys.includes("default");
1018
+ if (typeof caller !== "string") {
1019
+ if (hasDefault)
1020
+ return { ok: true, key: "default" };
1021
+ return { ok: false, code: "missing-caller", keys };
1022
+ }
1023
+ if (caller.trim().length === 0) {
1024
+ if (hasDefault)
1025
+ return { ok: true, key: "default" };
1026
+ return { ok: false, code: "unknown-caller", caller, keys };
1027
+ }
1028
+ if (keys.includes(caller)) {
1029
+ return { ok: true, key: caller };
1030
+ }
1031
+ const matches = matchVariantPatterns(keys, caller);
1032
+ if (matches.length === 1) {
1033
+ return { ok: true, key: matches[0] };
1034
+ }
1006
1035
  if (matches.length > 1) {
1007
- throw new CallerError(
1008
- `Ambiguous caller "${caller}" matches multiple patterns: ${matches.map((p) => `"${p}"`).join(", ")}`
1009
- );
1036
+ return {
1037
+ ok: false,
1038
+ code: "ambiguous-caller",
1039
+ caller,
1040
+ keys,
1041
+ matches
1042
+ };
1010
1043
  }
1011
- return matches[0];
1044
+ if (hasDefault)
1045
+ return { ok: true, key: "default" };
1046
+ return { ok: false, code: "unknown-caller", caller, keys };
1012
1047
  }
1013
1048
 
1014
1049
  // src/runtime/policy.ts
@@ -1482,12 +1517,15 @@ function applyForcedCountWhere(container, forcedCountWhere) {
1482
1517
  }
1483
1518
  function resolvedWhereCoversConstraint(where, constraint) {
1484
1519
  if (constraint.fields.length === 1) {
1485
- return constraint.fields[0] in where;
1520
+ const value2 = where[constraint.fields[0]];
1521
+ return value2 !== null && value2 !== void 0;
1486
1522
  }
1487
1523
  const value = where[constraint.selector];
1488
1524
  if (!isPlainObject(value))
1489
1525
  return false;
1490
- return constraint.fields.every((field) => field in value);
1526
+ return constraint.fields.every(
1527
+ (field) => value[field] !== null && value[field] !== void 0
1528
+ );
1491
1529
  }
1492
1530
  function validateResolvedUniqueWhere(model, where, method, uniqueMap) {
1493
1531
  const constraints = uniqueMap[model];
@@ -1514,17 +1552,16 @@ function assertDirectUniqueShapeValue(model, field, value, typeMap) {
1514
1552
  );
1515
1553
  }
1516
1554
  }
1517
- if (isForcedValue(value))
1518
- return;
1519
- if (isPlainObject(value)) {
1520
- const keys = Object.keys(value);
1555
+ const actual = isForcedValue(value) ? value.value : value;
1556
+ if (isPlainObject(actual)) {
1557
+ const keys = Object.keys(actual);
1521
1558
  throw new ShapeError(
1522
1559
  `Invalid unique where shape for "${model ?? "unknown"}.${field}". Prisma WhereUniqueInput does not accept filter operator objects${keys.length ? `: ${keys.join(", ")}` : ""}. Use { ${field}: true } in guard shape and { ${field}: value } in request args.`
1523
1560
  );
1524
1561
  }
1525
- if (value === null || value === void 0) {
1562
+ if (actual === null || actual === void 0) {
1526
1563
  throw new ShapeError(
1527
- `Invalid unique where shape for "${model ?? "unknown"}.${field}". Unique fields must use true or a forced value.`
1564
+ `Invalid unique where shape for "${model ?? "unknown"}.${field}". Unique fields must use true or a forced non-null value.`
1528
1565
  );
1529
1566
  }
1530
1567
  }
@@ -1538,9 +1575,8 @@ function shapeCoversConstraint(where, constraint, model, typeMap) {
1538
1575
  }
1539
1576
  if (!(constraint.selector in where))
1540
1577
  return false;
1541
- const selectorValue = where[constraint.selector];
1542
- if (isForcedValue(selectorValue))
1543
- return true;
1578
+ const rawSelectorValue = where[constraint.selector];
1579
+ const selectorValue = isForcedValue(rawSelectorValue) ? rawSelectorValue.value : rawSelectorValue;
1544
1580
  if (!isPlainObject(selectorValue)) {
1545
1581
  throw new ShapeError(
1546
1582
  `Compound unique selector "${model}.${constraint.selector}" must be an object with fields: ${constraint.fields.join(", ")}`
@@ -2043,8 +2079,18 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2043
2079
  (constraint) => constraint.selector === selector
2044
2080
  ) ?? null;
2045
2081
  }
2046
- function parseForcedUniqueValue(model, fieldName, fieldMeta, value) {
2082
+ function buildUniqueWhereFieldSchema(fieldMeta, allowNullableFilter) {
2047
2083
  const schema = buildDirectScalarSchema(fieldMeta, enumMap, scalarBase);
2084
+ if (allowNullableFilter && !fieldMeta.isRequired) {
2085
+ return schema.nullable();
2086
+ }
2087
+ return schema;
2088
+ }
2089
+ function parseForcedUniqueValue(model, fieldName, fieldMeta, value, allowNullableFilter) {
2090
+ const schema = buildUniqueWhereFieldSchema(
2091
+ fieldMeta,
2092
+ allowNullableFilter
2093
+ );
2048
2094
  const actual = isForcedValue(value) ? value.value : value;
2049
2095
  try {
2050
2096
  return schema.parse(actual);
@@ -2122,10 +2168,9 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2122
2168
  `Invalid compound unique where shape for "${model}.${key}.${fieldName}". Prisma compound unique selectors do not accept filter operator objects${operators.length ? `: ${operators.join(", ")}` : ""}. Use direct values only.`
2123
2169
  );
2124
2170
  }
2125
- const directSchema2 = buildDirectScalarSchema(
2171
+ const directSchema2 = buildUniqueWhereFieldSchema(
2126
2172
  fieldMeta2,
2127
- enumMap,
2128
- scalarBase
2173
+ false
2129
2174
  );
2130
2175
  if (fieldValue === true) {
2131
2176
  nestedSchemas[fieldName] = directSchema2;
@@ -2134,7 +2179,8 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2134
2179
  model,
2135
2180
  fieldName,
2136
2181
  fieldMeta2,
2137
- fieldValue
2182
+ fieldValue,
2183
+ false
2138
2184
  );
2139
2185
  }
2140
2186
  }
@@ -2173,7 +2219,7 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2173
2219
  `Invalid unique where shape for "${model}.${key}". Prisma WhereUniqueInput does not accept operators: ${keys.join(", ")}. Use a direct unique value shape, for example { ${key}: true }.`
2174
2220
  );
2175
2221
  }
2176
- const directSchema = buildDirectScalarSchema(fieldMeta, enumMap, scalarBase);
2222
+ const directSchema = buildUniqueWhereFieldSchema(fieldMeta, true);
2177
2223
  if (value === true) {
2178
2224
  fieldSchemas[key] = directSchema;
2179
2225
  continue;
@@ -2182,7 +2228,8 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2182
2228
  model,
2183
2229
  key,
2184
2230
  fieldMeta,
2185
- value
2231
+ value,
2232
+ true
2186
2233
  );
2187
2234
  forcedOnlyKeys.add(key);
2188
2235
  }
@@ -3441,22 +3488,27 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3441
3488
  forcedSelectCountWhere
3442
3489
  };
3443
3490
  }
3444
- function matchCaller(shapes, caller) {
3445
- const matched = matchCallerPattern(Object.keys(shapes), caller);
3446
- if (!matched)
3447
- return null;
3448
- return { key: matched, shape: shapes[matched] };
3449
- }
3450
- function resolveDefaultShape(config, model, method, normalizedBody, opts, builtCache, isUnique) {
3451
- const shapeOrFn = config.default;
3452
- let built;
3453
- if (typeof shapeOrFn === "function") {
3454
- const resolved = resolveAndValidateShape(shapeOrFn, opts?.ctx);
3455
- built = buildShapeZodSchema(model, method, resolved);
3456
- } else {
3457
- built = builtCache.get("default");
3491
+ function throwQueryVariantResolution(resolution) {
3492
+ const keys = resolution.keys.map((key) => `"${key}"`).join(", ");
3493
+ if (resolution.code === "reserved-key") {
3494
+ throw new ShapeError(
3495
+ `Caller key "${resolution.key}" collides with reserved shape config key. Rename the caller path.`
3496
+ );
3497
+ }
3498
+ if (resolution.code === "missing-caller") {
3499
+ throw new CallerError(
3500
+ `Missing caller. This query uses named shape routing with keys: ${keys}. Provide caller via opts.caller.`
3501
+ );
3458
3502
  }
3459
- return applyBuiltShape(built, normalizedBody, isUnique);
3503
+ if (resolution.code === "ambiguous-caller") {
3504
+ const matches = (resolution.matches ?? []).map((pattern) => `"${pattern}"`).join(", ");
3505
+ throw new CallerError(
3506
+ `Ambiguous caller "${resolution.caller}" matches multiple patterns: ${matches}`
3507
+ );
3508
+ }
3509
+ throw new CallerError(
3510
+ `Unknown caller: "${resolution.caller}". Allowed: ${keys}`
3511
+ );
3460
3512
  }
3461
3513
  function buildQuerySchema(model, method, config) {
3462
3514
  const isSingle = typeof config === "function" || isShapeConfig(config);
@@ -3468,12 +3520,15 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3468
3520
  );
3469
3521
  }
3470
3522
  if (!isSingle) {
3471
- for (const key of Object.keys(config)) {
3472
- if (SHAPE_CONFIG_KEYS.has(key)) {
3473
- throw new ShapeError(
3474
- `Caller key "${key}" collides with reserved shape config key. Rename the caller path.`
3475
- );
3476
- }
3523
+ const keys = Object.keys(config);
3524
+ const validation = resolveGuardVariantKey({
3525
+ kind: "named",
3526
+ keys,
3527
+ caller: void 0,
3528
+ reservedKeys: SHAPE_CONFIG_KEYS
3529
+ });
3530
+ if (!validation.ok && validation.code === "reserved-key") {
3531
+ throwQueryVariantResolution(validation);
3477
3532
  }
3478
3533
  for (const [key, shapeOrFn] of Object.entries(
3479
3534
  config
@@ -3511,50 +3566,37 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3511
3566
  );
3512
3567
  }
3513
3568
  const namedConfig = config;
3514
- const caller = opts?.caller;
3515
- if (typeof caller !== "string") {
3516
- if ("default" in namedConfig) {
3517
- return resolveDefaultShape(
3518
- namedConfig,
3519
- model,
3520
- method,
3521
- normalizedBody,
3522
- opts,
3523
- builtCache,
3524
- isUnique
3525
- );
3526
- }
3527
- const allowed = Object.keys(namedConfig);
3528
- throw new CallerError(
3529
- `Missing caller. This query uses named shape routing with keys: ${allowed.map((k) => `"${k}"`).join(", ")}. Provide caller via opts.caller.`
3530
- );
3531
- }
3532
- const matched = matchCaller(namedConfig, caller);
3533
- if (!matched) {
3534
- if ("default" in namedConfig) {
3535
- return resolveDefaultShape(
3536
- namedConfig,
3537
- model,
3538
- method,
3539
- normalizedBody,
3540
- opts,
3541
- builtCache,
3542
- isUnique
3543
- );
3544
- }
3545
- const allowed = Object.keys(namedConfig);
3546
- throw new CallerError(
3547
- `Unknown caller: "${caller}". Allowed: ${allowed.map((k) => `"${k}"`).join(", ")}`
3548
- );
3549
- }
3550
- if (typeof matched.shape === "function") {
3551
- const resolved = resolveAndValidateShape(matched.shape, opts?.ctx);
3569
+ const resolution = resolveGuardVariantKey({
3570
+ kind: "named",
3571
+ keys: Object.keys(namedConfig),
3572
+ caller: opts?.caller,
3573
+ reservedKeys: SHAPE_CONFIG_KEYS
3574
+ });
3575
+ if (!resolution.ok)
3576
+ throwQueryVariantResolution(resolution);
3577
+ const shapeOrFn = namedConfig[resolution.key];
3578
+ if (typeof shapeOrFn === "function") {
3579
+ const resolved = resolveAndValidateShape(shapeOrFn, opts?.ctx);
3552
3580
  built = buildShapeZodSchema(model, method, resolved);
3553
3581
  } else {
3554
- built = builtCache.get(matched.key);
3582
+ built = builtCache.get(resolution.key);
3555
3583
  }
3556
3584
  }
3557
- return applyBuiltShape(built, normalizedBody, isUnique);
3585
+ const result = applyBuiltShape(
3586
+ built,
3587
+ normalizedBody,
3588
+ isUnique,
3589
+ model
3590
+ );
3591
+ if (isUnique && result.where) {
3592
+ validateResolvedUniqueWhere(
3593
+ model,
3594
+ result.where,
3595
+ method,
3596
+ uniqueMap
3597
+ );
3598
+ }
3599
+ return result;
3558
3600
  }
3559
3601
  };
3560
3602
  }
@@ -4786,34 +4828,43 @@ function resolveDynamicShape(shapeFn, ctx, context) {
4786
4828
  }
4787
4829
  return result;
4788
4830
  }
4789
- function resolveNamedShape(input, body, contextFn, explicitCaller) {
4790
- assertNoCallerInBody(body);
4791
- const caller = explicitCaller;
4792
- const keys = Object.keys(input);
4793
- for (const key of keys) {
4794
- if (GUARD_SHAPE_KEYS.has(key)) {
4795
- throw new ShapeError(
4796
- `Caller key "${key}" collides with reserved guard shape key. Rename the caller path.`
4797
- );
4798
- }
4831
+ function throwVariantResolution(resolution) {
4832
+ const keys = resolution.keys.map((key) => `"${key}"`).join(", ");
4833
+ if (resolution.code === "reserved-key") {
4834
+ throw new ShapeError(
4835
+ `Caller key "${resolution.key}" collides with reserved guard shape key. Rename the caller path.`
4836
+ );
4799
4837
  }
4800
- if (typeof caller !== "string") {
4801
- if ("default" in input) {
4802
- return resolveShapeEntry(input.default, body, contextFn, "default");
4803
- }
4838
+ if (resolution.code === "missing-caller") {
4804
4839
  throw new CallerError(
4805
- `Missing caller. This guard uses named shape routing with keys: ${keys.map((k) => `"${k}"`).join(", ")}. Provide caller via guard(input, caller).`
4840
+ `Missing caller. This guard uses named shape routing with keys: ${keys}. Provide caller via guard(input, caller).`
4806
4841
  );
4807
4842
  }
4808
- const matched = matchCallerPattern(keys, caller);
4809
- if (matched) {
4810
- return resolveShapeEntry(input[matched], body, contextFn, matched);
4811
- }
4812
- if ("default" in input) {
4813
- return resolveShapeEntry(input.default, body, contextFn, "default");
4843
+ if (resolution.code === "ambiguous-caller") {
4844
+ const matches = (resolution.matches ?? []).map((pattern) => `"${pattern}"`).join(", ");
4845
+ throw new CallerError(
4846
+ `Ambiguous caller "${resolution.caller}" matches multiple patterns: ${matches}`
4847
+ );
4814
4848
  }
4815
4849
  throw new CallerError(
4816
- `Unknown caller: "${caller}". Allowed: ${keys.map((k) => `"${k}"`).join(", ")}`
4850
+ `Unknown caller: "${resolution.caller}". Allowed: ${keys}`
4851
+ );
4852
+ }
4853
+ function resolveNamedShape(input, body, contextFn, explicitCaller) {
4854
+ assertNoCallerInBody(body);
4855
+ const resolution = resolveGuardVariantKey({
4856
+ kind: "named",
4857
+ keys: Object.keys(input),
4858
+ caller: explicitCaller,
4859
+ reservedKeys: GUARD_SHAPE_KEYS
4860
+ });
4861
+ if (!resolution.ok)
4862
+ throwVariantResolution(resolution);
4863
+ return resolveShapeEntry(
4864
+ input[resolution.key],
4865
+ body,
4866
+ contextFn,
4867
+ resolution.key
4817
4868
  );
4818
4869
  }
4819
4870
  function resolveShapeEntry(entry, body, contextFn, matchedKey) {