@opensaas/stack-core 0.37.0 → 0.39.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 +191 -0
- package/CLAUDE.md +42 -3
- package/dist/access/access-filter.d.ts +70 -113
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +183 -210
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +148 -188
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/declared-dependencies.d.ts +66 -26
- package/dist/access/declared-dependencies.d.ts.map +1 -1
- package/dist/access/declared-dependencies.js +62 -17
- package/dist/access/declared-dependencies.js.map +1 -1
- package/dist/access/declared-dependencies.test.d.ts +2 -0
- package/dist/access/declared-dependencies.test.d.ts.map +1 -0
- package/dist/access/declared-dependencies.test.js +226 -0
- package/dist/access/declared-dependencies.test.js.map +1 -0
- package/dist/access/depth-limits.d.ts +8 -7
- package/dist/access/depth-limits.d.ts.map +1 -1
- package/dist/access/depth-limits.js +8 -7
- package/dist/access/depth-limits.js.map +1 -1
- package/dist/access/engine.d.ts +0 -20
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +0 -25
- package/dist/access/engine.js.map +1 -1
- package/dist/access/errors.d.ts +61 -8
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +91 -12
- package/dist/access/errors.js.map +1 -1
- package/dist/access/field-access.d.ts +39 -1
- package/dist/access/field-access.d.ts.map +1 -1
- package/dist/access/field-access.js +108 -42
- package/dist/access/field-access.js.map +1 -1
- package/dist/access/field-access.test.js +229 -2
- package/dist/access/field-access.test.js.map +1 -1
- package/dist/access/field-transforms.d.ts +0 -27
- package/dist/access/field-transforms.d.ts.map +1 -1
- package/dist/access/field-visibility.d.ts +2 -5
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +89 -41
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +6 -3
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +12 -2
- package/dist/access/index.js.map +1 -1
- package/dist/access/query-validation.d.ts +121 -0
- package/dist/access/query-validation.d.ts.map +1 -0
- package/dist/access/query-validation.js +292 -0
- package/dist/access/query-validation.js.map +1 -0
- package/dist/access/relationship-count.d.ts +1 -1
- package/dist/access/relationship-count.d.ts.map +1 -1
- package/dist/access/relationship-count.js +2 -10
- package/dist/access/relationship-count.js.map +1 -1
- package/dist/access/relationship-label-filter.d.ts +23 -23
- package/dist/access/relationship-label-filter.d.ts.map +1 -1
- package/dist/access/relationship-label-filter.js +5 -76
- package/dist/access/relationship-label-filter.js.map +1 -1
- package/dist/access/relationship-label-filter.test.js +19 -56
- package/dist/access/relationship-label-filter.test.js.map +1 -1
- package/dist/access/transaction-registry.d.ts +45 -0
- package/dist/access/transaction-registry.d.ts.map +1 -0
- package/dist/access/transaction-registry.js +37 -0
- package/dist/access/transaction-registry.js.map +1 -0
- package/dist/access/types.d.ts +55 -59
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/index.d.ts +9 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +10 -18
- package/dist/config/index.js.map +1 -1
- package/dist/config/label.d.ts +6 -13
- package/dist/config/label.d.ts.map +1 -1
- package/dist/config/label.js +6 -13
- package/dist/config/label.js.map +1 -1
- package/dist/config/nav-count.d.ts +11 -19
- package/dist/config/nav-count.d.ts.map +1 -1
- package/dist/config/nav-count.js +11 -19
- package/dist/config/nav-count.js.map +1 -1
- package/dist/config/plugin-engine.d.ts +0 -10
- package/dist/config/plugin-engine.d.ts.map +1 -1
- package/dist/config/plugin-engine.js +1 -37
- package/dist/config/plugin-engine.js.map +1 -1
- package/dist/config/types.d.ts +94 -109
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/apply-defaults.d.ts +8 -31
- package/dist/context/apply-defaults.d.ts.map +1 -1
- package/dist/context/apply-defaults.js +12 -41
- package/dist/context/apply-defaults.js.map +1 -1
- package/dist/context/hook-pipeline.d.ts +4 -29
- package/dist/context/hook-pipeline.d.ts.map +1 -1
- package/dist/context/hook-pipeline.js +8 -41
- package/dist/context/hook-pipeline.js.map +1 -1
- package/dist/context/index.d.ts +4 -15
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +223 -215
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts +7 -26
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +73 -171
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/transaction-boundary.d.ts +36 -47
- package/dist/context/transaction-boundary.d.ts.map +1 -1
- package/dist/context/transaction-boundary.js +92 -79
- package/dist/context/transaction-boundary.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +25 -64
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +81 -111
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/fields/format-prisma-default.d.ts.map +1 -1
- package/dist/fields/format-prisma-default.js +4 -7
- package/dist/fields/format-prisma-default.js.map +1 -1
- package/dist/fields/index.d.ts +17 -2
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +136 -147
- package/dist/fields/index.js.map +1 -1
- package/dist/filter/collect.d.ts +26 -3
- package/dist/filter/collect.d.ts.map +1 -1
- package/dist/filter/collect.js +30 -12
- package/dist/filter/collect.js.map +1 -1
- package/dist/filter/filter.test.js +98 -15
- package/dist/filter/filter.test.js.map +1 -1
- package/dist/filter/index.d.ts +1 -0
- package/dist/filter/index.d.ts.map +1 -1
- package/dist/filter/index.js.map +1 -1
- package/dist/filter/map.d.ts.map +1 -1
- package/dist/filter/map.js +0 -5
- package/dist/filter/map.js.map +1 -1
- package/dist/filter/parse.d.ts.map +1 -1
- package/dist/filter/parse.js +2 -9
- package/dist/filter/parse.js.map +1 -1
- package/dist/filter/serialize.d.ts.map +1 -1
- package/dist/filter/serialize.js +2 -4
- package/dist/filter/serialize.js.map +1 -1
- package/dist/hooks/index.d.ts +4 -42
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +5 -59
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -1
- package/dist/lib/case-utils.d.ts +0 -32
- package/dist/lib/case-utils.d.ts.map +1 -1
- package/dist/lib/case-utils.js +0 -32
- package/dist/lib/case-utils.js.map +1 -1
- package/dist/mcp/handler.d.ts +0 -4
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/mcp/handler.js +12 -69
- package/dist/mcp/handler.js.map +1 -1
- package/dist/query/index.d.ts +55 -227
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +41 -117
- package/dist/query/index.js.map +1 -1
- package/dist/query/relationship-options.d.ts +1 -1
- package/dist/query/relationship-options.js +1 -1
- package/dist/utils/password.d.ts +0 -68
- package/dist/utils/password.d.ts.map +1 -1
- package/dist/utils/password.js +7 -76
- package/dist/utils/password.js.map +1 -1
- package/dist/validation/field-config.d.ts.map +1 -1
- package/dist/validation/field-config.js +0 -6
- package/dist/validation/field-config.js.map +1 -1
- package/dist/validation/schema.d.ts +0 -7
- package/dist/validation/schema.d.ts.map +1 -1
- package/dist/validation/schema.js +0 -10
- package/dist/validation/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +205 -275
- package/src/access/access-filter.ts +236 -272
- package/src/access/declared-dependencies.test.ts +277 -0
- package/src/access/declared-dependencies.ts +117 -37
- package/src/access/depth-limits.ts +8 -7
- package/src/access/engine.ts +0 -25
- package/src/access/errors.ts +97 -12
- package/src/access/field-access.test.ts +312 -2
- package/src/access/field-access.ts +118 -42
- package/src/access/field-transforms.ts +8 -39
- package/src/access/field-visibility.ts +99 -45
- package/src/access/index.ts +17 -5
- package/src/access/query-validation.ts +359 -0
- package/src/access/relationship-count.ts +3 -11
- package/src/access/relationship-label-filter.test.ts +19 -68
- package/src/access/relationship-label-filter.ts +27 -104
- package/src/access/transaction-registry.ts +58 -0
- package/src/access/types.ts +83 -86
- package/src/config/index.ts +10 -22
- package/src/config/label.ts +6 -13
- package/src/config/nav-count.ts +11 -19
- package/src/config/plugin-engine.ts +1 -37
- package/src/config/types.ts +95 -110
- package/src/context/apply-defaults.ts +12 -44
- package/src/context/hook-pipeline.ts +12 -67
- package/src/context/index.ts +311 -276
- package/src/context/nested-operations.ts +94 -226
- package/src/context/transaction-boundary.ts +113 -109
- package/src/context/write-pipeline.ts +99 -155
- package/src/fields/format-prisma-default.ts +4 -7
- package/src/fields/index.ts +155 -147
- package/src/filter/collect.ts +48 -12
- package/src/filter/filter.test.ts +153 -15
- package/src/filter/index.ts +1 -0
- package/src/filter/map.ts +0 -5
- package/src/filter/parse.ts +2 -9
- package/src/filter/serialize.ts +2 -4
- package/src/hooks/index.ts +5 -59
- package/src/index.ts +23 -4
- package/src/internal.ts +5 -0
- package/src/lib/case-utils.ts +0 -32
- package/src/mcp/handler.ts +13 -71
- package/src/query/index.ts +82 -235
- package/src/query/relationship-options.ts +1 -1
- package/src/utils/password.ts +7 -76
- package/src/validation/field-config.ts +0 -6
- package/src/validation/schema.ts +0 -10
- package/tests/access-relationships.test.ts +18 -16
- package/tests/computed-field-selective-evaluation.test.ts +418 -0
- package/tests/context.test.ts +727 -1
- package/tests/field-types.test.ts +222 -0
- package/tests/mcp-handler.test.ts +34 -0
- package/tests/needs-declared-dependencies.test.ts +7 -4
- package/tests/resolve-chain.test.ts +11 -11
- package/tests/transaction-boundary-outermost.test.ts +530 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpE;;;;;;;;;;;;;GAaG;AAEH
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpE;;;;;;;;;;;;;GAaG;AAEH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,CAE1D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAEpE;AAED,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,cAAc,GAErB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,CAAA;CAAE,GAAG,IAAI,CAe1D;AAED,wBAAsB,WAAW,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,aAAa,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,SAAS,EAC3C,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,CAAC,CAAA;IACR,OAAO,EAAE,aAAa,CAAA;CACvB,GACA,OAAO,CAAC,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CASpC;AAED,wBAAgB,YAAY,CAC1B,UAAU,EAAE,YAAY,GAAG,SAAS,EACpC,YAAY,EAAE,OAAO,GAAG,YAAY,GACnC,YAAY,GAAG,IAAI,CAgBrB"}
|
package/dist/access/engine.js
CHANGED
|
@@ -12,26 +12,12 @@
|
|
|
12
12
|
* (`checkFieldAccess`). See `docs/adr/0001-access-control-is-a-two-phase-read.md`
|
|
13
13
|
* and the access-control glossary in `CONTEXT.md`.
|
|
14
14
|
*/
|
|
15
|
-
/**
|
|
16
|
-
* Check if access control result is a boolean
|
|
17
|
-
*/
|
|
18
15
|
export function isBoolean(value) {
|
|
19
16
|
return typeof value === 'boolean';
|
|
20
17
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Check if access control result is a Prisma filter
|
|
23
|
-
*/
|
|
24
18
|
export function isPrismaFilter(value) {
|
|
25
19
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
20
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Parse a relationship ref and get the related list configuration
|
|
29
|
-
* Relationship refs are in the format "ListName.fieldName"
|
|
30
|
-
*
|
|
31
|
-
* @param relationshipRef - The ref string (e.g., "Post.author")
|
|
32
|
-
* @param config - The OpenSaas configuration
|
|
33
|
-
* @returns The related list name and config, or null if not found
|
|
34
|
-
*/
|
|
35
21
|
export function getRelatedListConfig(relationshipRef, config) {
|
|
36
22
|
// Parse ref format: "ListName.fieldName" or "ListName"
|
|
37
23
|
const parts = relationshipRef.split('.');
|
|
@@ -45,35 +31,24 @@ export function getRelatedListConfig(relationshipRef, config) {
|
|
|
45
31
|
}
|
|
46
32
|
return { listName, listConfig };
|
|
47
33
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Execute an access control function
|
|
50
|
-
*/
|
|
51
34
|
export async function checkAccess(accessControl, args) {
|
|
52
35
|
// No access control means deny by default
|
|
53
36
|
if (!accessControl) {
|
|
54
37
|
return false;
|
|
55
38
|
}
|
|
56
|
-
// Execute the access control function
|
|
57
39
|
const result = await accessControl(args);
|
|
58
40
|
return result;
|
|
59
41
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Merge user filter with access control filter
|
|
62
|
-
*/
|
|
63
42
|
export function mergeFilters(userFilter, accessFilter) {
|
|
64
|
-
// If access is denied, return null
|
|
65
43
|
if (accessFilter === false) {
|
|
66
44
|
return null;
|
|
67
45
|
}
|
|
68
|
-
// If access is fully granted, use user filter
|
|
69
46
|
if (accessFilter === true) {
|
|
70
47
|
return userFilter || {};
|
|
71
48
|
}
|
|
72
|
-
// Merge access filter with user filter
|
|
73
49
|
if (!userFilter) {
|
|
74
50
|
return accessFilter;
|
|
75
51
|
}
|
|
76
|
-
// Combine filters with AND
|
|
77
52
|
return {
|
|
78
53
|
AND: [accessFilter, userFilter],
|
|
79
54
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,SAAS,CAAA;AACnC,CAAC;AAED,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,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,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,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAA;IAExC,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,UAAoC,EACpC,YAAoC;IAEpC,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,OAAO,UAAU,IAAI,EAAE,CAAA;IACzB,CAAC;IAED,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,YAAY,CAAA;IACrB,CAAC;IAED,OAAO;QACL,GAAG,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC;KAChC,CAAA;AACH,CAAC"}
|
package/dist/access/errors.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thrown when a caller-supplied `include` names a relation nested deeper than
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* swallow this.
|
|
3
|
+
* `READ_INCLUDE_MAX_DEPTH`. Deliberately distinct from `ValidationError`: this
|
|
4
|
+
* is not bad user input, it is the engine declining to serve a tree this
|
|
5
|
+
* expensive. Code that catches `ValidationError` to report form errors must
|
|
6
|
+
* not silently swallow this.
|
|
8
7
|
*
|
|
9
|
-
* Only an explicit
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Only an explicit request naming something at or past the cap triggers this
|
|
9
|
+
* — a read that simply doesn't reach this deep never throws. Before
|
|
10
|
+
* ADR-0026 made the read pipeline caller-directed, this cap was the engine's
|
|
11
|
+
* last line of defense against returning a relation it could not prove was
|
|
12
|
+
* row/field scoped (ADR-0022, issue #830); a request naming anything at this
|
|
13
|
+
* depth is now scoped exactly like every other named relation; the cap
|
|
14
|
+
* exists solely to bound how deep a request may cost the engine to serve. See
|
|
15
|
+
* ADR-0026 and `docs/adr/0022-access-control-fails-closed-when-it-cannot-scope.md`.
|
|
12
16
|
*/
|
|
13
17
|
export declare class AccessScopeDepthExceededError extends Error {
|
|
14
18
|
listKey: string;
|
|
@@ -40,4 +44,53 @@ export declare class ResolveOutputCycleError extends Error {
|
|
|
40
44
|
fieldKey: string;
|
|
41
45
|
}[]);
|
|
42
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Thrown when a relation filter in a caller's `where` (`some`/`every`/`none`/
|
|
49
|
+
* `is`/`isNot`) names a relationship whose related list denies operation-level
|
|
50
|
+
* `query` access outright (`=== false`). Deliberately a loud failure, not a
|
|
51
|
+
* silently narrowed `{ id: { in: [] } }` or a pass-through: ADR-0022 requires
|
|
52
|
+
* an engine that cannot compute a scope to deny, never pass through, and here
|
|
53
|
+
* the engine CAN compute the scope (denied) — passing that through as a
|
|
54
|
+
* silently-empty match would itself be a distinguishable signal for a caller
|
|
55
|
+
* probing which relations they may filter on. `include`'s equivalent case
|
|
56
|
+
* (`buildAccessScopedInclude`) drops the relation silently instead, because a
|
|
57
|
+
* `null`/missing key in a response is indistinguishable from "not requested";
|
|
58
|
+
* a `where` predicate has no such neutral outcome, so a relation filter on a
|
|
59
|
+
* fully denied relation is refused instead. `sudo` bypasses this check
|
|
60
|
+
* entirely, matching every other access-control escape hatch. See #916 and
|
|
61
|
+
* `docs/adr/0022-access-control-fails-closed-when-it-cannot-scope.md`.
|
|
62
|
+
*/
|
|
63
|
+
export declare class RelationFilterAccessDeniedError extends Error {
|
|
64
|
+
listKey: string;
|
|
65
|
+
fieldKey: string;
|
|
66
|
+
relatedListKey: string;
|
|
67
|
+
constructor(listKey: string, fieldKey: string, relatedListKey: string);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Thrown when a field-level access control function returns anything other
|
|
71
|
+
* than a strict `boolean`. `FieldAccessControl` is typed to return `boolean`
|
|
72
|
+
* only: field access is a single per-field visibility decision, not a row
|
|
73
|
+
* filter, and a denied field is removed rather than used to scope rows (see
|
|
74
|
+
* the "Field-level access" glossary entry in `CONTEXT.md`, ADR-0001, and
|
|
75
|
+
* ADR-0030).
|
|
76
|
+
*
|
|
77
|
+
* A rule that type-checks can never reach this — the only way here is a
|
|
78
|
+
* caller that bypasses the type (an untyped JS config, or a value forced past
|
|
79
|
+
* the checker), most notably a Prisma filter, which is the shape
|
|
80
|
+
* operation-level `AccessControl` accepts but `FieldAccessControl` does not.
|
|
81
|
+
* Before #913 this fell through to an unconditional `return true`, silently
|
|
82
|
+
* granting the field blanket access; it now fails loudly and closed instead,
|
|
83
|
+
* for `read`, `create`, and `update` alike.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately does not expose the offending result as a public field: unlike
|
|
86
|
+
* `AccessScopeDepthExceededError`/`ResolveOutputCycleError`'s fields, there is
|
|
87
|
+
* no concretely-typed shape to give it (the whole problem is that it isn't
|
|
88
|
+
* the `boolean` the caller's rule promised), and the root CLAUDE.md forbids
|
|
89
|
+
* exposing `unknown`/`any` as part of a package's external API. A description
|
|
90
|
+
* of what was returned instead is folded into the message text.
|
|
91
|
+
*/
|
|
92
|
+
export declare class InvalidFieldAccessResultError extends Error {
|
|
93
|
+
operation: 'read' | 'create' | 'update';
|
|
94
|
+
constructor(operation: 'read' | 'create' | 'update', result: unknown);
|
|
95
|
+
}
|
|
43
96
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/access/errors.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/access/errors.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;IAC/C,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IAEpB,YAAY,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAW3D;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IACzC,KAAK,EAAE,SAAS;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAE9D,YAAY,KAAK,EAAE,SAAS;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,EAUlE;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,+BAAgC,SAAQ,KAAK;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IAE7B,YAAY,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAUpE;CACF;AASD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;IAC/C,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAA;IAE9C,YAAY,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,EAAE,MAAM,EAAE,OAAO,EAWnE;CACF"}
|
package/dist/access/errors.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { READ_INCLUDE_MAX_DEPTH } from './depth-limits.js';
|
|
2
2
|
/**
|
|
3
3
|
* Thrown when a caller-supplied `include` names a relation nested deeper than
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* swallow this.
|
|
4
|
+
* `READ_INCLUDE_MAX_DEPTH`. Deliberately distinct from `ValidationError`: this
|
|
5
|
+
* is not bad user input, it is the engine declining to serve a tree this
|
|
6
|
+
* expensive. Code that catches `ValidationError` to report form errors must
|
|
7
|
+
* not silently swallow this.
|
|
9
8
|
*
|
|
10
|
-
* Only an explicit
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Only an explicit request naming something at or past the cap triggers this
|
|
10
|
+
* — a read that simply doesn't reach this deep never throws. Before
|
|
11
|
+
* ADR-0026 made the read pipeline caller-directed, this cap was the engine's
|
|
12
|
+
* last line of defense against returning a relation it could not prove was
|
|
13
|
+
* row/field scoped (ADR-0022, issue #830); a request naming anything at this
|
|
14
|
+
* depth is now scoped exactly like every other named relation; the cap
|
|
15
|
+
* exists solely to bound how deep a request may cost the engine to serve. See
|
|
16
|
+
* ADR-0026 and `docs/adr/0022-access-control-fails-closed-when-it-cannot-scope.md`.
|
|
13
17
|
*/
|
|
14
18
|
export class AccessScopeDepthExceededError extends Error {
|
|
15
19
|
listKey;
|
|
16
20
|
fieldKey;
|
|
17
21
|
depth;
|
|
18
22
|
constructor(listKey, fieldKey, depth) {
|
|
19
|
-
super(`Cannot
|
|
20
|
-
`
|
|
21
|
-
`
|
|
22
|
-
`
|
|
23
|
+
super(`Cannot include "${listKey}.${fieldKey}" at include depth ${depth}: this exceeds the read ` +
|
|
24
|
+
`pipeline's maximum include depth (${READ_INCLUDE_MAX_DEPTH}). This is a cost limit, not an ` +
|
|
25
|
+
`inability to scope — the engine declines to serve a tree this deep rather than returning ` +
|
|
26
|
+
`it. Restructure the query to fetch this relation separately.`);
|
|
23
27
|
this.name = 'AccessScopeDepthExceededError';
|
|
24
28
|
this.listKey = listKey;
|
|
25
29
|
this.fieldKey = fieldKey;
|
|
@@ -52,4 +56,79 @@ export class ResolveOutputCycleError extends Error {
|
|
|
52
56
|
this.chain = chain;
|
|
53
57
|
}
|
|
54
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Thrown when a relation filter in a caller's `where` (`some`/`every`/`none`/
|
|
61
|
+
* `is`/`isNot`) names a relationship whose related list denies operation-level
|
|
62
|
+
* `query` access outright (`=== false`). Deliberately a loud failure, not a
|
|
63
|
+
* silently narrowed `{ id: { in: [] } }` or a pass-through: ADR-0022 requires
|
|
64
|
+
* an engine that cannot compute a scope to deny, never pass through, and here
|
|
65
|
+
* the engine CAN compute the scope (denied) — passing that through as a
|
|
66
|
+
* silently-empty match would itself be a distinguishable signal for a caller
|
|
67
|
+
* probing which relations they may filter on. `include`'s equivalent case
|
|
68
|
+
* (`buildAccessScopedInclude`) drops the relation silently instead, because a
|
|
69
|
+
* `null`/missing key in a response is indistinguishable from "not requested";
|
|
70
|
+
* a `where` predicate has no such neutral outcome, so a relation filter on a
|
|
71
|
+
* fully denied relation is refused instead. `sudo` bypasses this check
|
|
72
|
+
* entirely, matching every other access-control escape hatch. See #916 and
|
|
73
|
+
* `docs/adr/0022-access-control-fails-closed-when-it-cannot-scope.md`.
|
|
74
|
+
*/
|
|
75
|
+
export class RelationFilterAccessDeniedError extends Error {
|
|
76
|
+
listKey;
|
|
77
|
+
fieldKey;
|
|
78
|
+
relatedListKey;
|
|
79
|
+
constructor(listKey, fieldKey, relatedListKey) {
|
|
80
|
+
super(`Cannot filter "${listKey}.${fieldKey}" — the related list "${relatedListKey}" denies ` +
|
|
81
|
+
`query access to this session, so this relation filter cannot be scoped. Denial is loud ` +
|
|
82
|
+
`rather than silently narrowed to match nothing (ADR-0022). Use sudo to bypass.`);
|
|
83
|
+
this.name = 'RelationFilterAccessDeniedError';
|
|
84
|
+
this.listKey = listKey;
|
|
85
|
+
this.fieldKey = fieldKey;
|
|
86
|
+
this.relatedListKey = relatedListKey;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function describeFieldAccessResult(result) {
|
|
90
|
+
if (result === null)
|
|
91
|
+
return 'null';
|
|
92
|
+
if (result === undefined)
|
|
93
|
+
return 'undefined';
|
|
94
|
+
if (typeof result === 'object')
|
|
95
|
+
return 'an object (e.g. a Prisma filter)';
|
|
96
|
+
return `a ${typeof result}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Thrown when a field-level access control function returns anything other
|
|
100
|
+
* than a strict `boolean`. `FieldAccessControl` is typed to return `boolean`
|
|
101
|
+
* only: field access is a single per-field visibility decision, not a row
|
|
102
|
+
* filter, and a denied field is removed rather than used to scope rows (see
|
|
103
|
+
* the "Field-level access" glossary entry in `CONTEXT.md`, ADR-0001, and
|
|
104
|
+
* ADR-0030).
|
|
105
|
+
*
|
|
106
|
+
* A rule that type-checks can never reach this — the only way here is a
|
|
107
|
+
* caller that bypasses the type (an untyped JS config, or a value forced past
|
|
108
|
+
* the checker), most notably a Prisma filter, which is the shape
|
|
109
|
+
* operation-level `AccessControl` accepts but `FieldAccessControl` does not.
|
|
110
|
+
* Before #913 this fell through to an unconditional `return true`, silently
|
|
111
|
+
* granting the field blanket access; it now fails loudly and closed instead,
|
|
112
|
+
* for `read`, `create`, and `update` alike.
|
|
113
|
+
*
|
|
114
|
+
* Deliberately does not expose the offending result as a public field: unlike
|
|
115
|
+
* `AccessScopeDepthExceededError`/`ResolveOutputCycleError`'s fields, there is
|
|
116
|
+
* no concretely-typed shape to give it (the whole problem is that it isn't
|
|
117
|
+
* the `boolean` the caller's rule promised), and the root CLAUDE.md forbids
|
|
118
|
+
* exposing `unknown`/`any` as part of a package's external API. A description
|
|
119
|
+
* of what was returned instead is folded into the message text.
|
|
120
|
+
*/
|
|
121
|
+
export class InvalidFieldAccessResultError extends Error {
|
|
122
|
+
operation;
|
|
123
|
+
constructor(operation, result) {
|
|
124
|
+
super(`Field-level access control for operation "${operation}" returned ` +
|
|
125
|
+
`${describeFieldAccessResult(result)}, not a boolean. Field access is a per-field ` +
|
|
126
|
+
`visibility decision — it must return true or false, and (unlike operation-level access) ` +
|
|
127
|
+
`cannot scope which rows are affected. If you meant to restrict access based on the row or ` +
|
|
128
|
+
`the write payload, evaluate the condition yourself and return a boolean, e.g. ` +
|
|
129
|
+
`\`({ item, session }) => item?.ownerId === session?.userId\`.`);
|
|
130
|
+
this.name = 'InvalidFieldAccessResultError';
|
|
131
|
+
this.operation = operation;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
55
134
|
//# sourceMappingURL=errors.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/access/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAE1D
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/access/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAE1D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,6BAA8B,SAAQ,KAAK;IAC/C,OAAO,CAAQ;IACf,QAAQ,CAAQ;IAChB,KAAK,CAAQ;IAEpB,YAAY,OAAe,EAAE,QAAgB,EAAE,KAAa;QAC1D,KAAK,CACH,mBAAmB,OAAO,IAAI,QAAQ,sBAAsB,KAAK,0BAA0B;YACzF,qCAAqC,sBAAsB,kCAAkC;YAC7F,2FAA2F;YAC3F,8DAA8D,CACjE,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAA;QAC3C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IACzC,KAAK,CAAkD;IAE9D,YAAY,KAAuD;QACjE,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAChF,KAAK,CACH,iCAAiC,IAAI,+CAA+C;YAClF,wFAAwF;YACxF,0FAA0F;YAC1F,iCAAiC,CACpC,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAA;QACrC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,+BAAgC,SAAQ,KAAK;IACjD,OAAO,CAAQ;IACf,QAAQ,CAAQ;IAChB,cAAc,CAAQ;IAE7B,YAAY,OAAe,EAAE,QAAgB,EAAE,cAAsB;QACnE,KAAK,CACH,kBAAkB,OAAO,IAAI,QAAQ,yBAAyB,cAAc,WAAW;YACrF,yFAAyF;YACzF,gFAAgF,CACnF,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,iCAAiC,CAAA;QAC7C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,cAAc,GAAG,cAAc,CAAA;IACtC,CAAC;CACF;AAED,SAAS,yBAAyB,CAAC,MAAe;IAChD,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,MAAM,CAAA;IAClC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,WAAW,CAAA;IAC5C,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,kCAAkC,CAAA;IACzE,OAAO,KAAK,OAAO,MAAM,EAAE,CAAA;AAC7B,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,6BAA8B,SAAQ,KAAK;IAC/C,SAAS,CAA8B;IAE9C,YAAY,SAAuC,EAAE,MAAe;QAClE,KAAK,CACH,6CAA6C,SAAS,aAAa;YACjE,GAAG,yBAAyB,CAAC,MAAM,CAAC,+CAA+C;YACnF,0FAA0F;YAC1F,4FAA4F;YAC5F,gFAAgF;YAChF,+DAA+D,CAClE,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAA;QAC3C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;IAC5B,CAAC;CACF"}
|
|
@@ -18,6 +18,10 @@ import type { FieldAccess } from './types.js';
|
|
|
18
18
|
* (read/update/delete), and on the `inputData` being written (create/update),
|
|
19
19
|
* so all of those are accepted. Do not introduce a parallel evaluator with a
|
|
20
20
|
* narrower signature.
|
|
21
|
+
*
|
|
22
|
+
* Returns a strict boolean, never a filter: a rule that returns anything else
|
|
23
|
+
* throws `InvalidFieldAccessResultError` rather than defaulting to allow (see
|
|
24
|
+
* that error's doc, ADR-0001, and ADR-0030).
|
|
21
25
|
*/
|
|
22
26
|
export declare function checkFieldAccess(fieldAccess: FieldAccess | undefined, operation: 'read' | 'create' | 'update', args: {
|
|
23
27
|
session: Session | null;
|
|
@@ -28,8 +32,42 @@ export declare function checkFieldAccess(fieldAccess: FieldAccess | undefined, o
|
|
|
28
32
|
inputData?: Record<string, unknown>;
|
|
29
33
|
}): Promise<boolean>;
|
|
30
34
|
/**
|
|
31
|
-
*
|
|
35
|
+
* Whether a field's `read` access allows it to be NAMED in a `where`/`orderBy`
|
|
36
|
+
* predicate, evaluated BEFORE the query runs (#915).
|
|
37
|
+
*
|
|
38
|
+
* Field-level `read` access has always been a post-query check (Field
|
|
39
|
+
* Visibility, `field-visibility.ts`): it strips a denied key from a row that
|
|
40
|
+
* has already been fetched. That leaves the predicate itself unconstrained —
|
|
41
|
+
* a caller can still filter or sort by a field whose value they could never
|
|
42
|
+
* read, and recover it (or its relative order) by probing. This function is
|
|
43
|
+
* the pre-query counterpart, called from `query-validation.ts`'s `where`/
|
|
44
|
+
* `orderBy` walk for every key that resolves to a declared field.
|
|
45
|
+
*
|
|
46
|
+
* It delegates to the same evaluator Field Visibility uses (`checkFieldAccess`),
|
|
47
|
+
* per this module's canonical-evaluator rule above. The one difference is the
|
|
48
|
+
* `item` it hands the rule: there is no fetched row
|
|
49
|
+
* yet, so a rule that depends on one (the shape `FieldAccess['read']`
|
|
50
|
+
* documents as the norm, e.g. `item?.ownerId === session?.userId`) cannot be
|
|
51
|
+
* answered here. Rather than skip the check for such a rule — which would
|
|
52
|
+
* reopen exactly the hole this closes for the fields most likely to be
|
|
53
|
+
* sensitive — it is handed a poisoned `item` that throws on any property
|
|
54
|
+
* read, and that throw is caught and resolved to `false`: a row-dependent
|
|
55
|
+
* `read` rule always denies at predicate time, documented and deliberate
|
|
56
|
+
* (see docs/adr/0031). A rule that never touches `item` (checking only
|
|
57
|
+
* `session`, the common case for a field meant to be filterable/sortable at
|
|
58
|
+
* all) evaluates normally and returns its real answer.
|
|
59
|
+
*
|
|
60
|
+
* A rule that returns a non-boolean is a distinct, louder failure (#913,
|
|
61
|
+
* ADR-0030) — `InvalidFieldAccessResultError` — and is deliberately NOT
|
|
62
|
+
* folded into the `false` here; it propagates so the config bug it signals is
|
|
63
|
+
* never mistaken for an ordinary field-level denial.
|
|
32
64
|
*/
|
|
65
|
+
export declare function isFieldReadableForPredicate(fieldAccess: FieldAccess | undefined, args: {
|
|
66
|
+
session: Session | null;
|
|
67
|
+
context: AccessContext & {
|
|
68
|
+
_isSudo?: boolean;
|
|
69
|
+
};
|
|
70
|
+
}): Promise<boolean>;
|
|
33
71
|
export declare function filterWritableFields<T extends Record<string, unknown>>(data: T, fieldConfigs: Record<string, {
|
|
34
72
|
access?: FieldAccess;
|
|
35
73
|
type?: string;
|
|
@@ -1 +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,
|
|
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,EAAsB,MAAM,YAAY,CAAA;AA6CjE;;;;;;;;;GASG;AAEH;;;;;;;;;;;;GAYG;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,CA+ClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,2BAA2B,CAC/C,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAC/C,GACA,OAAO,CAAC,OAAO,CAAC,CAelB;AAED,wBAAsB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,IAAI,EAAE,CAAC,EACP,YAAY,EAAE,MAAM,CAClB,MAAM,EACN;IACE,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EAAE,CAAA;CACjD,CACF,EACD,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,CAyHrB"}
|
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
// at module-evaluation time, so the field-access ⇄ hooks import cycle is safe
|
|
3
3
|
// under ESM live bindings.
|
|
4
4
|
import { ValidationError } from '../hooks/index.js';
|
|
5
|
+
import { InvalidFieldAccessResultError } from './errors.js';
|
|
6
|
+
/**
|
|
7
|
+
* Marks a throw caused by touching {@link createPoisonedItem}'s `item`, as
|
|
8
|
+
* opposed to some other error a field rule legitimately raises. Not exported
|
|
9
|
+
* — callers only ever see its effect (a `false` from
|
|
10
|
+
* `isFieldReadableForPredicate`), never the class itself.
|
|
11
|
+
*/
|
|
12
|
+
class PredicateTimeItemAccessError extends Error {
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* An `item` that throws {@link PredicateTimeItemAccessError} on ANY attempt to
|
|
16
|
+
* read a property off it — including via optional chaining (`item?.x`),
|
|
17
|
+
* since the Proxy itself is a truthy object and optional chaining still
|
|
18
|
+
* performs the property read once its base is non-nullish. Used by
|
|
19
|
+
* `isFieldReadableForPredicate` to detect a read rule that depends on the
|
|
20
|
+
* fetched row: there is no row yet at predicate-evaluation time (see that
|
|
21
|
+
* function's doc), so any rule that reaches into `item` at all cannot be
|
|
22
|
+
* answered here.
|
|
23
|
+
*/
|
|
24
|
+
function createPoisonedItem() {
|
|
25
|
+
return new Proxy({}, {
|
|
26
|
+
get(_target, prop) {
|
|
27
|
+
throw new PredicateTimeItemAccessError(String(prop));
|
|
28
|
+
},
|
|
29
|
+
has(_target, prop) {
|
|
30
|
+
throw new PredicateTimeItemAccessError(String(prop));
|
|
31
|
+
},
|
|
32
|
+
ownKeys() {
|
|
33
|
+
throw new PredicateTimeItemAccessError('ownKeys');
|
|
34
|
+
},
|
|
35
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
36
|
+
throw new PredicateTimeItemAccessError(String(prop));
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
5
40
|
/**
|
|
6
41
|
* Shared field-level access evaluation.
|
|
7
42
|
*
|
|
@@ -20,18 +55,29 @@ import { ValidationError } from '../hooks/index.js';
|
|
|
20
55
|
* (read/update/delete), and on the `inputData` being written (create/update),
|
|
21
56
|
* so all of those are accepted. Do not introduce a parallel evaluator with a
|
|
22
57
|
* narrower signature.
|
|
58
|
+
*
|
|
59
|
+
* Returns a strict boolean, never a filter: a rule that returns anything else
|
|
60
|
+
* throws `InvalidFieldAccessResultError` rather than defaulting to allow (see
|
|
61
|
+
* that error's doc, ADR-0001, and ADR-0030).
|
|
23
62
|
*/
|
|
24
63
|
export async function checkFieldAccess(fieldAccess, operation, args) {
|
|
25
|
-
// Skip access check in sudo mode
|
|
26
64
|
if (args.context._isSudo) {
|
|
27
65
|
return true;
|
|
28
66
|
}
|
|
29
67
|
if (!fieldAccess) {
|
|
30
|
-
return true;
|
|
68
|
+
return true;
|
|
31
69
|
}
|
|
70
|
+
// `FieldAccess['read']` is narrower than `FieldAccess['create'/'update']` (it
|
|
71
|
+
// only accepts the single `operation: 'read'` call shape, see `types.ts`),
|
|
72
|
+
// so indexing by a not-yet-narrowed `operation` union produces a callable
|
|
73
|
+
// whose effective parameter type collapses to an intersection TypeScript
|
|
74
|
+
// can't satisfy generically here. Widen back to the general
|
|
75
|
+
// `FieldAccessControl` — the same shape this function has always built and
|
|
76
|
+
// passed below — since by construction the args object always matches
|
|
77
|
+
// whichever operation is actually requested.
|
|
32
78
|
const accessControl = fieldAccess[operation];
|
|
33
79
|
if (!accessControl) {
|
|
34
|
-
return true;
|
|
80
|
+
return true;
|
|
35
81
|
}
|
|
36
82
|
const result = await accessControl({
|
|
37
83
|
session: args.session,
|
|
@@ -40,53 +86,74 @@ export async function checkFieldAccess(fieldAccess, operation, args) {
|
|
|
40
86
|
inputData: args.inputData,
|
|
41
87
|
operation,
|
|
42
88
|
});
|
|
43
|
-
// If result is false, deny access
|
|
44
89
|
if (result === false) {
|
|
45
90
|
return false;
|
|
46
91
|
}
|
|
47
|
-
// If result is true, allow access
|
|
48
92
|
if (result === true) {
|
|
49
93
|
return true;
|
|
50
94
|
}
|
|
51
|
-
//
|
|
52
|
-
|
|
95
|
+
// `FieldAccessControl` is typed to return `boolean` only — field access is a
|
|
96
|
+
// per-field visibility decision, not a row filter (ADR-0001, ADR-0030). A
|
|
97
|
+
// well-typed rule can never reach this line; getting here means a caller
|
|
98
|
+
// bypassed the type (most notably by returning a Prisma filter, the shape
|
|
99
|
+
// operation-level `AccessControl` accepts but this does not). Fail loudly
|
|
100
|
+
// and closed — this operation applies identically whether or not `item` is
|
|
101
|
+
// available, so `create` (which has no `item` to evaluate a filter against)
|
|
102
|
+
// needs no separate answer: neither operation ever honours a non-boolean.
|
|
103
|
+
throw new InvalidFieldAccessResultError(operation, result);
|
|
53
104
|
}
|
|
54
105
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
106
|
+
* Whether a field's `read` access allows it to be NAMED in a `where`/`orderBy`
|
|
107
|
+
* predicate, evaluated BEFORE the query runs (#915).
|
|
108
|
+
*
|
|
109
|
+
* Field-level `read` access has always been a post-query check (Field
|
|
110
|
+
* Visibility, `field-visibility.ts`): it strips a denied key from a row that
|
|
111
|
+
* has already been fetched. That leaves the predicate itself unconstrained —
|
|
112
|
+
* a caller can still filter or sort by a field whose value they could never
|
|
113
|
+
* read, and recover it (or its relative order) by probing. This function is
|
|
114
|
+
* the pre-query counterpart, called from `query-validation.ts`'s `where`/
|
|
115
|
+
* `orderBy` walk for every key that resolves to a declared field.
|
|
116
|
+
*
|
|
117
|
+
* It delegates to the same evaluator Field Visibility uses (`checkFieldAccess`),
|
|
118
|
+
* per this module's canonical-evaluator rule above. The one difference is the
|
|
119
|
+
* `item` it hands the rule: there is no fetched row
|
|
120
|
+
* yet, so a rule that depends on one (the shape `FieldAccess['read']`
|
|
121
|
+
* documents as the norm, e.g. `item?.ownerId === session?.userId`) cannot be
|
|
122
|
+
* answered here. Rather than skip the check for such a rule — which would
|
|
123
|
+
* reopen exactly the hole this closes for the fields most likely to be
|
|
124
|
+
* sensitive — it is handed a poisoned `item` that throws on any property
|
|
125
|
+
* read, and that throw is caught and resolved to `false`: a row-dependent
|
|
126
|
+
* `read` rule always denies at predicate time, documented and deliberate
|
|
127
|
+
* (see docs/adr/0031). A rule that never touches `item` (checking only
|
|
128
|
+
* `session`, the common case for a field meant to be filterable/sortable at
|
|
129
|
+
* all) evaluates normally and returns its real answer.
|
|
130
|
+
*
|
|
131
|
+
* A rule that returns a non-boolean is a distinct, louder failure (#913,
|
|
132
|
+
* ADR-0030) — `InvalidFieldAccessResultError` — and is deliberately NOT
|
|
133
|
+
* folded into the `false` here; it propagates so the config bug it signals is
|
|
134
|
+
* never mistaken for an ordinary field-level denial.
|
|
57
135
|
*/
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else {
|
|
75
|
-
// Direct equality check
|
|
76
|
-
if (item[key] !== condition) {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
136
|
+
export async function isFieldReadableForPredicate(fieldAccess, args) {
|
|
137
|
+
try {
|
|
138
|
+
return await checkFieldAccess(fieldAccess, 'read', {
|
|
139
|
+
session: args.session,
|
|
140
|
+
context: args.context,
|
|
141
|
+
item: createPoisonedItem(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
// Only the poisoned-item signal means "row-dependent, deny". Anything
|
|
146
|
+
// else — including `InvalidFieldAccessResultError` and a genuine bug in
|
|
147
|
+
// the rule itself — propagates unchanged rather than being silently
|
|
148
|
+
// folded into an ordinary denial (found in review of #925).
|
|
149
|
+
if (err instanceof PredicateTimeItemAccessError)
|
|
150
|
+
return false;
|
|
151
|
+
throw err;
|
|
80
152
|
}
|
|
81
|
-
return true;
|
|
82
153
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Filter fields from input data based on write access (create/update)
|
|
85
|
-
*/
|
|
86
154
|
export async function filterWritableFields(data, fieldConfigs, operation, args) {
|
|
87
155
|
const filtered = {};
|
|
88
|
-
//
|
|
89
|
-
// Foreign keys should not be in the data when using Prisma's relation syntax
|
|
156
|
+
// Foreign keys must not appear in `data` when using Prisma's relation syntax.
|
|
90
157
|
const foreignKeyFields = new Set();
|
|
91
158
|
// Map each raw per-part column name contributed by a multi-column field
|
|
92
159
|
// (e.g. storage image()/file() in Keystone-parity mode) back to its OWNING
|
|
@@ -122,17 +189,16 @@ export async function filterWritableFields(data, fieldConfigs, operation, args)
|
|
|
122
189
|
const isSudo = args.context._isSudo === true;
|
|
123
190
|
for (const [fieldName, value] of Object.entries(data)) {
|
|
124
191
|
const fieldConfig = fieldConfigs[fieldName];
|
|
125
|
-
// Skip system fields
|
|
126
192
|
if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) {
|
|
127
193
|
continue;
|
|
128
194
|
}
|
|
129
|
-
//
|
|
130
|
-
//
|
|
195
|
+
// Virtual fields don't store in the database — skipped here, but their
|
|
196
|
+
// resolveInput hooks still run as a separate side-effect step.
|
|
131
197
|
if (fieldConfig && 'virtual' in fieldConfig && fieldConfig.virtual) {
|
|
132
198
|
continue;
|
|
133
199
|
}
|
|
134
|
-
//
|
|
135
|
-
//
|
|
200
|
+
// Prevents conflicts with Prisma's relation syntax (e.g.,
|
|
201
|
+
// `author: { connect: { id } }`).
|
|
136
202
|
if (foreignKeyFields.has(fieldName)) {
|
|
137
203
|
continue;
|
|
138
204
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-access.js","sourceRoot":"","sources":["../../src/access/field-access.ts"],"names":[],"mappings":"AAEA,iFAAiF;AACjF,8EAA8E;AAC9E,2BAA2B;AAC3B,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD;;;;;;;;;GASG;AAEH
|
|
1
|
+
{"version":3,"file":"field-access.js","sourceRoot":"","sources":["../../src/access/field-access.ts"],"names":[],"mappings":"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"}
|