@lunora/server 1.0.0-alpha.11 → 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 +3 -3
- package/dist/packem_shared/{buildRlsReadRegistry-1jexWrb3.mjs → buildRlsReadRegistry-Do1CSBTr.mjs} +1 -1
- package/dist/packem_shared/{mask-BV_jNzsN.mjs → mask-E8MgAS3N.mjs} +87 -7
- package/dist/packem_shared/{rls-2Jhd0uev.mjs → rls-DhNgKFeW.mjs} +6 -3
- package/dist/rls/testing.mjs +1 -1
- package/package.json +1 -1
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-
|
|
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-
|
|
24
|
-
export { rls } from './packem_shared/rls-
|
|
23
|
+
export { mask } from './packem_shared/mask-E8MgAS3N.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";
|
package/dist/packem_shared/{buildRlsReadRegistry-1jexWrb3.mjs → buildRlsReadRegistry-Do1CSBTr.mjs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { indexRolePermissions, computeReadBaseWhere, permissionName } from './rls-
|
|
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: [] };
|
|
@@ -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,21 +77,27 @@ 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();
|
|
61
84
|
return rows.map((row) => maskRow(row, columns, context));
|
|
62
85
|
},
|
|
86
|
+
// SECURITY (value oracle): the predicate must see the MASKED row, not
|
|
87
|
+
// the raw stored row — otherwise a caller can `.filter(d => d.ssn ===
|
|
88
|
+
// guess)` to read the value the mask hides. Masking before the
|
|
89
|
+
// predicate keeps filtering on non-masked columns working while
|
|
90
|
+
// redacting masked cells the predicate can observe.
|
|
63
91
|
filter: (predicate) => wrapReader(
|
|
64
|
-
reader.filter((document) => predicate(document)),
|
|
65
|
-
columns
|
|
92
|
+
reader.filter((document) => predicate(maskRow(document, columns, context))),
|
|
93
|
+
columns,
|
|
94
|
+
tableName
|
|
66
95
|
),
|
|
67
96
|
first: async () => {
|
|
68
97
|
const row = await reader.first();
|
|
69
98
|
return row ? maskRow(row, columns, context) : null;
|
|
70
99
|
},
|
|
71
|
-
order: (direction) => wrapReader(reader.order(direction), columns),
|
|
100
|
+
order: (direction) => wrapReader(reader.order(direction), columns, tableName),
|
|
72
101
|
paginate: async (options) => maskPage(await reader.paginate(options), columns, context),
|
|
73
102
|
take: async (limit) => {
|
|
74
103
|
const rows = await reader.take(limit);
|
|
@@ -78,8 +107,19 @@ const wrapDatabase = (base, perTable, context) => {
|
|
|
78
107
|
const row = await reader.unique();
|
|
79
108
|
return row ? maskRow(row, columns, context) : null;
|
|
80
109
|
},
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
}
|
|
83
123
|
};
|
|
84
124
|
};
|
|
85
125
|
const locate = async (id, expectedTable) => {
|
|
@@ -120,23 +160,63 @@ const wrapDatabase = (base, perTable, context) => {
|
|
|
120
160
|
throw new LunoraError("MASK_UNSUPPORTED", `${method}() over masked column "${offending}" on "${tableName}" is not supported`);
|
|
121
161
|
}
|
|
122
162
|
};
|
|
163
|
+
const collectWhereFields = (where, into) => {
|
|
164
|
+
if (!where || typeof where !== "object" || Array.isArray(where)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
for (const [key, value] of Object.entries(where)) {
|
|
168
|
+
if (key === "AND" || key === "OR") {
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
for (const clause of value) {
|
|
171
|
+
collectWhereFields(clause, into);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else if (key === "NOT") {
|
|
175
|
+
collectWhereFields(value, into);
|
|
176
|
+
} else if (!key.startsWith("__")) {
|
|
177
|
+
into.add(key);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const assertWhereAllowed = (tableName, where, method) => {
|
|
182
|
+
const columns = perTable.get(tableName);
|
|
183
|
+
if (!columns || where === void 0) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
187
|
+
collectWhereFields(where, referenced);
|
|
188
|
+
for (const field of referenced) {
|
|
189
|
+
if (field in columns) {
|
|
190
|
+
throw new LunoraError("MASK_UNSUPPORTED", `${method}() filtering "${tableName}" by masked column "${field}" is not supported`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
123
194
|
const wrapped = {
|
|
124
195
|
...base,
|
|
125
196
|
aggregate(tableName, options) {
|
|
126
197
|
assertReductionAllowed(tableName, [options.field], "aggregate");
|
|
127
198
|
return base.aggregate(tableName, options);
|
|
128
199
|
},
|
|
200
|
+
count(tableName, whereOrArgs) {
|
|
201
|
+
const wrapper = whereOrArgs && typeof whereOrArgs === "object" && !Array.isArray(whereOrArgs) ? whereOrArgs : void 0;
|
|
202
|
+
const where = wrapper && ("where" in wrapper || "baseWhere" in wrapper || "restrictsCounts" in wrapper) ? wrapper.where : whereOrArgs;
|
|
203
|
+
assertWhereAllowed(tableName, where, "count");
|
|
204
|
+
return base.count(tableName, whereOrArgs);
|
|
205
|
+
},
|
|
129
206
|
async findFirst(tableName, args) {
|
|
207
|
+
assertWhereAllowed(tableName, args?.where, "findFirst");
|
|
130
208
|
const row = await base.findFirst(tableName, args);
|
|
131
209
|
const columns = perTable.get(tableName);
|
|
132
210
|
return row && columns ? maskRow(row, columns, context) : row;
|
|
133
211
|
},
|
|
134
212
|
async findFirstOrThrow(tableName, args) {
|
|
213
|
+
assertWhereAllowed(tableName, args?.where, "findFirstOrThrow");
|
|
135
214
|
const row = await base.findFirstOrThrow(tableName, args);
|
|
136
215
|
const columns = perTable.get(tableName);
|
|
137
216
|
return columns ? maskRow(row, columns, context) : row;
|
|
138
217
|
},
|
|
139
218
|
async findMany(tableName, args) {
|
|
219
|
+
assertWhereAllowed(tableName, args?.where, "findMany");
|
|
140
220
|
const page = await base.findMany(tableName, args);
|
|
141
221
|
const columns = perTable.get(tableName);
|
|
142
222
|
return columns ? maskPage(page, columns, context) : page;
|
|
@@ -156,7 +236,7 @@ const wrapDatabase = (base, perTable, context) => {
|
|
|
156
236
|
query(tableName) {
|
|
157
237
|
const reader = base.query(tableName);
|
|
158
238
|
const columns = perTable.get(tableName);
|
|
159
|
-
return columns ? wrapReader(reader, columns) : reader;
|
|
239
|
+
return columns ? wrapReader(reader, columns, tableName) : reader;
|
|
160
240
|
},
|
|
161
241
|
async rankPage(tableName, indexName, options) {
|
|
162
242
|
const page = await base.rankPage(tableName, indexName, options);
|
|
@@ -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}
|
|
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
|
|
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 } });
|
package/dist/rls/testing.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-
|
|
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);
|