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.
@@ -227,7 +227,7 @@ function isJsonSafe(value) {
227
227
  }
228
228
  return true;
229
229
  }
230
- var DECIMAL_REGEX = /^-?(\d+\.?\d*|\.\d+)([eE]-?\d+)?$/;
230
+ var DECIMAL_REGEX = /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
231
231
  var decimalStringSchema = import_zod.z.string().refine((s) => DECIMAL_REGEX.test(s), "Invalid decimal string");
232
232
  var decimalObjectSchema = import_zod.z.custom(
233
233
  (v) => v !== null && typeof v === "object" && typeof v.toFixed === "function" && typeof v.toNumber === "function",
@@ -987,8 +987,13 @@ function matchVariantPatterns(keys, caller) {
987
987
  continue;
988
988
  let matchesCaller = true;
989
989
  for (let index = 0; index < patternParts.length; index++) {
990
- if (patternParts[index].startsWith(":"))
990
+ if (patternParts[index].startsWith(":")) {
991
+ if (callerParts[index].length === 0) {
992
+ matchesCaller = false;
993
+ break;
994
+ }
991
995
  continue;
996
+ }
992
997
  if (patternParts[index] !== callerParts[index]) {
993
998
  matchesCaller = false;
994
999
  break;
@@ -1083,8 +1088,12 @@ function deepClone(value) {
1083
1088
  case "object": {
1084
1089
  if (value instanceof Date)
1085
1090
  return new Date(value.getTime());
1086
- if (value instanceof Uint8Array)
1091
+ if (value instanceof Uint8Array) {
1092
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer?.(value)) {
1093
+ return Buffer.from(value);
1094
+ }
1087
1095
  return value.slice();
1096
+ }
1088
1097
  if (value instanceof RegExp)
1089
1098
  return new RegExp(value.source, value.flags);
1090
1099
  if (Array.isArray(value))
@@ -1094,7 +1103,17 @@ function deepClone(value) {
1094
1103
  return value;
1095
1104
  const result = {};
1096
1105
  for (const [k, v] of Object.entries(value)) {
1097
- result[k] = deepClone(v);
1106
+ const cloned = deepClone(v);
1107
+ if (k === "__proto__") {
1108
+ Object.defineProperty(result, k, {
1109
+ value: cloned,
1110
+ enumerable: true,
1111
+ writable: true,
1112
+ configurable: true
1113
+ });
1114
+ } else {
1115
+ result[k] = cloned;
1116
+ }
1098
1117
  }
1099
1118
  return result;
1100
1119
  }
@@ -1104,6 +1123,9 @@ function deepClone(value) {
1104
1123
  }
1105
1124
 
1106
1125
  // src/shared/deep-equal.ts
1126
+ function isDecimalLike(v) {
1127
+ return v !== null && typeof v === "object" && typeof v.toFixed === "function" && typeof v.toNumber === "function";
1128
+ }
1107
1129
  function deepEqual(a, b) {
1108
1130
  if (a === b)
1109
1131
  return true;
@@ -1129,6 +1151,28 @@ function deepEqual(a, b) {
1129
1151
  }
1130
1152
  if (a instanceof RegExp || b instanceof RegExp)
1131
1153
  return false;
1154
+ if (a instanceof Uint8Array || b instanceof Uint8Array) {
1155
+ if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array))
1156
+ return false;
1157
+ if (a.byteLength !== b.byteLength)
1158
+ return false;
1159
+ for (let i = 0; i < a.byteLength; i++) {
1160
+ if (a[i] !== b[i])
1161
+ return false;
1162
+ }
1163
+ return true;
1164
+ }
1165
+ if (isDecimalLike(a) || isDecimalLike(b)) {
1166
+ if (!isDecimalLike(a) || !isDecimalLike(b))
1167
+ return false;
1168
+ try {
1169
+ if (typeof a.equals === "function")
1170
+ return a.equals(b) === true;
1171
+ return a.toString() === b.toString();
1172
+ } catch {
1173
+ return false;
1174
+ }
1175
+ }
1132
1176
  if (Array.isArray(a)) {
1133
1177
  if (!Array.isArray(b))
1134
1178
  return false;
@@ -1517,12 +1561,15 @@ function applyForcedCountWhere(container, forcedCountWhere) {
1517
1561
  }
1518
1562
  function resolvedWhereCoversConstraint(where, constraint) {
1519
1563
  if (constraint.fields.length === 1) {
1520
- return constraint.fields[0] in where;
1564
+ const value2 = where[constraint.fields[0]];
1565
+ return value2 !== null && value2 !== void 0;
1521
1566
  }
1522
1567
  const value = where[constraint.selector];
1523
1568
  if (!isPlainObject(value))
1524
1569
  return false;
1525
- return constraint.fields.every((field) => field in value);
1570
+ return constraint.fields.every(
1571
+ (field) => value[field] !== null && value[field] !== void 0
1572
+ );
1526
1573
  }
1527
1574
  function validateResolvedUniqueWhere(model, where, method, uniqueMap) {
1528
1575
  const constraints = uniqueMap[model];
@@ -1549,17 +1596,16 @@ function assertDirectUniqueShapeValue(model, field, value, typeMap) {
1549
1596
  );
1550
1597
  }
1551
1598
  }
1552
- if (isForcedValue(value))
1553
- return;
1554
- if (isPlainObject(value)) {
1555
- const keys = Object.keys(value);
1599
+ const actual = isForcedValue(value) ? value.value : value;
1600
+ if (isPlainObject(actual)) {
1601
+ const keys = Object.keys(actual);
1556
1602
  throw new ShapeError(
1557
1603
  `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.`
1558
1604
  );
1559
1605
  }
1560
- if (value === null || value === void 0) {
1606
+ if (actual === null || actual === void 0) {
1561
1607
  throw new ShapeError(
1562
- `Invalid unique where shape for "${model ?? "unknown"}.${field}". Unique fields must use true or a forced value.`
1608
+ `Invalid unique where shape for "${model ?? "unknown"}.${field}". Unique fields must use true or a forced non-null value.`
1563
1609
  );
1564
1610
  }
1565
1611
  }
@@ -1573,9 +1619,8 @@ function shapeCoversConstraint(where, constraint, model, typeMap) {
1573
1619
  }
1574
1620
  if (!(constraint.selector in where))
1575
1621
  return false;
1576
- const selectorValue = where[constraint.selector];
1577
- if (isForcedValue(selectorValue))
1578
- return true;
1622
+ const rawSelectorValue = where[constraint.selector];
1623
+ const selectorValue = isForcedValue(rawSelectorValue) ? rawSelectorValue.value : rawSelectorValue;
1579
1624
  if (!isPlainObject(selectorValue)) {
1580
1625
  throw new ShapeError(
1581
1626
  `Compound unique selector "${model}.${constraint.selector}" must be an object with fields: ${constraint.fields.join(", ")}`
@@ -2078,8 +2123,18 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2078
2123
  (constraint) => constraint.selector === selector
2079
2124
  ) ?? null;
2080
2125
  }
2081
- function parseForcedUniqueValue(model, fieldName, fieldMeta, value) {
2126
+ function buildUniqueWhereFieldSchema(fieldMeta, allowNullableFilter) {
2082
2127
  const schema = buildDirectScalarSchema(fieldMeta, enumMap, scalarBase);
2128
+ if (allowNullableFilter && !fieldMeta.isRequired) {
2129
+ return schema.nullable();
2130
+ }
2131
+ return schema;
2132
+ }
2133
+ function parseForcedUniqueValue(model, fieldName, fieldMeta, value, allowNullableFilter) {
2134
+ const schema = buildUniqueWhereFieldSchema(
2135
+ fieldMeta,
2136
+ allowNullableFilter
2137
+ );
2083
2138
  const actual = isForcedValue(value) ? value.value : value;
2084
2139
  try {
2085
2140
  return schema.parse(actual);
@@ -2157,10 +2212,9 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2157
2212
  `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.`
2158
2213
  );
2159
2214
  }
2160
- const directSchema2 = buildDirectScalarSchema(
2215
+ const directSchema2 = buildUniqueWhereFieldSchema(
2161
2216
  fieldMeta2,
2162
- enumMap,
2163
- scalarBase
2217
+ false
2164
2218
  );
2165
2219
  if (fieldValue === true) {
2166
2220
  nestedSchemas[fieldName] = directSchema2;
@@ -2169,7 +2223,8 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2169
2223
  model,
2170
2224
  fieldName,
2171
2225
  fieldMeta2,
2172
- fieldValue
2226
+ fieldValue,
2227
+ false
2173
2228
  );
2174
2229
  }
2175
2230
  }
@@ -2208,7 +2263,7 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2208
2263
  `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 }.`
2209
2264
  );
2210
2265
  }
2211
- const directSchema = buildDirectScalarSchema(fieldMeta, enumMap, scalarBase);
2266
+ const directSchema = buildUniqueWhereFieldSchema(fieldMeta, true);
2212
2267
  if (value === true) {
2213
2268
  fieldSchemas[key] = directSchema;
2214
2269
  continue;
@@ -2217,12 +2272,32 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2217
2272
  model,
2218
2273
  key,
2219
2274
  fieldMeta,
2220
- value
2275
+ value,
2276
+ true
2221
2277
  );
2222
2278
  forcedOnlyKeys.add(key);
2223
2279
  }
2280
+ const selectorKeys = Object.keys(fieldSchemas);
2281
+ let schema;
2282
+ if (selectorKeys.length === 0) {
2283
+ schema = null;
2284
+ } else if (selectorKeys.length === 1) {
2285
+ schema = import_zod5.z.object(fieldSchemas).strict();
2286
+ } else {
2287
+ const optionalSchemas = {};
2288
+ for (const [k, s] of Object.entries(fieldSchemas)) {
2289
+ optionalSchemas[k] = s.optional();
2290
+ }
2291
+ const objectSchema = import_zod5.z.object(optionalSchemas).strict();
2292
+ schema = Object.keys(forcedConditions).length > 0 ? objectSchema : objectSchema.refine(
2293
+ (v) => selectorKeys.some((k) => v[k] !== void 0),
2294
+ {
2295
+ message: `Unique where for model "${model}" must specify at least one selector: ${selectorKeys.join(", ")}`
2296
+ }
2297
+ );
2298
+ }
2224
2299
  return {
2225
- schema: Object.keys(fieldSchemas).length > 0 ? import_zod5.z.object(fieldSchemas).strict() : null,
2300
+ schema,
2226
2301
  forced: {
2227
2302
  conditions: forcedConditions,
2228
2303
  relations: {}
@@ -3570,7 +3645,21 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3570
3645
  built = builtCache.get(resolution.key);
3571
3646
  }
3572
3647
  }
3573
- return applyBuiltShape(built, normalizedBody, isUnique);
3648
+ const result = applyBuiltShape(
3649
+ built,
3650
+ normalizedBody,
3651
+ isUnique,
3652
+ model
3653
+ );
3654
+ if (isUnique && result.where) {
3655
+ validateResolvedUniqueWhere(
3656
+ model,
3657
+ result.where,
3658
+ method,
3659
+ uniqueMap
3660
+ );
3661
+ }
3662
+ return result;
3574
3663
  }
3575
3664
  };
