@lunora/server 1.0.0-alpha.11 → 1.0.0-alpha.12

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
@@ -17,11 +17,11 @@ export { defineShape } from './packem_shared/defineShape-CJ27Wx7o.mjs';
17
17
  export { anyApi } from './types.mjs';
18
18
  export { cronJobs } from '@lunora/scheduler';
19
19
  export { ValidationError, v } from '@lunora/values';
20
- export { buildRlsReadRegistry, composeShapeReadWhere } from './packem_shared/buildRlsReadRegistry-1jexWrb3.mjs';
20
+ export { buildRlsReadRegistry, composeShapeReadWhere } from './packem_shared/buildRlsReadRegistry-Do1CSBTr.mjs';
21
21
  export { createPolicyDsl, definePermission, definePolicies, definePolicy, defineRole } from './packem_shared/createPolicyDsl-De67zPDS.mjs';
22
22
  export { defineStorageRule, defineStorageRules } from './packem_shared/defineStorageRule-qu0mpilX.mjs';
23
- export { mask } from './packem_shared/mask-BV_jNzsN.mjs';
24
- export { rls } from './packem_shared/rls-2Jhd0uev.mjs';
23
+ export { mask } from './packem_shared/mask-CWfMjb5Y.mjs';
24
+ export { rls } from './packem_shared/rls-DhNgKFeW.mjs';
25
25
  export { storageRules } from './packem_shared/storageRules-Cje6Woea.mjs';
26
26
 
27
27
  const VERSION = "0.0.0";
@@ -1,4 +1,4 @@
1
- import { indexRolePermissions, computeReadBaseWhere, permissionName } from './rls-2Jhd0uev.mjs';
1
+ import { indexRolePermissions, computeReadBaseWhere, permissionName } from './rls-DhNgKFeW.mjs';
2
2
  import { r as readRlsTag } from './policy-tag-DvpVH2tv.mjs';
3
3
 
4
4
  const FALSE_PREDICATE = { OR: [] };
@@ -60,8 +60,13 @@ const wrapDatabase = (base, perTable, context) => {
60
60
  const rows = await reader.collect();
61
61
  return rows.map((row) => maskRow(row, columns, context));
62
62
  },
63
+ // SECURITY (value oracle): the predicate must see the MASKED row, not
64
+ // the raw stored row — otherwise a caller can `.filter(d => d.ssn ===
65
+ // guess)` to read the value the mask hides. Masking before the
66
+ // predicate keeps filtering on non-masked columns working while
67
+ // redacting masked cells the predicate can observe.
63
68
  filter: (predicate) => wrapReader(
64
- reader.filter((document) => predicate(document)),
69
+ reader.filter((document) => predicate(maskRow(document, columns, context))),
65
70
  columns
66
71
  ),
67
72
  first: async () => {
@@ -120,23 +125,63 @@ const wrapDatabase = (base, perTable, context) => {
120
125
  throw new LunoraError("MASK_UNSUPPORTED", `${method}() over masked column "${offending}" on "${tableName}" is not supported`);
121
126
  }
122
127
  };
