@aventara/core 1.0.0-rc.1 → 1.0.0-rc.10

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.
Files changed (228) hide show
  1. package/CHANGELOG.md +943 -16
  2. package/ERRORS.md +142 -0
  3. package/README.md +274 -16
  4. package/dist/adapters/adapter-discovery-diagnostic.guidance.d.ts +6 -0
  5. package/dist/adapters/adapter-discovery-diagnostic.guidance.js +7 -0
  6. package/dist/adapters/adapter-field.d.ts +22 -0
  7. package/dist/adapters/adapter-identifier.d.ts +9 -0
  8. package/dist/adapters/adapter-identifier.js +6 -1
  9. package/dist/adapters/adapter-model.d.ts +9 -0
  10. package/dist/adapters/adapter-operation.d.ts +18 -3
  11. package/dist/adapters/adapter-resource.d.ts +5 -0
  12. package/dist/adapters/adapter.interface.d.ts +10 -0
  13. package/dist/avz/avz.d.ts +1 -1
  14. package/dist/avz/json-shape.registry.d.ts +2 -1
  15. package/dist/compiler/contract-compiler-diagnostic.guidance.d.ts +6 -0
  16. package/dist/compiler/contract-compiler-diagnostic.guidance.js +97 -0
  17. package/dist/compiler/contract.compiler.d.ts +6 -0
  18. package/dist/compiler/contract.compiler.js +227 -24
  19. package/dist/compiler/managed-elsewhere.projector.d.ts +52 -0
  20. package/dist/compiler/managed-elsewhere.projector.js +179 -0
  21. package/dist/compiler/managed-elsewhere.validator.d.ts +9 -0
  22. package/dist/compiler/managed-elsewhere.validator.js +74 -0
  23. package/dist/config/computed-fields.d.ts +62 -0
  24. package/dist/config/computed-fields.js +73 -0
  25. package/dist/config/config-diagnostic.guidance.d.ts +6 -0
  26. package/dist/config/config-diagnostic.guidance.js +32 -0
  27. package/dist/config/config.resolver.d.ts +24 -0
  28. package/dist/config/config.resolver.js +65 -0
  29. package/dist/config/config.validator.js +222 -8
  30. package/dist/config/define-resource.d.ts +1 -5
  31. package/dist/config/entrypoint.normalizer.d.ts +3 -2
  32. package/dist/config/exact-framework-config.d.ts +67 -22
  33. package/dist/config/field-compute.resolver.d.ts +2 -0
  34. package/dist/config/field-compute.resolver.js +8 -0
  35. package/dist/config/framework-config.d.ts +53 -15
  36. package/dist/config/framework-resources-config.d.ts +25 -14
  37. package/dist/config/pipeline-config.d.ts +4 -4
  38. package/dist/config/pipeline-tree.resolver.d.ts +2 -2
  39. package/dist/contracts/access-route.rules.d.ts +10 -0
  40. package/dist/contracts/access-route.rules.js +45 -0
  41. package/dist/contracts/capability.d.ts +26 -1
  42. package/dist/contracts/capability.js +20 -0
  43. package/dist/contracts/client-contract-structure.validator.d.ts +301 -89
  44. package/dist/contracts/client-contract-structure.validator.js +37 -2
  45. package/dist/contracts/contract-access.d.ts +29 -0
  46. package/dist/contracts/contract-access.js +1 -0
  47. package/dist/contracts/contract-canonicalizer.d.ts +9 -1
  48. package/dist/contracts/contract-canonicalizer.js +30 -4
  49. package/dist/contracts/contract-hash.d.ts +0 -1
  50. package/dist/contracts/contract-hash.js +1 -1
  51. package/dist/contracts/contract.d.ts +27 -1
  52. package/dist/contracts/field.d.ts +58 -1
  53. package/dist/contracts/json-schema.projector.d.ts +11 -0
  54. package/dist/contracts/json-schema.projector.js +78 -0
  55. package/dist/contracts/scalar-value-type.d.ts +3 -1
  56. package/dist/framework/access-facade.d.ts +116 -0
  57. package/dist/framework/access-facade.js +120 -0
  58. package/dist/framework/contract-view.d.ts +120 -16
  59. package/dist/framework/framework-construction.error.d.ts +14 -4
  60. package/dist/framework/framework-construction.error.js +18 -2
  61. package/dist/framework/framework.d.ts +26 -5
  62. package/dist/framework/framework.factory.d.ts +1 -1
  63. package/dist/framework/framework.factory.js +115 -23
  64. package/dist/framework/operation-facade.d.ts +80 -13
  65. package/dist/framework/operation-facade.js +2 -1
  66. package/dist/index.d.ts +17 -5
  67. package/dist/index.js +1 -0
  68. package/dist/internal.d.ts +7 -2
  69. package/dist/internal.js +6 -0
  70. package/dist/operations/database-refusal.d.ts +51 -0
  71. package/dist/operations/database-refusal.js +45 -0
  72. package/dist/operations/operation-arguments.d.ts +148 -60
  73. package/dist/operations/operation-call.d.ts +9 -5
  74. package/dist/operations/operation-result.d.ts +6 -4
  75. package/dist/operations/operation-validator-core.js +159 -36
  76. package/dist/operations/operation-validator.d.ts +1 -1
  77. package/dist/operations/structural-limits.walker.d.ts +1 -1
  78. package/dist/operations/validation-code.guidance.d.ts +6 -0
  79. package/dist/operations/validation-code.guidance.js +90 -0
  80. package/dist/operations/write-scope.resolver.d.ts +4 -0
  81. package/dist/operations/write-scope.resolver.js +2 -0
  82. package/dist/policy/access-provider.interface.d.ts +91 -0
  83. package/dist/policy/access-provider.interface.js +1 -0
  84. package/dist/policy/access-provider.registry.d.ts +17 -0
  85. package/dist/policy/access-provider.registry.js +15 -0
  86. package/dist/policy/access-question.d.ts +20 -0
  87. package/dist/policy/access-question.js +1 -0
  88. package/dist/policy/access-question.registry.d.ts +5 -0
  89. package/dist/policy/access-question.registry.js +19 -0
  90. package/dist/policy/access-question.runtime.d.ts +36 -0
  91. package/dist/policy/access-question.runtime.js +297 -0
  92. package/dist/policy/access-verb.table.d.ts +56 -0
  93. package/dist/policy/access-verb.table.js +21 -0
  94. package/dist/policy/access-verbs.d.ts +46 -0
  95. package/dist/policy/access-verbs.js +1 -0
  96. package/dist/policy/policy-admission.d.ts +20 -0
  97. package/dist/policy/policy-admission.js +409 -0
  98. package/dist/policy/policy-after-update.runtime.d.ts +79 -0
  99. package/dist/policy/policy-after-update.runtime.js +208 -0
  100. package/dist/policy/policy-condition.d.ts +15 -0
  101. package/dist/policy/policy-condition.js +36 -0
  102. package/dist/policy/policy-engine.d.ts +205 -0
  103. package/dist/policy/policy-engine.js +1 -0
  104. package/dist/policy/policy-named-rows.d.ts +18 -0
  105. package/dist/policy/policy-named-rows.js +51 -0
  106. package/dist/policy/policy-nested-write.runtime.d.ts +44 -0
  107. package/dist/policy/policy-nested-write.runtime.js +545 -0
  108. package/dist/policy/policy-read.runtime.d.ts +81 -0
  109. package/dist/policy/policy-read.runtime.js +503 -0
  110. package/dist/policy/policy-reference-write.runtime.d.ts +51 -0
  111. package/dist/policy/policy-reference-write.runtime.js +345 -0
  112. package/dist/policy/policy-request.d.ts +41 -0
  113. package/dist/policy/policy-request.js +63 -0
  114. package/dist/policy/policy-session.d.ts +106 -0
  115. package/dist/policy/policy-session.js +295 -0
  116. package/dist/policy/policy-startup.validator.d.ts +10 -0
  117. package/dist/policy/policy-startup.validator.js +112 -0
  118. package/dist/policy/policy-write.runtime.d.ts +39 -0
  119. package/dist/policy/policy-write.runtime.js +532 -0
  120. package/dist/prebuilt/core-version.generated.d.ts +2 -0
  121. package/dist/prebuilt/core-version.generated.js +1 -0
  122. package/dist/prebuilt/prebuilt-changes.d.ts +29 -0
  123. package/dist/prebuilt/prebuilt-changes.js +65 -0
  124. package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
  125. package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
  126. package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
  127. package/dist/prebuilt/prebuilt-contracts.js +1 -0
  128. package/dist/prebuilt/prebuilt-contracts.loader.d.ts +19 -0
  129. package/dist/prebuilt/prebuilt-contracts.loader.js +77 -0
  130. package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
  131. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +229 -0
  132. package/dist/protocol/absent-route.responder.d.ts +9 -27
  133. package/dist/protocol/absent-route.responder.js +36 -3
  134. package/dist/protocol/contract-document.responder.d.ts +2 -1
  135. package/dist/protocol/http-request.decoder.d.ts +19 -3
  136. package/dist/protocol/http-request.decoder.js +24 -1
  137. package/dist/protocol/http-request.reader.d.ts +6 -3
  138. package/dist/protocol/http-request.reader.js +1 -0
  139. package/dist/protocol/http-response.encoder.js +2 -2
  140. package/dist/protocol/protocol-headers.d.ts +3 -3
  141. package/dist/protocol/protocol-surface.d.ts +6 -1
  142. package/dist/protocol/protocol-surface.js +7 -0
  143. package/dist/protocol/protocol.binder.d.ts +16 -1
  144. package/dist/protocol/protocol.binder.js +4 -1
  145. package/dist/protocol/protocol.handler.d.ts +12 -1
  146. package/dist/protocol/protocol.handler.js +86 -17
  147. package/dist/runtime/caller-data.copier.d.ts +18 -3
  148. package/dist/runtime/caller-data.copier.js +21 -8
  149. package/dist/runtime/caller-data.door.d.ts +13 -6
  150. package/dist/runtime/computed-read-runtime.d.ts +6 -1
  151. package/dist/runtime/computed-read-runtime.js +232 -45
  152. package/dist/runtime/computed-value.error.d.ts +3 -2
  153. package/dist/runtime/computed-value.error.js +3 -2
  154. package/dist/runtime/context-executor.runtime.d.ts +4 -3
  155. package/dist/runtime/context-executor.runtime.js +4 -2
  156. package/dist/runtime/core-class.brand.d.ts +10 -0
  157. package/dist/runtime/core-class.brand.js +21 -0
  158. package/dist/runtime/create-requirement.d.ts +31 -5
  159. package/dist/runtime/create-requirement.js +129 -21
  160. package/dist/runtime/decimal.d.ts +14 -0
  161. package/dist/runtime/decimal.js +21 -0
  162. package/dist/runtime/diagnostics-reporter.d.ts +44 -9
  163. package/dist/runtime/diagnostics-reporter.js +13 -1
  164. package/dist/runtime/error-boundary.d.ts +9 -2
  165. package/dist/runtime/error-boundary.js +12 -0
  166. package/dist/runtime/error-guidance.d.ts +12 -0
  167. package/dist/runtime/error-guidance.js +1 -0
  168. package/dist/runtime/error.catalog.d.ts +49 -0
  169. package/dist/runtime/error.catalog.js +49 -0
  170. package/dist/runtime/execution-options.d.ts +8 -0
  171. package/dist/runtime/field-check.evaluator.d.ts +0 -2
  172. package/dist/runtime/field-check.evaluator.js +1 -1
  173. package/dist/runtime/field-compute.d.ts +11 -10
  174. package/dist/runtime/field-stage.runtime.d.ts +3 -1
  175. package/dist/runtime/field-stage.runtime.js +8 -4
  176. package/dist/runtime/field-validator.runtime.d.ts +19 -1
  177. package/dist/runtime/field-validator.runtime.js +47 -8
  178. package/dist/runtime/framework-error.js +9 -0
  179. package/dist/runtime/framework-warning.emitter.d.ts +6 -0
  180. package/dist/runtime/framework-warning.emitter.js +8 -0
  181. package/dist/runtime/json-value.d.ts +10 -5
  182. package/dist/runtime/level-lifecycle.runtime.d.ts +4 -3
  183. package/dist/runtime/level-lifecycle.runtime.js +3 -5
  184. package/dist/runtime/mutation-planner.d.ts +1 -6
  185. package/dist/runtime/mutation-planner.js +3 -14
  186. package/dist/runtime/operation-code.guidance.d.ts +6 -0
  187. package/dist/runtime/operation-code.guidance.js +167 -0
  188. package/dist/runtime/operation-request.d.ts +16 -9
  189. package/dist/runtime/operation-runtime.d.ts +35 -17
  190. package/dist/runtime/operation-runtime.js +220 -49
  191. package/dist/runtime/outcome-mapper.d.ts +2 -1
  192. package/dist/runtime/output-validator.d.ts +1 -1
  193. package/dist/runtime/owned-execution.registry.d.ts +13 -3
  194. package/dist/runtime/pipe-output.validator.d.ts +10 -0
  195. package/dist/runtime/pipe-output.validator.js +4 -2
  196. package/dist/runtime/pipeline.d.ts +47 -22
  197. package/dist/runtime/readonly-value.d.ts +20 -5
  198. package/dist/runtime/readonly-value.js +74 -26
  199. package/dist/runtime/referential-action.d.ts +1 -1
  200. package/dist/runtime/scalar-runtime.d.ts +4 -11
  201. package/dist/runtime/scalar-runtime.js +91 -22
  202. package/dist/runtime/unsent-value.normalizer.d.ts +15 -0
  203. package/dist/runtime/unsent-value.normalizer.js +128 -0
  204. package/dist/transactions/operation-arguments-encoder.js +8 -3
  205. package/dist/transactions/operation-fingerprint.js +2 -2
  206. package/dist/transactions/sql-statement.d.ts +10 -2
  207. package/dist/transactions/sql-statement.validator.d.ts +2 -2
  208. package/dist/transactions/sql-statement.validator.js +38 -5
  209. package/dist/transactions/transaction-envelope.d.ts +5 -4
  210. package/dist/transactions/transaction-integrity-error.js +2 -0
  211. package/dist/transactions/transaction-plan-assembler.d.ts +12 -5
  212. package/dist/transactions/transaction-plan-assembler.js +48 -10
  213. package/dist/transactions/transaction-plan-validator.d.ts +23 -7
  214. package/dist/transactions/transaction-plan-validator.js +27 -1
  215. package/dist/transactions/transaction-reference-resolver.d.ts +3 -2
  216. package/dist/transactions/transaction-reference-validator.d.ts +43 -3
  217. package/dist/transactions/transaction-reference-validator.js +83 -57
  218. package/dist/transactions/transaction-reference.d.ts +1 -1
  219. package/dist/transactions/transaction-reference.js +5 -0
  220. package/dist/transactions/transaction-request.d.ts +8 -8
  221. package/dist/transactions/transaction-runtime.d.ts +5 -2
  222. package/dist/transactions/transaction-runtime.js +79 -27
  223. package/dist/transactions/transaction-settlement.runtime.d.ts +4 -3
  224. package/dist/transactions/transaction-slot-path-error.js +2 -0
  225. package/dist/transactions/transaction-stage.runtime.d.ts +5 -3
  226. package/dist/transactions/transaction-stage.runtime.js +7 -4
  227. package/package.json +6 -5
  228. package/src/runtime/field-check.evaluator.ts +1 -1
