@objectstack/plugin-security 9.8.0 → 9.9.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.
package/dist/index.mjs CHANGED
@@ -2261,17 +2261,29 @@ var defaultPermissionSets = [
2261
2261
  operation: "all",
2262
2262
  using: "organization_id = current_user.organization_id"
2263
2263
  },
2264
+ // Owner-scoped writes/deletes for rank-and-file members: you may modify
2265
+ // and delete the records you created, not other users'. Keyed on
2266
+ // `created_by` — the column the engine stamps on EVERY record — rather
2267
+ // than `owner_id`, which author-defined objects almost never declare. The
2268
+ // old `owner_id` key referenced a missing column on real objects, so
2269
+ // `computeRlsFilter` dropped the policy and the scoping silently no-op'd
2270
+ // (any member could edit/delete any record — #1985). These policies are
2271
+ // ENFORCED on writes via the security middleware's pre-image check (a
2272
+ // by-id update/delete never builds an RLS `where`, so the predicate is
2273
+ // verified against the target row before the mutation). Objects that
2274
+ // model transferable ownership with a dedicated owner field should
2275
+ // override these with a per-object policy.
2264
2276
  {
2265
2277
  name: "owner_only_writes",
2266
2278
  object: "*",
2267
2279
  operation: "update",
2268
- using: "owner_id = current_user.id"
2280
+ using: "created_by = current_user.id"
2269
2281
  },
2270
2282
  {
2271
2283
  name: "owner_only_deletes",
2272
2284
  object: "*",
2273
2285
  operation: "delete",
2274
- using: "owner_id = current_user.id"
2286
+ using: "created_by = current_user.id"
2275
2287
  },
2276
2288
  // ── better-auth system tables that lack `organization_id` and would
2277
2289
  // otherwise be left unprotected by the wildcard rule above. ────
@@ -2703,6 +2715,40 @@ var SecurityPlugin = class {
2703
2715
  );
2704
2716
  }
2705
2717
  }
2718
+ if ((opCtx.operation === "update" || opCtx.operation === "delete") && permissionSets.length > 0 && !!opCtx.context?.userId && this.ql) {
2719
+ const targetId = this.extractSingleId(opCtx);
2720
+ if (targetId != null) {
2721
+ const writeFilter = await this.computeRlsFilter(
2722
+ permissionSets,
2723
+ opCtx.object,
2724
+ opCtx.operation,
2725
+ opCtx.context
2726
+ );
2727
+ if (writeFilter) {
2728
+ let visible = null;
2729
+ try {
2730
+ visible = await this.ql.findOne(opCtx.object, {
2731
+ where: { $and: [{ id: targetId }, writeFilter] },
2732
+ context: opCtx.context
2733
+ });
2734
+ } catch {
2735
+ visible = null;
2736
+ }
2737
+ if (!visible) {
2738
+ throw new PermissionDeniedError(
2739
+ `[Security] Access denied: not permitted to ${opCtx.operation} this '${opCtx.object}' record (row-level security)`,
2740
+ {
2741
+ operation: opCtx.operation,
2742
+ object: opCtx.object,
2743
+ roles,
2744
+ permissionSets: explicitPermissionSets,
2745
+ recordId: targetId
2746
+ }
2747
+ );
2748
+ }
2749
+ }
2750
+ }
2751
+ }
2706
2752
  if ((opCtx.operation === "insert" || opCtx.operation === "update") && opCtx.data && permissionSets.length > 0) {
2707
2753
  const fieldPerms = this.permissionEvaluator.getFieldPermissions(
2708
2754
  opCtx.object,
@@ -2894,6 +2940,26 @@ var SecurityPlugin = class {
2894
2940
  }
2895
2941
  return permissionSets;
2896
2942
  }
2943
+ /**
2944
+ * Resolve a single scalar primary-key id from an update/delete operation
2945
+ * context, mirroring the engine's "single-id vs predicate" rule
2946
+ * (`engine.ts` update/delete): only a scalar `data.id` or `where.id`
2947
+ * identifies one row. An operator object (`{ $in: [...] }`, …) is a
2948
+ * multi-row predicate and returns `null` (multi-row writes route through the
2949
+ * `*Many` paths, out of scope for the by-id pre-image check).
2950
+ */
2951
+ extractSingleId(opCtx) {
2952
+ const isScalar = (v) => v !== null && (typeof v === "string" || typeof v === "number" || typeof v === "bigint");
2953
+ const data = opCtx?.data;
2954
+ if (data && typeof data === "object" && !Array.isArray(data) && isScalar(data.id)) {
2955
+ return data.id;
2956
+ }
2957
+ const where = opCtx?.options?.where;
2958
+ if (where && typeof where === "object" && "id" in where && isScalar(where.id)) {
2959
+ return where.id;
2960
+ }
2961
+ return null;
2962
+ }
2897
2963
  /**
2898
2964
  * Compile the applicable RLS policies for (object, operation) into a single
2899
2965
  * `FilterCondition`, applying the field-existence safety net (wildcard