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

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
@@ -20,7 +20,7 @@ export { ValidationError, v } from '@lunora/values';
20
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-CWfMjb5Y.mjs';
23
+ export { mask } from './packem_shared/mask-E8MgAS3N.mjs';
24
24
  export { rls } from './packem_shared/rls-DhNgKFeW.mjs';
25
25
  export { storageRules } from './packem_shared/storageRules-Cje6Woea.mjs';
26
26
 
@@ -46,6 +46,29 @@ const maskRow = (row, columns, base) => {
46
46
  const maskPage = (page, columns, base) => {
47
47
  return { ...page, page: page.page.map((row) => maskRow(row, columns, base)) };
48
48
  };
49
+ const assertIndexFieldsAllowed = (builderCallback, columns, tableName, method) => {
50
+ if (typeof builderCallback !== "function") {
51
+ return;
52
+ }
53
+ const referenced = /* @__PURE__ */ new Set();
54
+ const makeRecorder = () => /* @__PURE__ */ new Proxy(
55
+ {},
56
+ {
57
+ get: () => (field) => {
58
+ if (typeof field === "string") {
59
+ referenced.add(field);
60
+ }
61
+ return makeRecorder();
62
+ }
63
+ }
64
+ );
65
+ builderCallback(makeRecorder());
66
+ for (const field of referenced) {
67
+ if (field in columns) {
68
+ throw new LunoraError("MASK_UNSUPPORTED", `${method}() filtering "${tableName}" by masked column "${field}" is not supported`);
69
+ }
70
+ }
71
+ };
49
72
  const isFacadeEntry = (value) => {
50
73
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
51
74
  return false;
@@ -54,7 +77,7 @@ const isFacadeEntry = (value) => {
54
77
  return typeof candidate["findMany"] === "function" && typeof candidate["withSearchIndex"] === "function";
55
78
  };
56
79
  const wrapDatabase = (base, perTable, context) => {
57
- const wrapReader = (reader, columns) => {
80
+ const wrapReader = (reader, columns, tableName) => {
58
81
  return {
59
82
  collect: async () => {
60
83
  const rows = await reader.collect();
@@ -67,13 +90,14 @@ const wrapDatabase = (base, perTable, context) => {
67
90
  // redacting masked cells the predicate can observe.
68
91
  filter: (predicate) => wrapReader(
69
92
  reader.filter((document) => predicate(maskRow(document, columns, context))),
70
- columns
93
+ columns,
94
+ tableName
71
95
  ),
72
96
  first: async () => {
73
97
  const row = await reader.first();
74
98
  return row ? maskRow(row, columns, context) : null;
75
99
  },
76
- order: (direction) => wrapReader(reader.order(direction), columns),
100
+ order: (direction) => wrapReader(reader.order(direction), columns, tableName),
77
101
  paginate: async (options) => maskPage(await reader.paginate(options), columns, context),
78
102
  take: async (limit) => {
79
103
  const rows = await reader.take(limit);
@@ -83,8 +107,19 @@ const wrapDatabase = (base, perTable, context) => {
83
107
  const row = await reader.unique();
84
108
  return row ? maskRow(row, columns, context) : null;
85
109
  },
86
- withIndex: (indexName, range) => wrapReader(reader.withIndex(indexName, range), columns),
87
- withSearchIndex: (indexName, search) => wrapReader(reader.withSearchIndex(indexName, search), columns)
110
+ // SECURITY (value oracle): reject before delegating when the range /
111
+ // search references a masked column — an index range or search term
112
+ // over a masked column is the same value oracle as a masked-column
113
+ // `where`, so it must fail closed (see `assertIndexFieldsAllowed`).
114
+ // Reads over NON-masked columns pass through and still mask output.
115
+ withIndex: (indexName, range) => {
116
+ assertIndexFieldsAllowed(range, columns, tableName, "withIndex");
117
+ return wrapReader(reader.withIndex(indexName, range), columns, tableName);
118
+ },
119
+ withSearchIndex: (indexName, search) => {
120
+ assertIndexFieldsAllowed(search, columns, tableName, "withSearchIndex");
121
+ return wrapReader(reader.withSearchIndex(indexName, search), columns, tableName);
122
+ }
88
123
  };
89
124
  };
90
125
  const locate = async (id, expectedTable) => {
@@ -201,7 +236,7 @@ const wrapDatabase = (base, perTable, context) => {
201
236
  query(tableName) {
202
237
  const reader = base.query(tableName);
203
238
  const columns = perTable.get(tableName);
204
- return columns ? wrapReader(reader, columns) : reader;
239
+ return columns ? wrapReader(reader, columns, tableName) : reader;
205
240
  },
206
241
  async rankPage(tableName, indexName, options) {
207
242
  const page = await base.rankPage(tableName, indexName, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/server",
3
- "version": "1.0.0-alpha.12",
3
+ "version": "1.0.0-alpha.13",
4
4
  "description": "Server primitives for Lunora: defineSchema, defineTable, query, mutation, and action",
5
5
  "keywords": [
6
6
  "backend",