@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-
|
|
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
|
-
|
|
87
|
-
|
|
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);
|