@aventara/core 1.0.0-rc.2 → 1.0.0-rc.4

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 (78) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/ERRORS.md +9 -9
  3. package/README.md +72 -12
  4. package/dist/adapters/adapter-operation.d.ts +7 -0
  5. package/dist/compiler/contract.compiler.d.ts +6 -0
  6. package/dist/compiler/contract.compiler.js +73 -12
  7. package/dist/config/computed-fields.js +2 -9
  8. package/dist/config/config-diagnostic.guidance.js +3 -3
  9. package/dist/config/config.resolver.d.ts +14 -0
  10. package/dist/config/config.resolver.js +31 -0
  11. package/dist/config/config.validator.js +82 -0
  12. package/dist/config/exact-framework-config.d.ts +8 -1
  13. package/dist/config/framework-config.d.ts +26 -1
  14. package/dist/contracts/client-contract-structure.validator.d.ts +8 -0
  15. package/dist/contracts/client-contract-structure.validator.js +6 -1
  16. package/dist/contracts/field.d.ts +11 -0
  17. package/dist/framework/contract-view.d.ts +41 -10
  18. package/dist/framework/framework.factory.js +39 -11
  19. package/dist/framework/operation-facade.d.ts +24 -5
  20. package/dist/framework/operation-facade.js +2 -1
  21. package/dist/index.d.ts +4 -1
  22. package/dist/internal.d.ts +1 -0
  23. package/dist/internal.js +1 -0
  24. package/dist/operations/database-refusal.d.ts +1 -1
  25. package/dist/operations/database-refusal.js +1 -1
  26. package/dist/operations/operation-arguments.d.ts +1 -1
  27. package/dist/operations/operation-call.d.ts +9 -5
  28. package/dist/operations/operation-result.d.ts +5 -3
  29. package/dist/operations/operation-validator-core.js +66 -33
  30. package/dist/operations/validation-code.guidance.js +2 -2
  31. package/dist/policy/policy-admission.d.ts +22 -0
  32. package/dist/policy/policy-admission.js +400 -0
  33. package/dist/policy/policy-after-update.runtime.d.ts +29 -0
  34. package/dist/policy/policy-after-update.runtime.js +131 -0
  35. package/dist/policy/policy-condition.d.ts +15 -0
  36. package/dist/policy/policy-condition.js +36 -0
  37. package/dist/policy/policy-engine.d.ts +111 -0
  38. package/dist/policy/policy-engine.js +1 -0
  39. package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
  40. package/dist/policy/policy-nested-write.runtime.js +449 -0
  41. package/dist/policy/policy-read.runtime.d.ts +83 -0
  42. package/dist/policy/policy-read.runtime.js +494 -0
  43. package/dist/policy/policy-request.d.ts +37 -0
  44. package/dist/policy/policy-request.js +52 -0
  45. package/dist/policy/policy-session.d.ts +83 -0
  46. package/dist/policy/policy-session.js +234 -0
  47. package/dist/policy/policy-startup.validator.d.ts +8 -0
  48. package/dist/policy/policy-startup.validator.js +106 -0
  49. package/dist/policy/policy-write.runtime.d.ts +30 -0
  50. package/dist/policy/policy-write.runtime.js +394 -0
  51. package/dist/prebuilt/core-version.generated.d.ts +2 -0
  52. package/dist/prebuilt/core-version.generated.js +1 -0
  53. package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
  54. package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
  55. package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
  56. package/dist/prebuilt/prebuilt-contracts.js +1 -0
  57. package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
  58. package/dist/prebuilt/prebuilt-contracts.loader.js +122 -0
  59. package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
  60. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
  61. package/dist/runtime/computed-read-runtime.d.ts +6 -1
  62. package/dist/runtime/computed-read-runtime.js +224 -45
  63. package/dist/runtime/context-executor.runtime.js +4 -2
  64. package/dist/runtime/execution-options.d.ts +8 -0
  65. package/dist/runtime/field-validator.runtime.d.ts +9 -0
  66. package/dist/runtime/field-validator.runtime.js +36 -2
  67. package/dist/runtime/framework-warning.emitter.d.ts +6 -0
  68. package/dist/runtime/framework-warning.emitter.js +8 -0
  69. package/dist/runtime/level-lifecycle.runtime.js +3 -2
  70. package/dist/runtime/operation-code.guidance.js +4 -4
  71. package/dist/runtime/operation-request.d.ts +7 -0
  72. package/dist/runtime/operation-runtime.d.ts +9 -1
  73. package/dist/runtime/operation-runtime.js +119 -11
  74. package/dist/runtime/readonly-value.d.ts +5 -1
  75. package/dist/runtime/readonly-value.js +67 -28
  76. package/dist/transactions/transaction-runtime.d.ts +4 -1
  77. package/dist/transactions/transaction-runtime.js +27 -5
  78. package/package.json +2 -2