128
+ const collectWhereFields = (where, into) => {
129
+ if (!where || typeof where !== "object" || Array.isArray(where)) {
130
+ return;
131
+ }
132
+ for (const [key, value] of Object.entries(where)) {
133
+ if (key === "AND" || key === "OR") {
134
+ if (Array.isArray(value)) {
135
+ for (const clause of value) {
136
+ collectWhereFields(clause, into);
137
+ }
138
+ }
139
+ } else if (key === "NOT") {
140
+ collectWhereFields(value, into);
141
+ } else if (!key.startsWith("__")) {
142
+ into.add(key);
143
+ }
144
+ }
145
+ };
146
+ const assertWhereAllowed = (tableName, where, method) => {
147
+ const columns = perTable.get(tableName);
148
+ if (!columns || where === void 0) {
149
+ return;
150
+ }
151
+ const referenced = /* @__PURE__ */ new Set();
152
+ collectWhereFields(where, referenced);
153
+ for (const field of referenced) {
154
+ if (field in columns) {
155
+ throw new LunoraError("MASK_UNSUPPORTED", `${method}() filtering "${tableName}" by masked column "${field}" is not supported`);
156
+ }
157
+ }
158
+ };
123
159
  const wrapped = {
124
160
  ...base,
125
161
  aggregate(tableName, options) {
126
162
  assertReductionAllowed(tableName, [options.field], "aggregate");
127
163
  return base.aggregate(tableName, options);
128
164
  },
165
+ count(tableName, whereOrArgs) {
166
+ const wrapper = whereOrArgs && typeof whereOrArgs === "object" && !Array.isArray(whereOrArgs) ? whereOrArgs : void 0;
167
+ const where = wrapper && ("where" in wrapper || "baseWhere" in wrapper || "restrictsCounts" in wrapper) ? wrapper.where : whereOrArgs;
168
+ assertWhereAllowed(tableName, where, "count");
169
+ return base.count(tableName, whereOrArgs);
170
+ },
129
171
  async findFirst(tableName, args) {
172
+ assertWhereAllowed(tableName, args?.where, "findFirst");
130
173
  const row = await base.findFirst(tableName, args);
131
174
  const columns = perTable.get(tableName);
132
175
  return row && columns ? maskRow(row, columns, context) : row;
133
176
  },
134
177
  async findFirstOrThrow(tableName, args) {
178
+ assertWhereAllowed(tableName, args?.where, "findFirstOrThrow");
135
179
  const row = await base.findFirstOrThrow(tableName, args);
136
180
  const columns = perTable.get(tableName);
137
181
  return columns ? maskRow(row, columns, context) : row;
138
182
  },
139
183
  async findMany(tableName, args) {
184
+ assertWhereAllowed(tableName, args?.where, "findMany");
140
185
  const page = await base.findMany(tableName, args);
141
186
  const columns = perTable.get(tableName);
142
187
  return columns ? maskPage(page, columns, context) : page;
@@ -245,7 +245,6 @@ const isFacadeEntry = (value) => {
245
245
  return typeof candidate["findMany"] === "function" && typeof candidate["withSearchIndex"] === "function";
246
246
  };
247
247
  const wrapDatabase = (base, raw, perTable, context) => {
248
- const route = (tableName) => perTable.has(tableName) ? raw : base;
249
248
  const readBaseCache = /* @__PURE__ */ new Map();
250
249
  const readBase = (tableName) => {
251
250
  const cached = readBaseCache.get(tableName);
@@ -263,6 +262,7 @@ const wrapDatabase = (base, raw, perTable, context) => {
263
262
  readBaseCache.set(tableName, result);
264
263
  return result;
265
264
  };
265
+ const route = (tableName) => readBase(tableName).restricts ? raw : base;
266
266
  const relationReadFilter = (table) => readBase(table).baseWhere;
267
267
  const locateRow = async (id, expectedTable) => {
268
268
  if (raw.lookupById) {
@@ -300,7 +300,7 @@ const wrapDatabase = (base, raw, perTable, context) => {
300
300
  const nextRow = computeNextRow ? computeNextRow(located.row) : void 0;
301
301
  const writeOk = evaluateWrite(policies, op, { ...context, row: located.row }, nextRow);
302
302
  if (!writeOk) {
303
- throw new LunoraError("FORBIDDEN", `${op} on "${located.tableName}" denied by policy`);
303
+ throw new LunoraError("FORBIDDEN", `${op} denied by policy`);
304
304
  }
305
305
  }
306
306
  return perform(raw);
@@ -366,7 +366,10 @@ const wrapDatabase = (base, raw, perTable, context) => {
366
366
  return base.get(id, expectedTable);
367
367
  }
368
368
  const { baseWhere, restricts } = readBase(located.tableName);
369
- if (!restricts || !baseWhere) {
369
+ if (!restricts) {
370
+ return base.get(id, expectedTable);
371
+ }
372
+ if (!baseWhere) {
370
373
  return located.row;
371
374
  }
372
375
  const allowed = await raw.findFirst(located.tableName, { baseWhere, limit: 1, where: { _id: id } });
@@ -1,4 +1,4 @@
1
- import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-2Jhd0uev.mjs';
1
+ import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-DhNgKFeW.mjs';
2
2
 
3
3
  const expectPolicy = (policies, options = {}) => {
4
4
  const rolePermissions = indexRolePermissions(options.roles);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/server",
3
- "version": "1.0.0-alpha.11",
3
+ "version": "1.0.0-alpha.12",
4
4
  "description": "Server primitives for Lunora: defineSchema, defineTable, query, mutation, and action",
5
5
  "keywords": [
6
6
  "backend",