@@ -0,0 +1,545 @@
1
+ import { adapterPrimaryIdentifier } from "../adapters/adapter-identifier.js";
2
+ import { toOneRelinkWithRewrite } from "../contracts/capability.js";
3
+ import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
4
+ import { isRelationMutationDirective } from "../runtime/mutation-planner.js";
5
+ import { frozenCopy } from "../runtime/readonly-value.js";
6
+ import { policyRefusal } from "./policy-admission.js";
7
+ import { afterUpdate, beforeFields, checkChanged, coveredBefore, rowFieldsOf, unchangedSince, } from "./policy-after-update.runtime.js";
8
+ import { allOf, noneOf, someOf, whereOf, } from "./policy-condition.js";
9
+ import { NamedRows } from "./policy-named-rows.js";
10
+ import { identifiedRows } from "./policy-read.runtime.js";
11
+ import { isHeldTarget } from "./policy-reference-write.runtime.js";
12
+ import { fieldAccess } from "./policy-session.js";
13
+ function isRecord(value) {
14
+ return value !== null && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+ function isPlainObject(value) {
17
+ if (!isRecord(value)) {
18
+ return false;
19
+ }
20
+ const prototype = Object.getPrototypeOf(value);
21
+ return prototype === Object.prototype || prototype === null;
22
+ }
23
+ function namesReference(selector, references) {
24
+ if (references === undefined) {
25
+ return false;
26
+ }
27
+ return (Object.keys(selector).length === references.length &&
28
+ references.every((reference) => {
29
+ const value = Object.hasOwn(selector, reference)
30
+ ? selector[reference]
31
+ : undefined;
32
+ return (value !== undefined &&
33
+ value !== null &&
34
+ !Array.isArray(value) &&
35
+ !isPlainObject(value));
36
+ }));
37
+ }
38
+ const DROPPED = Symbol("dropped");
39
+ function rewritten(value, replaced) {
40
+ if (value !== null && typeof value === "object" && replaced.has(value)) {
41
+ return replaced.get(value);
42
+ }
43
+ if (Array.isArray(value)) {
44
+ const entries = value.map((entry) => rewritten(entry, replaced));
45
+ return entries.every((entry, index) => entry === value[index])
46
+ ? value
47
+ : entries.filter((entry) => entry !== DROPPED);
48
+ }
49
+ if (!isPlainObject(value)) {
50
+ return value;
51
+ }
52
+ let changed = false;
53
+ const copy = {};
54
+ for (const [key, member] of Object.entries(value)) {
55
+ const next = rewritten(member, replaced);
56
+ changed ||= next !== member;
57
+ copy[key] = next === DROPPED ? [] : next;
58
+ }
59
+ return changed ? copy : value;
60
+ }
61
+ function conditionOf(where) {
62
+ return isRecord(where) && Object.keys(where).length > 0 ? where : true;
63
+ }
64
+ function relationOf(core, resource, name) {
65
+ const field = lookupField(lookupResource(core, resource), name);
66
+ return field?.kind === "relation"
67
+ ? field
68
+ : undefined;
69
+ }
70
+ export function writesNested(core, resource, record) {
71
+ return (isRecord(record) &&
72
+ Object.entries(record).some(([key, value]) => isRecord(value) &&
73
+ relationOf(core, resource, key) !== undefined &&
74
+ Object.keys(value).some(isRelationMutationDirective)));
75
+ }
76
+ class NestedRefusal extends Error {
77
+ name = "NestedRefusal";
78
+ }
79
+ const QUESTION_MESSAGES = {
80
+ link: (resource) => `The policy does not allow this nested write to link or unlink a row of Resource "${resource}" outside what the caller may change or read.`,
81
+ update: (resource) => `The policy does not allow this nested write to change a row of Resource "${resource}" outside what the caller may change.`,
82
+ delete: (resource) => `The policy does not allow this nested write to delete a row of Resource "${resource}" outside what the caller may delete.`,
83
+ fields: (resource) => `The policy does not allow this nested write to write a field of Resource "${resource}" on every row it reaches.`,
84
+ };
85
+ function linkedTo(inverse, parents) {
86
+ if (parents === false) {
87
+ return false;
88
+ }
89
+ const reach = whereOf(parents);
90
+ return inverse.cardinality === "one"
91
+ ? { [inverse.name]: reach }
92
+ : { [inverse.name]: { some: reach } };
93
+ }
94
+ class NestedWalk {
95
+ session;
96
+ core;
97
+ model;
98
+ questions = new Map();
99
+ named = new Map();
100
+ replaced = new Map();
101
+ held = [];
102
+ constructor(session, core, model) {
103
+ this.session = session;
104
+ this.core = core;
105
+ this.model = model;
106
+ }
107
+ ask(resource, kind, condition) {
108
+ if (condition === false) {
109
+ return;
110
+ }
111
+ const key = `${resource}\u0000${kind}`;
112
+ let question = this.questions.get(key);
113
+ if (question === undefined) {
114
+ question = { resource, kind, conditions: [] };
115
+ this.questions.set(key, question);
116
+ }
117
+ question.conditions.push(condition);
118
+ }
119
+ name(resource, selector, scope) {
120
+ let rows = this.named.get(resource);
121
+ if (rows === undefined) {
122
+ rows = new NamedRows();
123
+ this.named.set(resource, rows);
124
+ }
125
+ rows.add(selector, scope);
126
+ }
127
+ async answer() {
128
+ for (const [resource, rows] of this.named) {
129
+ if (rows.size > 0 && !(await rows.within(this.session, resource))) {
130
+ return policyRefusal(QUESTION_MESSAGES.link(resource));
131
+ }
132
+ }
133
+ for (const { resource, kind, conditions } of this.questions.values()) {
134
+ const reach = someOf(conditions);
135
+ if (reach !== false &&
136
+ (await this.session.exists(resource, whereOf(reach)))) {
137
+ return policyRefusal(QUESTION_MESSAGES[kind](resource));
138
+ }
139
+ }
140
+ return undefined;
141
+ }
142
+ identifier(resource) {
143
+ const identifier = adapterPrimaryIdentifier(this.model, resource);
144
+ if (identifier.length === 0) {
145
+ throw new NestedRefusal(`The policy checks the rows a nested write reaches in Resource "${resource}" by identifier, and the Resource has none.`);
146
+ }
147
+ return identifier;
148
+ }
149
+ async grantsOf(resource, records, action) {
150
+ const fields = await this.session.fields(resource, {
151
+ access: "write",
152
+ action,
153
+ });
154
+ const grants = new Map();
155
+ for (const record of records) {
156
+ for (const [name, value] of Object.entries(record)) {
157
+ if (value === undefined) {
158
+ continue;
159
+ }
160
+ const relation = relationOf(this.core, resource, name);
161
+ for (const member of [name, ...(relation?.referenceFields ?? [])]) {
162
+ const access = fieldAccess(this.core, resource, fields, member);
163
+ if (access === "never") {
164
+ throw new NestedRefusal(`The policy does not allow writing field "${member}" of Resource "${resource}".`);
165
+ }
166
+ if (access !== "always") {
167
+ grants.set(member, access);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ return [...grants.values()];
173
+ }
174
+ async level(resource, parent, records) {
175
+ const relations = new Map();
176
+ for (const record of records) {
177
+ for (const [key, value] of Object.entries(record)) {
178
+ const relation = isRecord(value)
179
+ ? relationOf(this.core, resource, key)
180
+ : undefined;
181
+ if (!isRecord(value) || relation === undefined) {
182
+ continue;
183
+ }
184
+ const paired = relation.cardinality === "one"
185
+ ? toOneRelinkWithRewrite(Object.fromEntries(Object.entries(value).filter(([, operand]) => operand !== undefined)))
186
+ : undefined;
187
+ if (paired !== undefined) {
188
+ throw new NestedRefusal(`Relation "${key}" of Resource "${resource}" pairs ${paired.rewrite.join("/")} with ${paired.relink.join("/")}: the row it would update is the newly linked one, which the policy cannot check before the write.`);
189
+ }
190
+ for (const [directive, operand] of Object.entries(value)) {
191
+ if (isRelationMutationDirective(directive) && operand !== undefined) {
192
+ let operands = relations.get(key);
193
+ if (operands === undefined) {
194
+ operands = [];
195
+ relations.set(key, operands);
196
+ }
197
+ operands.push([directive, operand]);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ const checks = [];
203
+ for (const [name, operands] of relations) {
204
+ checks.push(...(await this.relation(resource, parent, name, operands)));
205
+ }
206
+ return checks;
207
+ }
208
+ async relation(resource, parent, name, operands) {
209
+ const core = this.core;
210
+ const relation = relationOf(core, resource, name);
211
+ const target = relation.target;
212
+ const back = relation.inverse === undefined
213
+ ? undefined
214
+ : relationOf(core, target, relation.inverse);
215
+ const inverse = back === undefined || relation.inverse === undefined
216
+ ? undefined
217
+ : { name: relation.inverse, cardinality: back.cardinality };
218
+ const throughInverse = () => {
219
+ if (inverse === undefined) {
220
+ throw new NestedRefusal(`The policy checks the rows relation "${name}" of Resource "${resource}" reaches through its other end, and the relation states none.`);
221
+ }
222
+ return inverse;
223
+ };
224
+ let reached;
225
+ const linked = () => {
226
+ reached ??=
227
+ parent.kind === "new"
228
+ ? false
229
+ : linkedTo(throughInverse(), parent.reach);
230
+ return reached;
231
+ };
232
+ const read = await this.session.scope(target, "read");
233
+ const change = allOf(await this.session.scope(target, "update"), read);
234
+ const remove = allOf(await this.session.scope(target, "delete"), read);
235
+ const targetHolds = relation.referenceFields === undefined;
236
+ const unheld = !targetHolds && !isHeldTarget(core, this.session.unheld, target);
237
+ const linkScope = targetHolds ? change : unheld ? true : read;
238
+ const selectorScope = (selector) => unheld && !namesReference(selector, relation.references)
239
+ ? read
240
+ : linkScope;
241
+ const many = relation.cardinality === "many";
242
+ const selectors = [];
243
+ const created = [];
244
+ const checks = [];
245
+ const changing = [];
246
+ const link = (selector, named = true) => {
247
+ if (isRecord(selector)) {
248
+ selectors.push(selector);
249
+ const scope = selectorScope(selector);
250
+ if (named) {
251
+ this.name(target, selector, scope);
252
+ }
253
+ else {
254
+ this.ask(target, "link", allOf(selector, noneOf(scope)));
255
+ }
256
+ if (targetHolds) {
257
+ changing.push(selector);
258
+ }
259
+ }
260
+ };
261
+ const unlinkLinked = (except) => {
262
+ const unlinked = allOf(linked(), noneOf(except));
263
+ this.ask(target, "link", allOf(unlinked, noneOf(linkScope)));
264
+ if (targetHolds) {
265
+ changing.push(unlinked);
266
+ }
267
+ };
268
+ const entriesOf = (operand) => many && Array.isArray(operand) ? operand : [operand];
269
+ for (const [directive, operand] of operands) {
270
+ if (!many &&
271
+ targetHolds &&
272
+ parent.kind === "existing" &&
273
+ (directive === "$create" ||
274
+ directive === "$connect" ||
275
+ directive === "$connectOrCreate" ||
276
+ directive === "$set")) {
277
+ unlinkLinked(false);
278
+ }
279
+ switch (directive) {
280
+ case "$create":
281
+ created.push(...entriesOf(operand).filter(isRecord));
282
+ break;
283
+ case "$connect":
284
+ entriesOf(operand).forEach((one) => {
285
+ link(one);
286
+ });
287
+ break;
288
+ case "$connectOrCreate":
289
+ for (const entry of entriesOf(operand)) {
290
+ if (isRecord(entry)) {
291
+ link(entry.where, false);
292
+ if (isRecord(entry.create)) {
293
+ created.push(entry.create);
294
+ }
295
+ }
296
+ }
297
+ break;
298
+ case "$disconnect":
299
+ if (many) {
300
+ entriesOf(operand).forEach((one) => {
301
+ link(one);
302
+ });
303
+ }
304
+ else {
305
+ unlinkLinked(false);
306
+ }
307
+ break;
308
+ case "$set":
309
+ if (!many) {
310
+ if (operand === null) {
311
+ unlinkLinked(false);
312
+ }
313
+ else {
314
+ link(operand);
315
+ }
316
+ break;
317
+ }
318
+ {
319
+ const kept = (Array.isArray(operand) ? operand : []).filter(isRecord);
320
+ kept.forEach((one) => {
321
+ link(one);
322
+ });
323
+ unlinkLinked(someOf(kept));
324
+ }
325
+ break;
326
+ case "$update":
327
+ for (const entry of entriesOf(operand)) {
328
+ if (isRecord(entry) && isRecord(entry.data)) {
329
+ checks.push(await this.changed(target, allOf(linked(), conditionOf(entry.where)), change, entry.data, many ? { kind: "narrow", entry } : { kind: "hold", entry }));
330
+ }
331
+ }
332
+ break;
333
+ case "$upsert":
334
+ for (const entry of entriesOf(operand)) {
335
+ if (!isRecord(entry)) {
336
+ continue;
337
+ }
338
+ const reach = allOf(linked(), conditionOf(entry.where));
339
+ if (reach !== false &&
340
+ isRecord(entry.update) &&
341
+ (await this.session.exists(target, whereOf(reach)))) {
342
+ checks.push(await this.changed(target, reach, change, entry.update));
343
+ }
344
+ else if (isRecord(entry.create)) {
345
+ created.push(entry.create);
346
+ }
347
+ }
348
+ break;
349
+ case "$delete":
350
+ for (const entry of entriesOf(operand)) {
351
+ const reach = allOf(linked(), conditionOf(isRecord(entry) ? entry.where : undefined));
352
+ if (many && isRecord(entry)) {
353
+ await this.narrow(entry, target, reach, remove);
354
+ }
355
+ else {
356
+ this.ask(target, "delete", allOf(reach, noneOf(remove)));
357
+ }
358
+ }
359
+ break;
360
+ }
361
+ }
362
+ if (changing.length > 0) {
363
+ checks.push(await this.relinked(target, inverse, read, changing));
364
+ }
365
+ if (created.length > 0) {
366
+ checks.push(await this.creates(resource, target, throughInverse(), linked(), selectors, created));
367
+ }
368
+ return checks;
369
+ }
370
+ async narrow(entry, resource, reach, scope, fields = this.identifier(resource), held = false) {
371
+ const identifier = this.identifier(resource);
372
+ const within = allOf(reach, scope);
373
+ const rows = within === false
374
+ ? []
375
+ : await this.session.rows(resource, whereOf(within), fields);
376
+ const where = held
377
+ ? unchangedSince(this.core, this.model, resource, rows)
378
+ : identifiedRows(this.core, identifier, resource, rows);
379
+ this.replaced.set(entry, where === false ? DROPPED : { ...entry, where: whereOf(where) });
380
+ return rows;
381
+ }
382
+ hold(entry, resource, rows) {
383
+ const unchanged = unchangedSince(this.core, this.model, resource, rows);
384
+ const where = typeof unchanged === "boolean"
385
+ ? false
386
+ : allOf(conditionOf(entry.where), unchanged);
387
+ if (where === false) {
388
+ return;
389
+ }
390
+ this.replaced.set(entry, { ...entry, where: whereOf(where) });
391
+ this.held.push({ resource, rows });
392
+ }
393
+ async failed() {
394
+ const fresh = this.session.renewed();
395
+ for (const { resource, rows } of this.held) {
396
+ const changed = allOf(identifiedRows(this.core, this.identifier(resource), resource, rows), noneOf(unchangedSince(this.core, this.model, resource, rows)));
397
+ if (changed !== false &&
398
+ (await fresh.exists(resource, whereOf(changed)))) {
399
+ return policyRefusal(`The policy checks this nested update of Resource "${resource}" against the row as it was before it, and a concurrent write changed that row before it ran, so it was not applied.`);
400
+ }
401
+ }
402
+ return undefined;
403
+ }
404
+ async changed(resource, reach, scope, data, directive) {
405
+ const identifier = this.identifier(resource);
406
+ const after = await afterUpdate(this.session, this.core, this.model, resource, data, true);
407
+ const fields = beforeFields(this.core, this.model, resource, after);
408
+ let rows;
409
+ if (directive?.kind !== "narrow") {
410
+ this.ask(resource, "update", allOf(reach, noneOf(scope)));
411
+ rows =
412
+ reach === false
413
+ ? []
414
+ : await this.session.rows(resource, whereOf(reach), fields);
415
+ if (directive !== undefined && after?.before === true) {
416
+ this.hold(directive.entry, resource, rows);
417
+ }
418
+ }
419
+ else {
420
+ rows = await this.narrow(directive.entry, resource, reach, scope, fields, after?.before === true);
421
+ reach =
422
+ rows.length === 0
423
+ ? false
424
+ : identifiedRows(this.core, identifier, resource, rows);
425
+ }
426
+ const grants = await this.grantsOf(resource, [data], "update");
427
+ if (grants.length > 0) {
428
+ this.ask(resource, "fields", allOf(reach, someOf(grants.map(noneOf))));
429
+ }
430
+ const before = await coveredBefore(this.session, this.core, this.model, resource, after, rows);
431
+ const inner = await this.level(resource, { kind: "existing", reach }, [
432
+ data,
433
+ ]);
434
+ return async () => {
435
+ if (after !== undefined) {
436
+ const refused = await checkChanged(this.session, this.core, this.model, resource, after, rows, false, before);
437
+ if (refused !== undefined) {
438
+ return refused;
439
+ }
440
+ }
441
+ return runChecks(inner, rows);
442
+ };
443
+ }
444
+ async relinked(resource, inverse, read, changing) {
445
+ this.identifier(resource);
446
+ const reach = someOf(changing);
447
+ const after = (inverse === undefined
448
+ ? undefined
449
+ : await afterUpdate(this.session, this.core, this.model, resource, { [inverse.name]: {} }, true)) ?? {
450
+ moveOut: read === true ? undefined : read,
451
+ after: true,
452
+ before: this.session.checksAfter(resource, "update"),
453
+ };
454
+ const rows = reach === false
455
+ ? []
456
+ : await this.session.rows(resource, whereOf(reach), beforeFields(this.core, this.model, resource, after));
457
+ const before = await coveredBefore(this.session, this.core, this.model, resource, after, rows);
458
+ return () => checkChanged(this.session, this.core, this.model, resource, after, rows, false, before);
459
+ }
460
+ async creates(parentResource, resource, inverse, linked, selectors, records) {
461
+ const identifier = this.identifier(resource);
462
+ const parentIdentifier = this.identifier(parentResource);
463
+ const there = someOf([linked, ...selectors]);
464
+ const before = there === false
465
+ ? []
466
+ : await this.session.rows(resource, whereOf(there), identifier);
467
+ const grants = await this.grantsOf(resource, records, "create");
468
+ const inner = await this.level(resource, { kind: "new" }, records);
469
+ return async (parents) => {
470
+ if (parents.length === 0) {
471
+ return undefined;
472
+ }
473
+ const fresh = this.session.renewed();
474
+ const reach = allOf(linkedTo(inverse, identifiedRows(this.core, parentIdentifier, parentResource, parents)), before.length === 0
475
+ ? true
476
+ : noneOf(identifiedRows(this.core, identifier, resource, before)));
477
+ const rows = reach === false
478
+ ? []
479
+ : await fresh.rows(resource, whereOf(reach), [
480
+ ...new Set([...identifier, ...rowFieldsOf(this.core, resource)]),
481
+ ]);
482
+ const refused = await checkCreatedRows(fresh, this.core, identifier, resource, grants, rows);
483
+ return refused ?? runChecks(inner, rows);
484
+ };
485
+ }
486
+ }
487
+ async function runChecks(checks, rows) {
488
+ for (const check of checks) {
489
+ const refused = await check(rows);
490
+ if (refused !== undefined) {
491
+ return refused;
492
+ }
493
+ }
494
+ return undefined;
495
+ }
496
+ async function checkCreatedRows(session, core, identifier, resource, grants, rows) {
497
+ if (rows.length === 0) {
498
+ return undefined;
499
+ }
500
+ const answers = await Promise.all(rows.map((row) => session.can(resource, "create", frozenCopy(row))));
501
+ if (answers.includes(false)) {
502
+ return policyRefusal(`The policy does not allow creating this row of Resource "${resource}".`);
503
+ }
504
+ const missed = allOf(identifiedRows(core, identifier, resource, rows), someOf(grants.map(noneOf)));
505
+ if (missed !== false && (await session.exists(resource, whereOf(missed)))) {
506
+ return policyRefusal(`The policy does not allow writing a field of Resource "${resource}" on the row this request creates.`);
507
+ }
508
+ return undefined;
509
+ }
510
+ export async function planNestedWrites(session, core, model, resource, record, parent) {
511
+ const walk = new NestedWalk(session, core, model);
512
+ try {
513
+ const identifier = adapterPrimaryIdentifier(model, resource);
514
+ if (identifier.length === 0) {
515
+ throw new NestedRefusal(`The policy checks the rows a nested write reaches by identifier, and Resource "${resource}" has none.`);
516
+ }
517
+ const checks = await walk.level(resource, parent, [record]);
518
+ const refused = await walk.answer();
519
+ if (refused !== undefined) {
520
+ return { kind: "refused", response: refused };
521
+ }
522
+ return {
523
+ kind: "planned",
524
+ record: rewritten(record, walk.replaced),
525
+ failed: () => walk.failed(),
526
+ after: async (rows) => {
527
+ if (parent.kind === "new" && rows.length > 0) {
528
+ const fresh = session.renewed();
529
+ const stored = await fresh.rows(resource, whereOf(identifiedRows(core, identifier, resource, rows)), [...new Set([...identifier, ...rowFieldsOf(core, resource)])]);
530
+ const refusal = await checkCreatedRows(fresh, core, identifier, resource, [], stored);
531
+ if (refusal !== undefined) {
532
+ return refusal;
533
+ }
534
+ }
535
+ return runChecks(checks, rows);
536
+ },
537
+ };
538
+ }
539
+ catch (error) {
540
+ if (error instanceof NestedRefusal) {
541
+ return { kind: "refused", response: policyRefusal(error.message) };
542
+ }
543
+ throw error;
544
+ }
545
+ }
@@ -0,0 +1,81 @@
1
+ import type { AvAdapterModel } from "../adapters/adapter-model.js";
2
+ import type { AvContractCore } from "../contracts/contract.js";
3
+ import type { AvOperationIdentity } from "../operations/operation-identity.js";
4
+ import type { PlannedReadOperation, ReadProjectionPlan } from "../runtime/computed-read-runtime.js";
5
+ import type { AvOperationFailureResponse } from "../runtime/operation-response.js";
6
+ import { type PolicyCondition } from "./policy-condition.js";
7
+ import { type PolicySession } from "./policy-session.js";
8
+ /** The answer once the policy checked it: its rows, or the refusal. */
9
+ export type PolicySettlement = {
10
+ readonly ok: true;
11
+ readonly data: unknown;
12
+ } | {
13
+ readonly ok: false;
14
+ readonly response: AvOperationFailureResponse;
15
+ };
16
+ /** What one read runs once its policy is applied. */
17
+ export type PolicyRead = {
18
+ /** The policy refuses the read before it runs. */
19
+ readonly kind: "refused";
20
+ readonly response: AvOperationFailureResponse;
21
+ } | {
22
+ /** The read reaches no row: its answer, without the adapter. */
23
+ readonly kind: "empty";
24
+ readonly data: unknown;
25
+ } | {
26
+ readonly kind: "run";
27
+ /** The operation the adapter runs: the request's own, or a unique read's first-style form. */
28
+ readonly identity: AvOperationIdentity;
29
+ readonly arguments: unknown;
30
+ readonly plan: PlannedReadOperation;
31
+ /**
32
+ * What the returned rows are checked for after the adapter answers (an
33
+ * `exists` per level): the root level's conditions, each a row it must
34
+ * not meet, and whether a level below the root is checked.
35
+ */
36
+ readonly checks: {
37
+ readonly root: readonly PolicyCondition[];
38
+ readonly nested: boolean;
39
+ };
40
+ /** The returned rows checked, and every emptied relation filled. */
41
+ readonly settle: (data: unknown) => Promise<PolicySettlement>;
42
+ };
43
+ /**
44
+ * How a write's root rows are reached (`policy-write.runtime.ts`): the
45
+ * condition ANDed into its `where`, or `undefined` for a write whose `where`
46
+ * stays as sent (a create has none, an upsert's is checked before it runs);
47
+ * and whether each returned row carries its identifier, for the checks that
48
+ * follow the write.
49
+ */
50
+ type PolicyRootRows = {
51
+ readonly scope: PolicyCondition | undefined;
52
+ readonly identified: boolean;
53
+ /**
54
+ * The fields each returned root row carries besides the caller's projection (its
55
+ * identifier among them when `identified`): what the after check asks `can` about,
56
+ * read from the write's own answer rather than read again. Trimmed before the
57
+ * caller sees it.
58
+ */
59
+ readonly carried?: readonly string[];
60
+ };
61
+ type Row = Readonly<Record<string, unknown>>;
62
+ /**
63
+ * One read's policy (see the module comment): what the adapter runs, and the
64
+ * checks its answer goes through; or the empty answer when the read reaches no
65
+ * row. A write's answer is read the same way, its root reached as `rows`
66
+ * says (`policy-write.runtime.ts`); a read's root is reached by its read scope.
67
+ */
68
+ export declare function enforceReadPolicy(session: PolicySession, core: AvContractCore, model: AvAdapterModel, request: AvOperationIdentity & {
69
+ readonly resource: string;
70
+ }, plan: PlannedReadOperation, adapterArguments: unknown, rows?: PolicyRootRows): Promise<PolicyRead>;
71
+ /**
72
+ * What the rows an answer returns are checked for after the adapter answers, by
73
+ * the projection level that returns them: each level's conditions, each a row
74
+ * it must not meet (a to-one level's rows outside their read scope, a level's
75
+ * rows outside the grant of a field it returns that is readable on some rows
76
+ * only). Empty when nothing is checked after the answer.
77
+ */
78
+ export declare function returnedRowChecks(session: PolicySession, core: AvContractCore, plan: PlannedReadOperation): Promise<Map<ReadProjectionPlan, readonly PolicyCondition[]>>;
79
+ /** A condition naming exactly `rows` of `resource` by their identifier. */
80
+ export declare function identifiedRows(core: AvContractCore, identifier: readonly string[], resource: string, rows: readonly Row[]): PolicyCondition;
81
+ export {};