@@ -0,0 +1,494 @@
1
+ import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
+ import { policyRefusal } from "./policy-admission.js";
3
+ import { allOf, anyOf, everyOf, noneOf, someOf, } from "./policy-condition.js";
4
+ import { fieldAccess } from "./policy-session.js";
5
+ function isRecord(value) {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+ function relationOf(core, resource, name) {
9
+ const field = lookupField(lookupResource(core, resource), name);
10
+ return field?.kind === "relation"
11
+ ? field
12
+ : undefined;
13
+ }
14
+ export function identifierOf(model, resource) {
15
+ const resources = model.resources;
16
+ return ((Object.hasOwn(resources, resource)
17
+ ? resources[resource]?.identifiers?.[0]
18
+ : undefined) ?? []);
19
+ }
20
+ function innerFilters(relation, value) {
21
+ return relation.cardinality === "many"
22
+ ? [value.some, value.every, value.none]
23
+ : [value];
24
+ }
25
+ class ReadTargets {
26
+ core;
27
+ resources = new Set();
28
+ constructor(core) {
29
+ this.core = core;
30
+ }
31
+ where(resource, where) {
32
+ if (!isRecord(where)) {
33
+ return;
34
+ }
35
+ for (const [key, value] of Object.entries(where)) {
36
+ if (key === "AND" || key === "OR" || key === "NOT") {
37
+ for (const entry of Array.isArray(value) ? value : [value]) {
38
+ this.where(resource, entry);
39
+ }
40
+ continue;
41
+ }
42
+ const relation = relationOf(this.core, resource, key);
43
+ if (relation === undefined || !isRecord(value)) {
44
+ continue;
45
+ }
46
+ this.resources.add(relation.target);
47
+ for (const filter of innerFilters(relation, value)) {
48
+ this.where(relation.target, filter);
49
+ }
50
+ }
51
+ }
52
+ orderBy(resource, orderBy) {
53
+ for (const entry of Array.isArray(orderBy) ? orderBy : [orderBy]) {
54
+ if (!isRecord(entry)) {
55
+ continue;
56
+ }
57
+ for (const [key, value] of Object.entries(entry)) {
58
+ const relation = relationOf(this.core, resource, key);
59
+ if (relation !== undefined) {
60
+ this.resources.add(relation.target);
61
+ this.orderBy(relation.target, value);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ select(resource, select) {
67
+ if (!Array.isArray(select)) {
68
+ return;
69
+ }
70
+ for (const entry of select) {
71
+ if (!isRecord(entry)) {
72
+ continue;
73
+ }
74
+ for (const [name, value] of Object.entries(entry)) {
75
+ const relation = relationOf(this.core, resource, name);
76
+ if (relation === undefined) {
77
+ continue;
78
+ }
79
+ this.resources.add(relation.target);
80
+ if (isRecord(value)) {
81
+ this.where(relation.target, value.where);
82
+ this.orderBy(relation.target, value.orderBy);
83
+ this.select(relation.target, value.select);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ class ScopedWhere {
90
+ core;
91
+ scopes;
92
+ constructor(core, scopes) {
93
+ this.core = core;
94
+ this.scopes = scopes;
95
+ }
96
+ scope(resource) {
97
+ return this.scopes.get(resource) ?? true;
98
+ }
99
+ of(resource, where) {
100
+ if (!isRecord(where)) {
101
+ return where;
102
+ }
103
+ let changed = false;
104
+ const conditions = [];
105
+ const kept = {};
106
+ for (const [key, value] of Object.entries(where)) {
107
+ if ((key === "AND" || key === "OR") && Array.isArray(value)) {
108
+ const parts = value.map((entry) => this.of(resource, entry));
109
+ if (parts.every((part, index) => part === value[index])) {
110
+ kept[key] = value;
111
+ continue;
112
+ }
113
+ changed = true;
114
+ conditions.push(key === "AND"
115
+ ? everyOf(parts)
116
+ : someOf(parts));
117
+ continue;
118
+ }
119
+ if (key === "NOT") {
120
+ const part = this.of(resource, value);
121
+ if (part === value) {
122
+ kept[key] = value;
123
+ continue;
124
+ }
125
+ changed = true;
126
+ conditions.push(noneOf(part));
127
+ continue;
128
+ }
129
+ const relation = relationOf(this.core, resource, key);
130
+ const filter = relation === undefined || !isRecord(value)
131
+ ? undefined
132
+ : this.relationFilter(key, relation, value);
133
+ if (filter === undefined) {
134
+ kept[key] = value;
135
+ continue;
136
+ }
137
+ changed = true;
138
+ conditions.push(...filter);
139
+ }
140
+ if (!changed) {
141
+ return where;
142
+ }
143
+ return everyOf([
144
+ Object.keys(kept).length === 0 ? true : kept,
145
+ ...conditions,
146
+ ]);
147
+ }
148
+ relationFilter(key, relation, value) {
149
+ const scope = this.scope(relation.target);
150
+ if (relation.cardinality === "one") {
151
+ const inner = this.of(relation.target, value);
152
+ if (scope === true && inner === value) {
153
+ return undefined;
154
+ }
155
+ const condition = allOf(inner, scope);
156
+ return [
157
+ typeof condition === "boolean"
158
+ ? condition && { [key]: {} }
159
+ : { [key]: condition },
160
+ ];
161
+ }
162
+ const inner = Object.keys(value).map((operator) => [operator, this.of(relation.target, value[operator])]);
163
+ if (scope === true &&
164
+ inner.every(([operator, condition]) => condition === value[operator])) {
165
+ return undefined;
166
+ }
167
+ return inner.map(([operator, raw]) => {
168
+ const condition = raw;
169
+ if (operator === "every") {
170
+ const every = anyOf(condition, noneOf(scope));
171
+ return typeof every === "boolean"
172
+ ? every || { [key]: { none: {} } }
173
+ : { [key]: { every } };
174
+ }
175
+ const scoped = allOf(condition, scope);
176
+ if (typeof scoped !== "boolean") {
177
+ return { [key]: { [operator]: scoped } };
178
+ }
179
+ if (operator === "some") {
180
+ return scoped && { [key]: { some: {} } };
181
+ }
182
+ return !scoped || { [key]: { none: {} } };
183
+ });
184
+ }
185
+ }
186
+ function emptyRelationValue(select) {
187
+ const entries = Array.isArray(select) ? select : [];
188
+ if (!entries.includes("$count")) {
189
+ return [];
190
+ }
191
+ return entries.some((entry) => entry !== "$count")
192
+ ? { data: [], count: 0 }
193
+ : { count: 0 };
194
+ }
195
+ function withIdentifier(select, identifier) {
196
+ const missing = identifier.filter((member) => !select.includes(member));
197
+ return missing.length === 0 ? select : [...select, ...missing];
198
+ }
199
+ export async function enforceReadPolicy(session, core, model, request, plan, adapterArguments, rows) {
200
+ const args = isRecord(adapterArguments) ? adapterArguments : {};
201
+ const root = request.resource;
202
+ const targets = new ReadTargets(core);
203
+ targets.resources.add(root);
204
+ targets.where(root, args.where);
205
+ targets.orderBy(root, args.orderBy);
206
+ targets.select(root, args.select);
207
+ const scopes = new Map(await Promise.all([...targets.resources].map(async (resource) => [resource, await session.scope(resource, "read")])));
208
+ const scoped = new ScopedWhere(core, scopes);
209
+ let sortRefusal = unscopedSort(core, scoped, root, args.orderBy);
210
+ const checks = await returnedRowChecks(session, core, plan);
211
+ let identity = request;
212
+ let rootWhere = args.where;
213
+ const scope = rows === undefined ? scoped.scope(root) : rows.scope;
214
+ if (scope === undefined) {
215
+ }
216
+ else if (request.variant === "unique") {
217
+ if (scope !== true) {
218
+ const within = allOf(args.where, scope);
219
+ if (within === false) {
220
+ return { kind: "empty", data: null };
221
+ }
222
+ const first = firstVariantOf(core, root, request.family);
223
+ if (rows !== undefined &&
224
+ (request.family === "update" || request.family === "delete")) {
225
+ rootWhere = { ...args.where, AND: [scope] };
226
+ }
227
+ else if (first !== undefined) {
228
+ identity = first;
229
+ rootWhere = within;
230
+ }
231
+ else if (!(await session.exists(root, within))) {
232
+ return { kind: "empty", data: null };
233
+ }
234
+ }
235
+ }
236
+ else {
237
+ const where = everyOf([
238
+ args.where === undefined
239
+ ? true
240
+ : scoped.of(root, args.where),
241
+ scope,
242
+ ]);
243
+ if (where === false) {
244
+ return {
245
+ kind: "empty",
246
+ data: request.variant === "many"
247
+ ? []
248
+ : request.variant === "count"
249
+ ? 0
250
+ : null,
251
+ };
252
+ }
253
+ rootWhere = where === true ? undefined : where;
254
+ }
255
+ const rewriteLevel = (resource, select, level) => {
256
+ const entries = [];
257
+ const emptied = [];
258
+ const nested = new Map();
259
+ for (const entry of Array.isArray(select) ? select : []) {
260
+ const [name] = isRecord(entry) ? Object.keys(entry) : [];
261
+ const relation = name === undefined ? undefined : relationOf(core, resource, name);
262
+ if (relation === undefined || name === undefined) {
263
+ entries.push(entry);
264
+ continue;
265
+ }
266
+ const value = isRecord(entry[name])
267
+ ? entry[name]
268
+ : {};
269
+ const nestedLevel = level?.relations.find((candidate) => candidate.field === name)?.projection;
270
+ let rewritten = { ...value };
271
+ sortRefusal ??= unscopedSort(core, scoped, relation.target, value.orderBy);
272
+ if (relation.cardinality === "many") {
273
+ const where = everyOf([
274
+ value.where === undefined
275
+ ? true
276
+ : scoped.of(relation.target, value.where),
277
+ scoped.scope(relation.target),
278
+ ]);
279
+ if (where === false) {
280
+ emptied.push({
281
+ field: name,
282
+ value: emptyRelationValue(value.select),
283
+ });
284
+ continue;
285
+ }
286
+ if (where === true) {
287
+ delete rewritten.where;
288
+ }
289
+ else {
290
+ rewritten.where = where;
291
+ }
292
+ }
293
+ const inner = rewriteLevel(relation.target, value.select, nestedLevel);
294
+ nested.set(name, inner);
295
+ rewritten = { ...rewritten, select: inner.select };
296
+ entries.push({ [name]: rewritten });
297
+ }
298
+ const checked = level !== undefined &&
299
+ (checks.has(level) ||
300
+ (level === plan.projection && rows?.identified === true));
301
+ const carried = level !== undefined && level === plan.projection
302
+ ? (rows?.carried ?? [])
303
+ : [];
304
+ return {
305
+ select: withIdentifier(checked
306
+ ? withIdentifier(entries, identifierOf(model, resource))
307
+ : entries, carried),
308
+ emptied,
309
+ nested,
310
+ };
311
+ };
312
+ const rewritten = rewriteLevel(root, args.select, plan.projection);
313
+ if (sortRefusal !== undefined) {
314
+ return { kind: "refused", response: policyRefusal(sortRefusal) };
315
+ }
316
+ const runArguments = { ...args };
317
+ if (rootWhere === undefined) {
318
+ delete runArguments.where;
319
+ }
320
+ else {
321
+ runArguments.where = rootWhere;
322
+ }
323
+ if (Array.isArray(args.select)) {
324
+ runArguments.select = rewritten.select;
325
+ }
326
+ const changesShape = checks.size > 0 || hasEmptied(rewritten);
327
+ return {
328
+ kind: "run",
329
+ identity,
330
+ arguments: runArguments,
331
+ plan: changesShape ||
332
+ rows?.identified === true ||
333
+ (rows?.carried?.length ?? 0) > 0
334
+ ? { ...plan, materialize: true }
335
+ : plan,
336
+ checks: {
337
+ root: plan.projection === undefined
338
+ ? []
339
+ : (checks.get(plan.projection) ?? []),
340
+ nested: [...checks.keys()].some((level) => level !== plan.projection),
341
+ },
342
+ settle: async (data) => (await rowsReadable(session, core, model, plan.projection, checks, data))
343
+ ? { ok: true, data: filledEmptied(rewritten, data) }
344
+ : {
345
+ ok: false,
346
+ response: policyRefusal("The policy does not allow reading every row or field this request returns."),
347
+ },
348
+ };
349
+ }
350
+ export async function returnedRowChecks(session, core, plan) {
351
+ const checks = new Map();
352
+ const collect = async (level, reachedOne) => {
353
+ if (level === undefined) {
354
+ return;
355
+ }
356
+ const fields = await session.fields(level.resource, "read");
357
+ const conditions = [];
358
+ if (reachedOne) {
359
+ const scope = await session.scope(level.resource, "read");
360
+ if (scope !== true) {
361
+ conditions.push(noneOf(scope));
362
+ }
363
+ }
364
+ for (const field of level.requested) {
365
+ const access = fieldAccess(core, level.resource, fields, field);
366
+ if (access !== "always" && access !== "never") {
367
+ conditions.push(noneOf(access));
368
+ }
369
+ }
370
+ if (conditions.length > 0) {
371
+ checks.set(level, conditions);
372
+ }
373
+ for (const relation of level.relations) {
374
+ await collect(relation.projection, relation.cardinality === "one");
375
+ }
376
+ };
377
+ await collect(plan.projection, false);
378
+ return checks;
379
+ }
380
+ function firstVariantOf(core, resource, family) {
381
+ const operations = lookupResource(core, resource)?.operations;
382
+ return family !== "create" &&
383
+ family !== "upsert" &&
384
+ operations?.[family]?.first !== undefined
385
+ ? { family, variant: "first" }
386
+ : undefined;
387
+ }
388
+ function hasEmptied(level) {
389
+ return (level.emptied.length > 0 ||
390
+ [...level.nested.values()].some((nested) => hasEmptied(nested)));
391
+ }
392
+ function unscopedSort(core, scoped, resource, orderBy) {
393
+ for (const entry of Array.isArray(orderBy) ? orderBy : [orderBy]) {
394
+ if (!isRecord(entry)) {
395
+ continue;
396
+ }
397
+ for (const key of Object.keys(entry)) {
398
+ const relation = relationOf(core, resource, key);
399
+ if (relation !== undefined && scoped.scope(relation.target) !== true) {
400
+ return `The policy does not allow sorting by relation "${key}" of Resource "${resource}": it reaches rows of "${relation.target}" this request may not read.`;
401
+ }
402
+ }
403
+ }
404
+ return undefined;
405
+ }
406
+ function rowsByLevel(level, rows, into) {
407
+ if (level === undefined) {
408
+ return;
409
+ }
410
+ const own = into.get(level) ?? [];
411
+ for (const row of rows) {
412
+ if (isRecord(row)) {
413
+ own.push(row);
414
+ }
415
+ }
416
+ into.set(level, own);
417
+ for (const relation of level.relations) {
418
+ const nested = [];
419
+ for (const row of own) {
420
+ const value = row[relation.field];
421
+ if (Array.isArray(value)) {
422
+ nested.push(...value);
423
+ }
424
+ else if (isRecord(value) && Array.isArray(value.data)) {
425
+ nested.push(...value.data);
426
+ }
427
+ else if (relation.cardinality === "one" && isRecord(value)) {
428
+ nested.push(value);
429
+ }
430
+ }
431
+ rowsByLevel(relation.projection, nested, into);
432
+ }
433
+ }
434
+ export function identifiedRows(core, identifier, resource, rows) {
435
+ const [single] = identifier;
436
+ if (identifier.length === 1 && single !== undefined) {
437
+ const values = [...new Set(rows.map((row) => row[single]))];
438
+ const field = lookupField(lookupResource(core, resource), single);
439
+ if (field?.kind === "scalar" && field.capabilities.filter?.includes("in")) {
440
+ return { [single]: { in: values } };
441
+ }
442
+ return someOf(values.map((value) => ({ [single]: value })));
443
+ }
444
+ return someOf(rows.map((row) => Object.fromEntries(identifier.map((member) => [member, row[member]]))));
445
+ }
446
+ async function rowsReadable(session, core, model, projection, checks, data) {
447
+ if (checks.size === 0 || projection === undefined) {
448
+ return true;
449
+ }
450
+ const levels = new Map();
451
+ rowsByLevel(projection, Array.isArray(data) ? data : [data], levels);
452
+ const questions = [...checks].map(async ([level, misses]) => {
453
+ const rows = levels.get(level) ?? [];
454
+ if (rows.length === 0) {
455
+ return false;
456
+ }
457
+ const miss = someOf(misses);
458
+ if (miss === false) {
459
+ return false;
460
+ }
461
+ if (miss === true) {
462
+ return true;
463
+ }
464
+ const identifier = identifierOf(model, level.resource);
465
+ const reach = allOf(identifiedRows(core, identifier, level.resource, rows), miss);
466
+ return (reach !== false &&
467
+ session.exists(level.resource, reach === true ? {} : reach));
468
+ });
469
+ const answers = await Promise.all(questions);
470
+ return !answers.some((answer) => answer === true);
471
+ }
472
+ function filledEmptied(scopedLevel, data) {
473
+ if (!hasEmptied(scopedLevel)) {
474
+ return data;
475
+ }
476
+ if (Array.isArray(data)) {
477
+ return data.map((row) => filledEmptied(scopedLevel, row));
478
+ }
479
+ if (!isRecord(data)) {
480
+ return data;
481
+ }
482
+ const row = { ...data };
483
+ for (const emptied of scopedLevel.emptied) {
484
+ row[emptied.field] = structuredClone(emptied.value);
485
+ }
486
+ for (const [field, nested] of scopedLevel.nested) {
487
+ const value = row[field];
488
+ row[field] =
489
+ isRecord(value) && Array.isArray(value.data)
490
+ ? { ...value, data: filledEmptied(nested, value.data) }
491
+ : filledEmptied(nested, value);
492
+ }
493
+ return row;
494
+ }
@@ -0,0 +1,37 @@
1
+ import type { AvAdapterOperationExecutor } from "../adapters/adapter-operation-executor.interface.js";
2
+ import type { ResolvedPolicyConfig } from "../config/config.resolver.js";
3
+ import type { AvRequestOrigin } from "../config/config-layer.js";
4
+ import type { AvContract, AvContractCore } from "../contracts/contract.js";
5
+ import type { AvOperationIdentity } from "../operations/operation-identity.js";
6
+ import type { DiagnosticsReporter } from "../runtime/diagnostics-reporter.js";
7
+ import { type CallerArgumentsView } from "../runtime/error-boundary.js";
8
+ import type { AvOperationFailureResponse } from "../runtime/operation-response.js";
9
+ import type { AvState } from "../runtime/request-state.js";
10
+ import { type PolicySession } from "./policy-session.js";
11
+ /** The principal an application call runs as (`{ as }`). */
12
+ export type PolicyCaller = {
13
+ readonly principal: unknown;
14
+ };
15
+ /**
16
+ * The principal a call's options name (`{ as }`), or `undefined` when they name
17
+ * none. `null` is a principal: the anonymous one.
18
+ */
19
+ export declare function callerOf(options: unknown): PolicyCaller | undefined;
20
+ /**
21
+ * The policy session of one request, or `undefined` when the policy does not
22
+ * check it. Deriving the principal is server code: a throw is the caller's
23
+ * to answer as the server's defect.
24
+ */
25
+ export declare function openRequestPolicy(policy: ResolvedPolicyConfig | undefined, core: AvContractCore, request: {
26
+ readonly origin: AvRequestOrigin;
27
+ readonly state: AvState;
28
+ }, caller: PolicyCaller | undefined, executor: AvAdapterOperationExecutor): PolicySession | undefined;
29
+ /**
30
+ * Policy admission inside its own boundary, as the Framework Validation Layer's
31
+ * is: a refusal, a defect of the engine or its answers (`A3000`), or an adapter
32
+ * failure of an `exists` question (`A3001`), answered without any developer
33
+ * filter; `undefined` when the request goes on to its guards.
34
+ */
35
+ export declare function admitRequestPolicy(session: PolicySession, core: AvContractCore, active: AvContract, request: AvOperationIdentity & {
36
+ readonly resource: string;
37
+ }, args: unknown, report: DiagnosticsReporter, view: CallerArgumentsView): Promise<AvOperationFailureResponse | undefined>;
@@ -0,0 +1,52 @@
1
+ import { AdapterBoundary, sanitizeOperationFailure, } from "../runtime/error-boundary.js";
2
+ import { admitByPolicy, admitWriteByPolicy } from "./policy-admission.js";
3
+ import { openPolicySession } from "./policy-session.js";
4
+ export function callerOf(options) {
5
+ if (typeof options !== "object" || options === null) {
6
+ return undefined;
7
+ }
8
+ let principal;
9
+ try {
10
+ principal = options.as;
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ return principal === undefined ? undefined : { principal };
16
+ }
17
+ export function openRequestPolicy(policy, core, request, caller, executor) {
18
+ if (policy === undefined) {
19
+ return undefined;
20
+ }
21
+ let principal;
22
+ if (caller !== undefined) {
23
+ principal = caller.principal;
24
+ }
25
+ else if (request.origin === "application") {
26
+ return undefined;
27
+ }
28
+ else {
29
+ principal = policy.principal({
30
+ $state: request.state,
31
+ $origin: request.origin,
32
+ });
33
+ }
34
+ return openPolicySession(policy, core, principal, request.origin, {
35
+ executor,
36
+ boundary: new AdapterBoundary(),
37
+ });
38
+ }
39
+ export async function admitRequestPolicy(session, core, active, request, args, report, view) {
40
+ try {
41
+ const refused = await admitByPolicy(session, core, active, request, args);
42
+ if (refused !== undefined) {
43
+ return refused;
44
+ }
45
+ return request.family === "find"
46
+ ? undefined
47
+ : await admitWriteByPolicy(session, core, request, args);
48
+ }
49
+ catch (error) {
50
+ return sanitizeOperationFailure(error, session.execution.boundary.originOf(error), report, view);
51
+ }
52
+ }
@@ -0,0 +1,83 @@
1
+ import type { AvAdapterOperationExecutor } from "../adapters/adapter-operation-executor.interface.js";
2
+ import type { ResolvedPolicyConfig } from "../config/config.resolver.js";
3
+ import type { AvRequestOrigin } from "../config/config-layer.js";
4
+ import type { AvContractCore } from "../contracts/contract.js";
5
+ import type { AdapterBoundary } from "../runtime/error-boundary.js";
6
+ import type { PolicyCondition } from "./policy-condition.js";
7
+ import type { AvPolicyWhere } from "./policy-engine.js";
8
+ /** An action a policy question names. */
9
+ export type PolicyAction = "read" | "create" | "update" | "delete";
10
+ /** `fieldsOf`'s answer, held: the names read into sets once. */
11
+ export type PolicyFields = {
12
+ /** `always` was `"all"`: every field that is not governed. */
13
+ readonly all: boolean;
14
+ /** The fields named in `always`, and those granted on every row. */
15
+ readonly listed: ReadonlySet<string>;
16
+ /** Each field readable on some rows only, and the condition those rows meet. */
17
+ readonly perRow: ReadonlyMap<string, AvPolicyWhere>;
18
+ };
19
+ /**
20
+ * A policy answer core cannot use: a fragment the Core Contract refuses, or an
21
+ * answer of the wrong shape. The engine is server code, so this is the server's
22
+ * defect (`A3000`), never the caller's.
23
+ */
24
+ export declare class PolicyDefectError extends Error {
25
+ readonly name = "PolicyDefectError";
26
+ }
27
+ /** Where `exists` runs: an executor, and the boundary its failures are classified by. */
28
+ export type PolicyExecution = {
29
+ readonly executor: AvAdapterOperationExecutor;
30
+ readonly boundary: AdapterBoundary;
31
+ };
32
+ /** One request's policy questions and answers. */
33
+ export type PolicySession = {
34
+ readonly principal: unknown;
35
+ readonly origin: AvRequestOrigin;
36
+ /** Where its `exists` runs now. */
37
+ readonly execution: PolicyExecution;
38
+ /** Whether the principal may run `action` on `resource` (`can`). */
39
+ can(resource: string, action: PolicyAction, row?: Readonly<Record<string, unknown>>): Promise<boolean>;
40
+ /** The rows the principal may `action` (`scopeOf`), checked. */
41
+ scope(resource: string, action: "read" | "update" | "delete"): Promise<PolicyCondition>;
42
+ /** The fields the principal may read or write (`fieldsOf`), checked. */
43
+ fields(resource: string, access: "read" | "write"): Promise<PolicyFields>;
44
+ /** Whether a row of `resource` meets `where`: core's own question, unchecked. */
45
+ exists(resource: string, where: AvPolicyWhere): Promise<boolean>;
46
+ /**
47
+ * The rows of `resource` meeting `where`, the scalar fields `select` names
48
+ * only: core's own read, unchecked, never cached (a write may change them).
49
+ */
50
+ rows(resource: string, where: AvPolicyWhere, select: readonly string[]): Promise<readonly Readonly<Record<string, unknown>>[]>;
51
+ /** Whether the engine lists `action` on `resource` in `afterChecks`. */
52
+ checksAfter(resource: string, action: "update"): boolean;
53
+ /**
54
+ * Notes a field a create sends that the principal may write on some rows
55
+ * only, with its grant: admission sees what the caller sent, and the row it
56
+ * creates is checked against the grant after the insert.
57
+ */
58
+ grantCreate(field: string, grant: AvPolicyWhere): void;
59
+ /** The grants admission noted for the fields a create sends, by field. */
60
+ createGrants(): ReadonlyMap<string, AvPolicyWhere>;
61
+ /** The same session, its `exists` running on `execution`. */
62
+ on(execution: PolicyExecution): PolicySession;
63
+ /**
64
+ * The same session with no `exists` answer kept: a write changed the rows,
65
+ * so a question asked before it may answer differently now.
66
+ */
67
+ renewed(): PolicySession;
68
+ };
69
+ /**
70
+ * Opens the policy session of one request: `principal` under `origin`, its
71
+ * `exists` on `execution` until {@link PolicySession.on} moves it.
72
+ */
73
+ export declare function openPolicySession(policy: ResolvedPolicyConfig, core: AvContractCore, principal: unknown, origin: AvRequestOrigin, execution: PolicyExecution): PolicySession;
74
+ /**
75
+ * Whether `fields` lets the principal use `field` of `resource` on every row it
76
+ * reaches: listed in `always`, or covered by `"all"` when it is not governed.
77
+ */
78
+ export declare function alwaysReadable(core: AvContractCore, resource: string, fields: PolicyFields, field: string): boolean;
79
+ /**
80
+ * How the principal may use `field`: on every row it reaches, on the rows a
81
+ * grant names, or not at all.
82
+ */
83
+ export declare function fieldAccess(core: AvContractCore, resource: string, fields: PolicyFields, field: string): "always" | AvPolicyWhere | "never";