prisma-guard 1.32.0 → 1.33.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.
@@ -196,7 +196,7 @@ function isJsonSafe(value) {
196
196
  }
197
197
  return true;
198
198
  }
199
- var DECIMAL_REGEX = /^-?(\d+\.?\d*|\.\d+)([eE]-?\d+)?$/;
199
+ var DECIMAL_REGEX = /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
200
200
  var decimalStringSchema = z.string().refine((s) => DECIMAL_REGEX.test(s), "Invalid decimal string");
201
201
  var decimalObjectSchema = z.custom(
202
202
  (v) => v !== null && typeof v === "object" && typeof v.toFixed === "function" && typeof v.toNumber === "function",
@@ -956,8 +956,13 @@ function matchVariantPatterns(keys, caller) {
956
956
  continue;
957
957
  let matchesCaller = true;
958
958
  for (let index = 0; index < patternParts.length; index++) {
959
- if (patternParts[index].startsWith(":"))
959
+ if (patternParts[index].startsWith(":")) {
960
+ if (callerParts[index].length === 0) {
961
+ matchesCaller = false;
962
+ break;
963
+ }
960
964
  continue;
965
+ }
961
966
  if (patternParts[index] !== callerParts[index]) {
962
967
  matchesCaller = false;
963
968
  break;
@@ -1052,8 +1057,12 @@ function deepClone(value) {
1052
1057
  case "object": {
1053
1058
  if (value instanceof Date)
1054
1059
  return new Date(value.getTime());
1055
- if (value instanceof Uint8Array)
1060
+ if (value instanceof Uint8Array) {
1061
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer?.(value)) {
1062
+ return Buffer.from(value);
1063
+ }
1056
1064
  return value.slice();
1065
+ }
1057
1066
  if (value instanceof RegExp)
1058
1067
  return new RegExp(value.source, value.flags);
1059
1068
  if (Array.isArray(value))
@@ -1063,7 +1072,17 @@ function deepClone(value) {
1063
1072
  return value;
1064
1073
  const result = {};
1065
1074
  for (const [k, v] of Object.entries(value)) {
1066
- result[k] = deepClone(v);
1075
+ const cloned = deepClone(v);
1076
+ if (k === "__proto__") {
1077
+ Object.defineProperty(result, k, {
1078
+ value: cloned,
1079
+ enumerable: true,
1080
+ writable: true,
1081
+ configurable: true
1082
+ });
1083
+ } else {
1084
+ result[k] = cloned;
1085
+ }
1067
1086
  }
1068
1087
  return result;
1069
1088
  }
@@ -1073,6 +1092,9 @@ function deepClone(value) {
1073
1092
  }
1074
1093
 
1075
1094
  // src/shared/deep-equal.ts
1095
+ function isDecimalLike(v) {
1096
+ return v !== null && typeof v === "object" && typeof v.toFixed === "function" && typeof v.toNumber === "function";
1097
+ }
1076
1098
  function deepEqual(a, b) {
1077
1099
  if (a === b)
1078
1100
  return true;
@@ -1098,6 +1120,28 @@ function deepEqual(a, b) {
1098
1120
  }
1099
1121
  if (a instanceof RegExp || b instanceof RegExp)
1100
1122
  return false;
1123
+ if (a instanceof Uint8Array || b instanceof Uint8Array) {
1124
+ if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array))
1125
+ return false;
1126
+ if (a.byteLength !== b.byteLength)
1127
+ return false;
1128
+ for (let i = 0; i < a.byteLength; i++) {
1129
+ if (a[i] !== b[i])
1130
+ return false;
1131
+ }
1132
+ return true;
1133
+ }
1134
+ if (isDecimalLike(a) || isDecimalLike(b)) {
1135
+ if (!isDecimalLike(a) || !isDecimalLike(b))
1136
+ return false;
1137
+ try {
1138
+ if (typeof a.equals === "function")
1139
+ return a.equals(b) === true;
1140
+ return a.toString() === b.toString();
1141
+ } catch {
1142
+ return false;
1143
+ }
1144
+ }
1101
1145
  if (Array.isArray(a)) {
1102
1146
  if (!Array.isArray(b))
1103
1147
  return false;
@@ -1486,12 +1530,15 @@ function applyForcedCountWhere(container, forcedCountWhere) {
1486
1530
  }
1487
1531
  function resolvedWhereCoversConstraint(where, constraint) {
1488
1532
  if (constraint.fields.length === 1) {
1489
- return constraint.fields[0] in where;
1533
+ const value2 = where[constraint.fields[0]];
1534
+ return value2 !== null && value2 !== void 0;
1490
1535
  }
1491
1536
  const value = where[constraint.selector];
1492
1537
  if (!isPlainObject(value))
1493
1538
  return false;
1494
- return constraint.fields.every((field) => field in value);
1539
+ return constraint.fields.every(
1540
+ (field) => value[field] !== null && value[field] !== void 0
1541
+ );
1495
1542
  }
1496
1543
  function validateResolvedUniqueWhere(model, where, method, uniqueMap) {
1497
1544
  const constraints = uniqueMap[model];
@@ -1518,17 +1565,16 @@ function assertDirectUniqueShapeValue(model, field, value, typeMap) {
1518
1565
  );
1519
1566
  }
1520
1567
  }
1521
- if (isForcedValue(value))
1522
- return;
1523
- if (isPlainObject(value)) {
1524
- const keys = Object.keys(value);
1568
+ const actual = isForcedValue(value) ? value.value : value;
1569
+ if (isPlainObject(actual)) {
1570
+ const keys = Object.keys(actual);
1525
1571
  throw new ShapeError(
1526
1572
  `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.`
1527
1573
  );
1528
1574
  }
1529
- if (value === null || value === void 0) {
1575
+ if (actual === null || actual === void 0) {
1530
1576
  throw new ShapeError(
1531
- `Invalid unique where shape for "${model ?? "unknown"}.${field}". Unique fields must use true or a forced value.`
1577
+ `Invalid unique where shape for "${model ?? "unknown"}.${field}". Unique fields must use true or a forced non-null value.`
1532
1578
  );
1533
1579
  }
1534
1580
  }
@@ -1542,9 +1588,8 @@ function shapeCoversConstraint(where, constraint, model, typeMap) {
1542
1588
  }
1543
1589
  if (!(constraint.selector in where))
1544
1590
  return false;
1545
- const selectorValue = where[constraint.selector];
1546
- if (isForcedValue(selectorValue))
1547
- return true;
1591
+ const rawSelectorValue = where[constraint.selector];
1592
+ const selectorValue = isForcedValue(rawSelectorValue) ? rawSelectorValue.value : rawSelectorValue;
1548
1593
  if (!isPlainObject(selectorValue)) {
1549
1594
  throw new ShapeError(
1550
1595
  `Compound unique selector "${model}.${constraint.selector}" must be an object with fields: ${constraint.fields.join(", ")}`
@@ -2047,8 +2092,18 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2047
2092
  (constraint) => constraint.selector === selector
2048
2093
  ) ?? null;
2049
2094
  }
2050
- function parseForcedUniqueValue(model, fieldName, fieldMeta, value) {
2095
+ function buildUniqueWhereFieldSchema(fieldMeta, allowNullableFilter) {
2051
2096
  const schema = buildDirectScalarSchema(fieldMeta, enumMap, scalarBase);
2097
+ if (allowNullableFilter && !fieldMeta.isRequired) {
2098
+ return schema.nullable();
2099
+ }
2100
+ return schema;
2101
+ }
2102
+ function parseForcedUniqueValue(model, fieldName, fieldMeta, value, allowNullableFilter) {
2103
+ const schema = buildUniqueWhereFieldSchema(
2104
+ fieldMeta,
2105
+ allowNullableFilter
2106
+ );
2052
2107
  const actual = isForcedValue(value) ? value.value : value;
2053
2108
  try {
2054
2109
  return schema.parse(actual);
@@ -2126,10 +2181,9 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2126
2181
  `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.`
2127
2182
  );
2128
2183
  }
2129
- const directSchema2 = buildDirectScalarSchema(
2184
+ const directSchema2 = buildUniqueWhereFieldSchema(
2130
2185
  fieldMeta2,
2131
- enumMap,
2132
- scalarBase
2186
+ false
2133
2187
  );
2134
2188
  if (fieldValue === true) {
2135
2189
  nestedSchemas[fieldName] = directSchema2;
@@ -2138,7 +2192,8 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2138
2192
  model,
2139
2193
  fieldName,
2140
2194
  fieldMeta2,
2141
- fieldValue
2195
+ fieldValue,
2196
+ false
2142
2197
  );
2143
2198
  }
2144
2199
  }
@@ -2177,7 +2232,7 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2177
2232
  `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 }.`
2178
2233
  );
2179
2234
  }
2180
- const directSchema = buildDirectScalarSchema(fieldMeta, enumMap, scalarBase);
2235
+ const directSchema = buildUniqueWhereFieldSchema(fieldMeta, true);
2181
2236
  if (value === true) {
2182
2237
  fieldSchemas[key] = directSchema;
2183
2238
  continue;
@@ -2186,12 +2241,32 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2186
2241
  model,
2187
2242
  key,
2188
2243
  fieldMeta,
2189
- value
2244
+ value,
2245
+ true
2190
2246
  );
2191
2247
  forcedOnlyKeys.add(key);
2192
2248
  }
2249
+ const selectorKeys = Object.keys(fieldSchemas);
2250
+ let schema;
2251
+ if (selectorKeys.length === 0) {
2252
+ schema = null;
2253
+ } else if (selectorKeys.length === 1) {
2254
+ schema = z5.object(fieldSchemas).strict();
2255
+ } else {
2256
+ const optionalSchemas = {};
2257
+ for (const [k, s] of Object.entries(fieldSchemas)) {
2258
+ optionalSchemas[k] = s.optional();
2259
+ }
2260
+ const objectSchema = z5.object(optionalSchemas).strict();
2261
+ schema = Object.keys(forcedConditions).length > 0 ? objectSchema : objectSchema.refine(
2262
+ (v) => selectorKeys.some((k) => v[k] !== void 0),
2263
+ {
2264
+ message: `Unique where for model "${model}" must specify at least one selector: ${selectorKeys.join(", ")}`
2265
+ }
2266
+ );
2267
+ }
2193
2268
  return {
2194
- schema: Object.keys(fieldSchemas).length > 0 ? z5.object(fieldSchemas).strict() : null,
2269
+ schema,
2195
2270
  forced: {
2196
2271
  conditions: forcedConditions,
2197
2272
  relations: {}
@@ -3539,7 +3614,21 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3539
3614
  built = builtCache.get(resolution.key);
3540
3615
  }
3541
3616
  }
3542
- return applyBuiltShape(built, normalizedBody, isUnique);
3617
+ const result = applyBuiltShape(
3618
+ built,
3619
+ normalizedBody,
3620
+ isUnique,
3621
+ model
3622
+ );
3623
+ if (isUnique && result.where) {
3624
+ validateResolvedUniqueWhere(
3625
+ model,
3626
+ result.where,
3627
+ method,
3628
+ uniqueMap
3629
+ );
3630
+ }
3631
+ return result;
3543
3632
  }
3544
3633
  };