3576
3665
  }
@@ -3669,6 +3758,12 @@ function pickMissingFksFromResult(result, fks) {
3669
3758
  return missing;
3670
3759
  }
3671
3760
  function validateScopeValue(root, value) {
3761
+ const t = typeof value;
3762
+ if (t !== "string" && t !== "number" && t !== "bigint") {
3763
+ throw new PolicyError(
3764
+ `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.`
3765
+ );
3766
+ }
3672
3767
  if (typeof value === "string" && value.length === 0) {
3673
3768
  throw new PolicyError(
3674
3769
  `Empty string scope value for root "${root}". This is almost certainly a bug in the context function.`
@@ -3711,10 +3806,12 @@ function enforceDataScope(data, scopes, overrides, log, model, operation, onScop
3711
3806
  }
3712
3807
  var VALID_FIND_UNIQUE_MODES = /* @__PURE__ */ new Set(["verify", "reject"]);
3713
3808
  var VALID_ON_SCOPE_RELATION_WRITES = /* @__PURE__ */ new Set(["error", "warn", "strip"]);
3809
+ var VALID_ON_MISSING_SCOPE_CONTEXT = /* @__PURE__ */ new Set(["error", "warn", "ignore"]);
3714
3810
  function createScopeExtension(scopeMap, contextFn, guardConfig, logger) {
3715
3811
  const log = logger ?? { warn: (msg) => console.warn(msg) };
3716
3812
  const findUniqueMode = guardConfig.findUniqueMode ?? "reject";
3717
3813
  const onScopeRelationWrite = guardConfig.onScopeRelationWrite ?? "error";
3814
+ const onMissingScopeContext = guardConfig.onMissingScopeContext ?? "error";
3718
3815
  if (!VALID_FIND_UNIQUE_MODES.has(findUniqueMode)) {
3719
3816
  throw new ShapeError(
3720
3817
  `prisma-guard: Invalid findUniqueMode "${findUniqueMode}". Allowed: ${[...VALID_FIND_UNIQUE_MODES].join(", ")}`
@@ -3725,6 +3822,11 @@ function createScopeExtension(scopeMap, contextFn, guardConfig, logger) {
3725
3822
  `prisma-guard: Invalid onScopeRelationWrite "${onScopeRelationWrite}". Allowed: ${[...VALID_ON_SCOPE_RELATION_WRITES].join(", ")}`
3726
3823
  );
3727
3824
  }
3825
+ if (!VALID_ON_MISSING_SCOPE_CONTEXT.has(onMissingScopeContext)) {
3826
+ throw new ShapeError(
3827
+ `prisma-guard: Invalid onMissingScopeContext "${onMissingScopeContext}". Allowed: ${[...VALID_ON_MISSING_SCOPE_CONTEXT].join(", ")}`
3828
+ );
3829
+ }
3728
3830
  const scopeFkSets = /* @__PURE__ */ new Map();
3729
3831
  for (const [model, entries] of Object.entries(scopeMap)) {
3730
3832
  const fks = /* @__PURE__ */ new Set();
@@ -3750,12 +3852,12 @@ function createScopeExtension(scopeMap, contextFn, guardConfig, logger) {
3750
3852
  const missingRoots = scopes.filter((s) => ctx[s.root] == null).map((s) => s.root);
3751
3853
  const isMutation = CREATE_OPS.has(operation) || UNIQUE_MUTATION_OPS.has(operation) || MULTI_MUTATION_OPS.has(operation) || operation === "upsert";
3752
3854
  if (missingRoots.length > 0) {
3753
- if (isMutation || guardConfig.onMissingScopeContext === "error") {
3855
+ if (isMutation || onMissingScopeContext === "error") {
3754
3856
  throw new PolicyError(
3755
3857
  `prisma-guard: Missing scope context for model "${model}": roots ${missingRoots.map((r) => `"${r}"`).join(", ")} not provided. All scope roots must be present.`
3756
3858
  );
3757
3859
  }
3758
- if (guardConfig.onMissingScopeContext === "warn") {
3860
+ if (onMissingScopeContext === "warn") {
3759
3861
  log.warn(
3760
3862
  `prisma-guard: Missing scope context for model "${model}": roots ${missingRoots.map((r) => `"${r}"`).join(", ")} not provided. Read proceeding with partial scope.`
3761
3863
  );