@opensaas/stack-core 0.20.1 → 0.22.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +334 -0
- package/CLAUDE.md +29 -11
- package/dist/access/access-filter.d.ts +29 -0
- package/dist/access/access-filter.d.ts.map +1 -0
- package/dist/access/access-filter.js +68 -0
- package/dist/access/access-filter.js.map +1 -0
- package/dist/access/engine.d.ts +15 -48
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +14 -280
- package/dist/access/engine.js.map +1 -1
- package/dist/access/field-access.d.ts +44 -0
- package/dist/access/field-access.d.ts.map +1 -0
- package/dist/access/field-access.js +123 -0
- package/dist/access/field-access.js.map +1 -0
- package/dist/access/field-access.test.d.ts +2 -0
- package/dist/access/field-access.test.d.ts.map +1 -0
- package/dist/access/{engine.test.js → field-access.test.js} +2 -2
- package/dist/access/field-access.test.js.map +1 -0
- package/dist/access/field-visibility.d.ts +13 -0
- package/dist/access/field-visibility.d.ts.map +1 -0
- package/dist/access/field-visibility.js +178 -0
- package/dist/access/field-visibility.js.map +1 -0
- package/dist/access/index.d.ts +4 -1
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +8 -1
- package/dist/access/index.js.map +1 -1
- package/dist/access/multi-column-read-write.test.d.ts +2 -0
- package/dist/access/multi-column-read-write.test.d.ts.map +1 -0
- package/dist/access/multi-column-read-write.test.js +149 -0
- package/dist/access/multi-column-read-write.test.js.map +1 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/types.d.ts +334 -5
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/hook-pipeline.d.ts +49 -0
- package/dist/context/hook-pipeline.d.ts.map +1 -0
- package/dist/context/hook-pipeline.js +75 -0
- package/dist/context/hook-pipeline.js.map +1 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +30 -462
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +72 -68
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +158 -0
- package/dist/context/write-pipeline.d.ts.map +1 -0
- package/dist/context/write-pipeline.js +306 -0
- package/dist/context/write-pipeline.js.map +1 -0
- package/dist/extend.d.ts +3 -0
- package/dist/extend.d.ts.map +1 -0
- package/dist/extend.js +10 -0
- package/dist/extend.js.map +1 -0
- package/dist/fields/format-prisma-default.d.ts +35 -0
- package/dist/fields/format-prisma-default.d.ts.map +1 -0
- package/dist/fields/format-prisma-default.js +52 -0
- package/dist/fields/format-prisma-default.js.map +1 -0
- package/dist/fields/format-prisma-default.test.d.ts +2 -0
- package/dist/fields/format-prisma-default.test.d.ts.map +1 -0
- package/dist/fields/format-prisma-default.test.js +54 -0
- package/dist/fields/format-prisma-default.test.js.map +1 -0
- package/dist/fields/index.d.ts +1 -0
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +267 -18
- package/dist/fields/index.js.map +1 -1
- package/dist/fields/select.test.js +85 -0
- package/dist/fields/select.test.js.map +1 -1
- package/dist/fields/text-keystone-compat.test.d.ts +2 -0
- package/dist/fields/text-keystone-compat.test.d.ts.map +1 -0
- package/dist/fields/text-keystone-compat.test.js +93 -0
- package/dist/fields/text-keystone-compat.test.js.map +1 -0
- package/dist/hooks/index.d.ts +20 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +246 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -9
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +33 -0
- package/dist/index.test.js.map +1 -0
- package/dist/internal.d.ts +8 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +16 -0
- package/dist/internal.js.map +1 -0
- package/dist/mcp/handler.js +0 -1
- package/dist/mcp/handler.js.map +1 -1
- package/dist/validation/field-config.d.ts +55 -0
- package/dist/validation/field-config.d.ts.map +1 -0
- package/dist/validation/field-config.js +100 -0
- package/dist/validation/field-config.js.map +1 -0
- package/dist/validation/field-config.test.d.ts +2 -0
- package/dist/validation/field-config.test.d.ts.map +1 -0
- package/dist/validation/field-config.test.js +159 -0
- package/dist/validation/field-config.test.js.map +1 -0
- package/package.json +11 -3
- package/src/access/access-filter.ts +97 -0
- package/src/access/engine.ts +13 -396
- package/src/access/{engine.test.ts → field-access.test.ts} +1 -1
- package/src/access/field-access.ts +159 -0
- package/src/access/field-visibility.ts +269 -0
- package/src/access/index.ts +7 -4
- package/src/access/multi-column-read-write.test.ts +255 -0
- package/src/config/index.ts +3 -0
- package/src/config/types.ts +342 -4
- package/src/context/hook-pipeline.ts +160 -0
- package/src/context/index.ts +29 -667
- package/src/context/nested-operations.ts +142 -111
- package/src/context/write-pipeline.ts +543 -0
- package/src/extend.ts +19 -0
- package/src/fields/format-prisma-default.test.ts +64 -0
- package/src/fields/format-prisma-default.ts +67 -0
- package/src/fields/index.ts +375 -20
- package/src/fields/select.test.ts +99 -0
- package/src/fields/text-keystone-compat.test.ts +126 -0
- package/src/hooks/index.ts +270 -0
- package/src/index.test.ts +50 -0
- package/src/index.ts +35 -82
- package/src/internal.ts +49 -0
- package/src/mcp/handler.ts +0 -2
- package/src/validation/field-config.test.ts +199 -0
- package/src/validation/field-config.ts +145 -0
- package/tests/access-relationships.test.ts +4 -4
- package/tests/access.test.ts +1 -1
- package/tests/field-hooks.test.ts +410 -0
- package/tests/field-types.test.ts +1 -1
- package/tests/hook-pipeline.test.ts +233 -0
- package/tests/nested-operation-registry.test.ts +206 -0
- package/tests/write-pipeline.test.ts +588 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +43 -1
- package/dist/access/engine.test.d.ts +0 -2
- package/dist/access/engine.test.d.ts.map +0 -1
- package/dist/access/engine.test.js.map +0 -1
package/dist/access/engine.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access engine — operation-level access control and shared helpers.
|
|
3
|
+
*
|
|
4
|
+
* This module holds the *operation-level* (list-level) access primitives and
|
|
5
|
+
* the ref-parsing helper shared across both phases of the two-phase read:
|
|
6
|
+
*
|
|
7
|
+
* - Phase 1, Access Filter (pre-query row/relation scoping): `access-filter.ts`
|
|
8
|
+
* - Phase 2, Field Visibility (post-query field stripping + resolveOutput +
|
|
9
|
+
* virtual fields): `field-visibility.ts`
|
|
10
|
+
*
|
|
11
|
+
* Field-level access evaluation is centralized in `field-access.ts`
|
|
12
|
+
* (`checkFieldAccess`). See `docs/adr/0001-access-control-is-a-two-phase-read.md`
|
|
13
|
+
* and the access-control glossary in `CONTEXT.md`.
|
|
14
|
+
*/
|
|
1
15
|
/**
|
|
2
16
|
* Check if access control result is a boolean
|
|
3
17
|
*/
|
|
@@ -64,284 +78,4 @@ export function mergeFilters(userFilter, accessFilter) {
|
|
|
64
78
|
AND: [accessFilter, userFilter],
|
|
65
79
|
};
|
|
66
80
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Check field-level access for a specific operation
|
|
69
|
-
*/
|
|
70
|
-
export async function checkFieldAccess(fieldAccess, operation, args) {
|
|
71
|
-
// Skip access check in sudo mode
|
|
72
|
-
if (args.context._isSudo) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
if (!fieldAccess) {
|
|
76
|
-
return true; // No field access means allow
|
|
77
|
-
}
|
|
78
|
-
const accessControl = fieldAccess[operation];
|
|
79
|
-
if (!accessControl) {
|
|
80
|
-
return true; // No specific access control means allow
|
|
81
|
-
}
|
|
82
|
-
const result = await accessControl({
|
|
83
|
-
session: args.session,
|
|
84
|
-
item: args.item,
|
|
85
|
-
context: args.context,
|
|
86
|
-
inputData: args.inputData,
|
|
87
|
-
operation,
|
|
88
|
-
});
|
|
89
|
-
// If result is false, deny access
|
|
90
|
-
if (result === false) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
// If result is true, allow access
|
|
94
|
-
if (result === true) {
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
// Default to allowing access if we can't determine
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Simple filter matching for field-level access
|
|
102
|
-
* Checks if an item matches a Prisma-like filter object
|
|
103
|
-
*/
|
|
104
|
-
function matchesFilter(item, filter) {
|
|
105
|
-
for (const [key, condition] of Object.entries(filter)) {
|
|
106
|
-
if (typeof condition === 'object' && condition !== null) {
|
|
107
|
-
// Handle nested conditions like { equals: value }
|
|
108
|
-
if ('equals' in condition) {
|
|
109
|
-
if (item[key] !== condition.equals) {
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
else if ('not' in condition) {
|
|
114
|
-
if (item[key] === condition.not) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
// Add more condition types as needed
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
// Direct equality check
|
|
122
|
-
if (item[key] !== condition) {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Build Prisma include object with access control filters
|
|
131
|
-
* This allows us to filter relationships at the database level instead of in memory
|
|
132
|
-
*/
|
|
133
|
-
export async function buildIncludeWithAccessControl(fieldConfigs, args, config, depth = 0) {
|
|
134
|
-
const MAX_DEPTH = 5;
|
|
135
|
-
if (depth >= MAX_DEPTH) {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
// Skip auto-including relationships when inside a resolveOutput hook
|
|
139
|
-
// This prevents infinite loops when hooks make DB queries that include
|
|
140
|
-
// relationships back to the same entity (e.g., User virtual field queries Posts
|
|
141
|
-
// which includes author back to User, triggering the virtual field again)
|
|
142
|
-
if (args.context._resolveOutputCounter.depth > 0) {
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
const include = {};
|
|
146
|
-
let hasRelationships = false;
|
|
147
|
-
for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
|
|
148
|
-
if (fieldConfig?.type === 'relationship' && 'ref' in fieldConfig && fieldConfig.ref) {
|
|
149
|
-
hasRelationships = true;
|
|
150
|
-
const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
|
|
151
|
-
if (relatedConfig) {
|
|
152
|
-
// Check query access for the related list
|
|
153
|
-
const queryAccess = relatedConfig.listConfig.access?.operation?.query;
|
|
154
|
-
const accessResult = await checkAccess(queryAccess, {
|
|
155
|
-
session: args.session,
|
|
156
|
-
context: args.context,
|
|
157
|
-
});
|
|
158
|
-
// If access is completely denied, exclude this relationship
|
|
159
|
-
if (accessResult === false) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
// Build the include entry
|
|
163
|
-
const includeEntry = {};
|
|
164
|
-
// If access returns a filter, add it to the where clause
|
|
165
|
-
if (typeof accessResult === 'object') {
|
|
166
|
-
includeEntry.where = accessResult;
|
|
167
|
-
}
|
|
168
|
-
// Recursively build nested includes
|
|
169
|
-
const nestedInclude = await buildIncludeWithAccessControl(relatedConfig.listConfig.fields, args, config, depth + 1);
|
|
170
|
-
if (nestedInclude && Object.keys(nestedInclude).length > 0) {
|
|
171
|
-
includeEntry.include = nestedInclude;
|
|
172
|
-
}
|
|
173
|
-
// Add to include object
|
|
174
|
-
include[fieldName] = Object.keys(includeEntry).length > 0 ? includeEntry : true;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return hasRelationships ? include : undefined;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Filter fields from an object based on read access
|
|
182
|
-
* Recursively applies access control to nested relationships
|
|
183
|
-
*/
|
|
184
|
-
export async function filterReadableFields(item, fieldConfigs, args, config, depth = 0, listKey) {
|
|
185
|
-
const filtered = {};
|
|
186
|
-
const MAX_DEPTH = 5; // Prevent infinite recursion
|
|
187
|
-
// Process existing fields from the database result
|
|
188
|
-
for (const [fieldName, value] of Object.entries(item)) {
|
|
189
|
-
const fieldConfig = fieldConfigs[fieldName];
|
|
190
|
-
// Always include id, createdAt, updatedAt
|
|
191
|
-
if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) {
|
|
192
|
-
filtered[fieldName] = value;
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
// Check field access (checkFieldAccess already handles sudo mode)
|
|
196
|
-
const canRead = await checkFieldAccess(fieldConfig?.access, 'read', {
|
|
197
|
-
...args,
|
|
198
|
-
item,
|
|
199
|
-
});
|
|
200
|
-
if (!canRead) {
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
// Handle relationship fields - recursively filter fields within related items
|
|
204
|
-
// Note: Access control filtering is now done at database level via buildIncludeWithAccessControl
|
|
205
|
-
// This only handles field-level access (hiding sensitive fields)
|
|
206
|
-
if (config &&
|
|
207
|
-
fieldConfig?.type === 'relationship' &&
|
|
208
|
-
'ref' in fieldConfig &&
|
|
209
|
-
fieldConfig.ref &&
|
|
210
|
-
value !== null &&
|
|
211
|
-
value !== undefined &&
|
|
212
|
-
depth < MAX_DEPTH) {
|
|
213
|
-
const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
|
|
214
|
-
if (relatedConfig) {
|
|
215
|
-
// For many relationships (arrays) - recursively filter fields in each item
|
|
216
|
-
// The recursive call already handles applying resolveOutput hooks
|
|
217
|
-
if (Array.isArray(value)) {
|
|
218
|
-
filtered[fieldName] = await Promise.all(value.map((relatedItem) => filterReadableFields(relatedItem, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName)));
|
|
219
|
-
}
|
|
220
|
-
// For single relationships (objects) - recursively filter fields
|
|
221
|
-
// The recursive call already handles applying resolveOutput hooks
|
|
222
|
-
else if (typeof value === 'object') {
|
|
223
|
-
filtered[fieldName] = await filterReadableFields(value, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
// Related config not found, include the value as-is
|
|
228
|
-
filtered[fieldName] = value;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
// Non-relationship field or no config provided - apply resolveOutput hook if present
|
|
233
|
-
if (fieldConfig?.hooks?.resolveOutput && listKey) {
|
|
234
|
-
// Cast to runtime type for generic execution
|
|
235
|
-
// At runtime, the hook will receive the correct value type for the field
|
|
236
|
-
const hook = fieldConfig.hooks.resolveOutput;
|
|
237
|
-
// Increment depth counter to prevent infinite loops from hooks making DB queries
|
|
238
|
-
// that include relationships back to the same entity
|
|
239
|
-
args.context._resolveOutputCounter.depth++;
|
|
240
|
-
try {
|
|
241
|
-
// Use Promise.resolve() to handle both sync and async hooks
|
|
242
|
-
filtered[fieldName] = await Promise.resolve(hook({
|
|
243
|
-
value,
|
|
244
|
-
operation: 'query',
|
|
245
|
-
fieldName,
|
|
246
|
-
listKey,
|
|
247
|
-
item,
|
|
248
|
-
context: args.context,
|
|
249
|
-
}));
|
|
250
|
-
}
|
|
251
|
-
finally {
|
|
252
|
-
args.context._resolveOutputCounter.depth--;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
filtered[fieldName] = value;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
// Process virtual fields - compute values from other fields
|
|
261
|
-
// Virtual fields don't exist in the database result, so we need to compute them separately
|
|
262
|
-
for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
|
|
263
|
-
// Skip if already processed (from database result)
|
|
264
|
-
if (fieldName in filtered) {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
// Only process virtual fields
|
|
268
|
-
if (!fieldConfig.virtual) {
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
// Check field access
|
|
272
|
-
const canRead = await checkFieldAccess(fieldConfig.access, 'read', {
|
|
273
|
-
...args,
|
|
274
|
-
item,
|
|
275
|
-
});
|
|
276
|
-
if (!canRead) {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
// Virtual fields must have resolveOutput hook to compute their value
|
|
280
|
-
if (fieldConfig.hooks?.resolveOutput && listKey) {
|
|
281
|
-
const hook = fieldConfig.hooks.resolveOutput;
|
|
282
|
-
// Increment depth counter to prevent infinite loops from hooks making DB queries
|
|
283
|
-
// that include relationships back to the same entity
|
|
284
|
-
args.context._resolveOutputCounter.depth++;
|
|
285
|
-
try {
|
|
286
|
-
// Use Promise.resolve() to handle both sync and async hooks
|
|
287
|
-
filtered[fieldName] = await Promise.resolve(hook({
|
|
288
|
-
value: undefined, // Virtual fields don't have a database value
|
|
289
|
-
operation: 'query',
|
|
290
|
-
fieldName,
|
|
291
|
-
listKey,
|
|
292
|
-
item: filtered, // Pass filtered item so virtual field can access other fields
|
|
293
|
-
context: args.context,
|
|
294
|
-
}));
|
|
295
|
-
}
|
|
296
|
-
finally {
|
|
297
|
-
args.context._resolveOutputCounter.depth--;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return filtered;
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Filter fields from input data based on write access (create/update)
|
|
305
|
-
*/
|
|
306
|
-
export async function filterWritableFields(data, fieldConfigs, operation, args) {
|
|
307
|
-
const filtered = {};
|
|
308
|
-
// Build a set of foreign key field names to exclude
|
|
309
|
-
// Foreign keys should not be in the data when using Prisma's relation syntax
|
|
310
|
-
const foreignKeyFields = new Set();
|
|
311
|
-
for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
|
|
312
|
-
if (fieldConfig.type === 'relationship') {
|
|
313
|
-
// For non-many relationships, Prisma creates a foreign key field named `${fieldName}Id`
|
|
314
|
-
const relConfig = fieldConfig;
|
|
315
|
-
if (!relConfig.many) {
|
|
316
|
-
foreignKeyFields.add(`${fieldName}Id`);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
for (const [fieldName, value] of Object.entries(data)) {
|
|
321
|
-
const fieldConfig = fieldConfigs[fieldName];
|
|
322
|
-
// Skip system fields
|
|
323
|
-
if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
// Skip virtual fields - they don't store in database
|
|
327
|
-
// Virtual fields with resolveInput hooks handle side effects separately
|
|
328
|
-
if (fieldConfig && 'virtual' in fieldConfig && fieldConfig.virtual) {
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
// Skip foreign key fields (e.g., authorId) when their corresponding relationship field exists
|
|
332
|
-
// This prevents conflicts when using Prisma's relation syntax (e.g., author: { connect: { id } })
|
|
333
|
-
if (foreignKeyFields.has(fieldName)) {
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
// Check field access (checkFieldAccess already handles sudo mode)
|
|
337
|
-
const canWrite = await checkFieldAccess(fieldConfig?.access, operation, {
|
|
338
|
-
...args,
|
|
339
|
-
inputData: args.inputData,
|
|
340
|
-
});
|
|
341
|
-
if (canWrite) {
|
|
342
|
-
filtered[fieldName] = value;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
return filtered;
|
|
346
|
-
}
|
|
347
81
|
//# sourceMappingURL=engine.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,SAAS,CAAA;AACnC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC7E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,eAAuB,EACvB,MAAsB;IAGtB,uDAAuD;IACvD,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACxC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7C,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzB,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAEzC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAA;AACjC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,aAA2C,EAC3C,IAIC;IAED,0CAA0C;IAC1C,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,sCAAsC;IACtC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAA;IAExC,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAC1B,UAAoC,EACpC,YAAoC;IAEpC,mCAAmC;IACnC,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAA;IACb,CAAC;IAED,8CAA8C;IAC9C,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,OAAO,UAAU,IAAI,EAAE,CAAA;IACzB,CAAC;IAED,uCAAuC;IACvC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,YAAY,CAAA;IACrB,CAAC;IAED,2BAA2B;IAC3B,OAAO;QACL,GAAG,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC;KAChC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Session, AccessContext } from './types.js';
|
|
2
|
+
import type { FieldAccess } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared field-level access evaluation.
|
|
5
|
+
*
|
|
6
|
+
* This module is the single, canonical home for field-level access checks. Both
|
|
7
|
+
* read-time (Field Visibility, see `field-visibility.ts`) and write-time paths
|
|
8
|
+
* evaluate field access through `checkFieldAccess` — there is intentionally no
|
|
9
|
+
* second, parallel field-access evaluator. See
|
|
10
|
+
* `docs/adr/0001-access-control-is-a-two-phase-read.md` and the access-control
|
|
11
|
+
* glossary in `CONTEXT.md` for the two-phase read model that motivates this.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Check field-level access for a specific operation.
|
|
15
|
+
*
|
|
16
|
+
* This is the canonical field-access evaluator. Its signature is deliberate:
|
|
17
|
+
* field access can depend on the `operation`, on the already-fetched `item`
|
|
18
|
+
* (read/update/delete), and on the `inputData` being written (create/update),
|
|
19
|
+
* so all of those are accepted. Do not introduce a parallel evaluator with a
|
|
20
|
+
* narrower signature.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkFieldAccess(fieldAccess: FieldAccess | undefined, operation: 'read' | 'create' | 'update', args: {
|
|
23
|
+
session: Session | null;
|
|
24
|
+
item?: Record<string, unknown>;
|
|
25
|
+
context: AccessContext & {
|
|
26
|
+
_isSudo?: boolean;
|
|
27
|
+
};
|
|
28
|
+
inputData?: Record<string, unknown>;
|
|
29
|
+
}): Promise<boolean>;
|
|
30
|
+
/**
|
|
31
|
+
* Filter fields from input data based on write access (create/update)
|
|
32
|
+
*/
|
|
33
|
+
export declare function filterWritableFields<T extends Record<string, unknown>>(data: T, fieldConfigs: Record<string, {
|
|
34
|
+
access?: FieldAccess;
|
|
35
|
+
type?: string;
|
|
36
|
+
}>, operation: 'create' | 'update', args: {
|
|
37
|
+
session: Session | null;
|
|
38
|
+
item?: Record<string, unknown>;
|
|
39
|
+
context: AccessContext & {
|
|
40
|
+
_isSudo?: boolean;
|
|
41
|
+
};
|
|
42
|
+
inputData?: Record<string, unknown>;
|
|
43
|
+
}): Promise<Partial<T>>;
|
|
44
|
+
//# sourceMappingURL=field-access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-access.d.ts","sourceRoot":"","sources":["../../src/access/field-access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,EACvC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,GACA,OAAO,CAAC,OAAO,CAAC,CAmClB;AA8BD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,IAAI,EAAE,CAAC,EACP,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EACrE,SAAS,EAAE,QAAQ,GAAG,QAAQ,EAC9B,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,GACA,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAgDrB"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared field-level access evaluation.
|
|
3
|
+
*
|
|
4
|
+
* This module is the single, canonical home for field-level access checks. Both
|
|
5
|
+
* read-time (Field Visibility, see `field-visibility.ts`) and write-time paths
|
|
6
|
+
* evaluate field access through `checkFieldAccess` — there is intentionally no
|
|
7
|
+
* second, parallel field-access evaluator. See
|
|
8
|
+
* `docs/adr/0001-access-control-is-a-two-phase-read.md` and the access-control
|
|
9
|
+
* glossary in `CONTEXT.md` for the two-phase read model that motivates this.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Check field-level access for a specific operation.
|
|
13
|
+
*
|
|
14
|
+
* This is the canonical field-access evaluator. Its signature is deliberate:
|
|
15
|
+
* field access can depend on the `operation`, on the already-fetched `item`
|
|
16
|
+
* (read/update/delete), and on the `inputData` being written (create/update),
|
|
17
|
+
* so all of those are accepted. Do not introduce a parallel evaluator with a
|
|
18
|
+
* narrower signature.
|
|
19
|
+
*/
|
|
20
|
+
export async function checkFieldAccess(fieldAccess, operation, args) {
|
|
21
|
+
// Skip access check in sudo mode
|
|
22
|
+
if (args.context._isSudo) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (!fieldAccess) {
|
|
26
|
+
return true; // No field access means allow
|
|
27
|
+
}
|
|
28
|
+
const accessControl = fieldAccess[operation];
|
|
29
|
+
if (!accessControl) {
|
|
30
|
+
return true; // No specific access control means allow
|
|
31
|
+
}
|
|
32
|
+
const result = await accessControl({
|
|
33
|
+
session: args.session,
|
|
34
|
+
item: args.item,
|
|
35
|
+
context: args.context,
|
|
36
|
+
inputData: args.inputData,
|
|
37
|
+
operation,
|
|
38
|
+
});
|
|
39
|
+
// If result is false, deny access
|
|
40
|
+
if (result === false) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
// If result is true, allow access
|
|
44
|
+
if (result === true) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
// Default to allowing access if we can't determine
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Simple filter matching for field-level access
|
|
52
|
+
* Checks if an item matches a Prisma-like filter object
|
|
53
|
+
*/
|
|
54
|
+
function matchesFilter(item, filter) {
|
|
55
|
+
for (const [key, condition] of Object.entries(filter)) {
|
|
56
|
+
if (typeof condition === 'object' && condition !== null) {
|
|
57
|
+
// Handle nested conditions like { equals: value }
|
|
58
|
+
if ('equals' in condition) {
|
|
59
|
+
if (item[key] !== condition.equals) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if ('not' in condition) {
|
|
64
|
+
if (item[key] === condition.not) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Add more condition types as needed
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Direct equality check
|
|
72
|
+
if (item[key] !== condition) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Filter fields from input data based on write access (create/update)
|
|
81
|
+
*/
|
|
82
|
+
export async function filterWritableFields(data, fieldConfigs, operation, args) {
|
|
83
|
+
const filtered = {};
|
|
84
|
+
// Build a set of foreign key field names to exclude
|
|
85
|
+
// Foreign keys should not be in the data when using Prisma's relation syntax
|
|
86
|
+
const foreignKeyFields = new Set();
|
|
87
|
+
for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
|
|
88
|
+
if (fieldConfig.type === 'relationship') {
|
|
89
|
+
// For non-many relationships, Prisma creates a foreign key field named `${fieldName}Id`
|
|
90
|
+
const relConfig = fieldConfig;
|
|
91
|
+
if (!relConfig.many) {
|
|
92
|
+
foreignKeyFields.add(`${fieldName}Id`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const [fieldName, value] of Object.entries(data)) {
|
|
97
|
+
const fieldConfig = fieldConfigs[fieldName];
|
|
98
|
+
// Skip system fields
|
|
99
|
+
if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Skip virtual fields - they don't store in database
|
|
103
|
+
// Virtual fields with resolveInput hooks handle side effects separately
|
|
104
|
+
if (fieldConfig && 'virtual' in fieldConfig && fieldConfig.virtual) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Skip foreign key fields (e.g., authorId) when their corresponding relationship field exists
|
|
108
|
+
// This prevents conflicts when using Prisma's relation syntax (e.g., author: { connect: { id } })
|
|
109
|
+
if (foreignKeyFields.has(fieldName)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Check field access (checkFieldAccess already handles sudo mode)
|
|
113
|
+
const canWrite = await checkFieldAccess(fieldConfig?.access, operation, {
|
|
114
|
+
...args,
|
|
115
|
+
inputData: args.inputData,
|
|
116
|
+
});
|
|
117
|
+
if (canWrite) {
|
|
118
|
+
filtered[fieldName] = value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return filtered;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=field-access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-access.js","sourceRoot":"","sources":["../../src/access/field-access.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-access.test.d.ts","sourceRoot":"","sources":["../../src/access/field-access.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { filterWritableFields } from './
|
|
2
|
+
import { filterWritableFields } from './field-access.js';
|
|
3
3
|
describe('filterWritableFields', () => {
|
|
4
4
|
it('should filter out foreign key fields when their corresponding relationship field exists', async () => {
|
|
5
5
|
// Setup: Define field configs with a relationship field
|
|
@@ -126,4 +126,4 @@ describe('filterWritableFields', () => {
|
|
|
126
126
|
expect(filtered).not.toHaveProperty('authorId');
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
|
-
//# sourceMappingURL=
|
|
129
|
+
//# sourceMappingURL=field-access.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-access.test.js","sourceRoot":"","sources":["../../src/access/field-access.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Session, AccessContext } from './types.js';
|
|
2
|
+
import type { OpenSaasConfig, FieldConfig } from '../config/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Filter fields from an object based on read access
|
|
5
|
+
* Recursively applies access control to nested relationships
|
|
6
|
+
*/
|
|
7
|
+
export declare function filterReadableFields<T extends Record<string, unknown>>(item: T, fieldConfigs: Record<string, FieldConfig>, args: {
|
|
8
|
+
session: Session | null;
|
|
9
|
+
context: AccessContext & {
|
|
10
|
+
_isSudo?: boolean;
|
|
11
|
+
};
|
|
12
|
+
}, config?: OpenSaasConfig, depth?: number, listKey?: string): Promise<Partial<T>>;
|
|
13
|
+
//# sourceMappingURL=field-visibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-visibility.d.ts","sourceRoot":"","sources":["../../src/access/field-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAqGrE;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,IAAI,EAAE,CAAC,EACP,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAC/C,EACD,MAAM,CAAC,EAAE,cAAc,EACvB,KAAK,GAAE,MAAU,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAwJrB"}
|