3545
3634
  }
@@ -3638,6 +3727,12 @@ function pickMissingFksFromResult(result, fks) {
3638
3727
  return missing;
3639
3728
  }
3640
3729
  function validateScopeValue(root, value) {
3730
+ const t = typeof value;
3731
+ if (t !== "string" && t !== "number" && t !== "bigint") {
3732
+ throw new PolicyError(
3733
+ `Non-primitive scope value for root "${root}" (${t}). Only string, number, and bigint are accepted \u2014 an object or array here would silently become a Prisma filter and could bypass scope isolation.`
3734
+ );
3735
+ }
3641
3736
  if (typeof value === "string" && value.length === 0) {
3642
3737
  throw new PolicyError(
3643
3738
  `Empty string scope value for root "${root}". This is almost certainly a bug in the context function.`
@@ -3680,10 +3775,12 @@ function enforceDataScope(data, scopes, overrides, log, model, operation, onScop
3680
3775
  }
3681
3776
  var VALID_FIND_UNIQUE_MODES = /* @__PURE__ */ new Set(["verify", "reject"]);
3682
3777
  var VALID_ON_SCOPE_RELATION_WRITES = /* @__PURE__ */ new Set(["error", "warn", "strip"]);
3778
+ var VALID_ON_MISSING_SCOPE_CONTEXT = /* @__PURE__ */ new Set(["error", "warn", "ignore"]);
3683
3779
  function createScopeExtension(scopeMap, contextFn, guardConfig, logger) {
3684
3780
  const log = logger ?? { warn: (msg) => console.warn(msg) };
3685
3781
  const findUniqueMode = guardConfig.findUniqueMode ?? "reject";
3686
3782
  const onScopeRelationWrite = guardConfig.onScopeRelationWrite ?? "error";
3783
+ const onMissingScopeContext = guardConfig.onMissingScopeContext ?? "error";
3687
3784
  if (!VALID_FIND_UNIQUE_MODES.has(findUniqueMode)) {
3688
3785
  throw new ShapeError(
3689
3786
  `prisma-guard: Invalid findUniqueMode "${findUniqueMode}". Allowed: ${[...VALID_FIND_UNIQUE_MODES].join(", ")}`
@@ -3694,6 +3791,11 @@ function createScopeExtension(scopeMap, contextFn, guardConfig, logger) {
3694
3791
  `prisma-guard: Invalid onScopeRelationWrite "${onScopeRelationWrite}". Allowed: ${[...VALID_ON_SCOPE_RELATION_WRITES].join(", ")}`
3695
3792
  );
3696
3793
  }
3794
+ if (!VALID_ON_MISSING_SCOPE_CONTEXT.has(onMissingScopeContext)) {
3795
+ throw new ShapeError(
3796
+ `prisma-guard: Invalid onMissingScopeContext "${onMissingScopeContext}". Allowed: ${[...VALID_ON_MISSING_SCOPE_CONTEXT].join(", ")}`
3797
+ );
3798
+ }
3697
3799
  const scopeFkSets = /* @__PURE__ */ new Map();
3698
3800
  for (const [model, entries] of Object.entries(scopeMap)) {
3699
3801
  const fks = /* @__PURE__ */ new Set();
@@ -3719,12 +3821,12 @@ function createScopeExtension(scopeMap, contextFn, guardConfig, logger) {
3719
3821
  const missingRoots = scopes.filter((s) => ctx[s.root] == null).map((s) => s.root);
3720
3822
  const isMutation = CREATE_OPS.has(operation) || UNIQUE_MUTATION_OPS.has(operation) || MULTI_MUTATION_OPS.has(operation) || operation === "upsert";
3721
3823
  if (missingRoots.length > 0) {
3722
- if (isMutation || guardConfig.onMissingScopeContext === "error") {
3824
+ if (isMutation || onMissingScopeContext === "error") {
3723
3825
  throw new PolicyError(
3724
3826
  `prisma-guard: Missing scope context for model "${model}": roots ${missingRoots.map((r) => `"${r}"`).join(", ")} not provided. All scope roots must be present.`
3725
3827
  );
3726
3828
  }
3727
- if (guardConfig.onMissingScopeContext === "warn") {
3829
+ if (onMissingScopeContext === "warn") {
3728
3830
  log.warn(
3729
3831
  `prisma-guard: Missing scope context for model "${model}": roots ${missingRoots.map((r) => `"${r}"`).join(", ")} not provided. Read proceeding with partial scope.`
3730
3832
  );