@aventara/core 1.0.0-rc.3 → 1.0.0-rc.8
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/CHANGELOG.md +231 -1
- package/ERRORS.md +6 -6
- package/README.md +76 -12
- package/dist/adapters/adapter-field.d.ts +8 -0
- package/dist/adapters/adapter-identifier.d.ts +9 -0
- package/dist/adapters/adapter-identifier.js +6 -1
- package/dist/adapters/adapter-operation.d.ts +13 -0
- package/dist/compiler/contract.compiler.d.ts +6 -0
- package/dist/compiler/contract.compiler.js +99 -15
- package/dist/config/computed-fields.js +2 -9
- package/dist/config/config-diagnostic.guidance.js +3 -3
- package/dist/config/config.resolver.d.ts +14 -0
- package/dist/config/config.resolver.js +31 -0
- package/dist/config/config.validator.js +82 -0
- package/dist/config/exact-framework-config.d.ts +5 -0
- package/dist/config/framework-config.d.ts +30 -1
- package/dist/contracts/client-contract-structure.validator.d.ts +8 -0
- package/dist/contracts/client-contract-structure.validator.js +6 -1
- package/dist/contracts/contract.d.ts +15 -1
- package/dist/contracts/field.d.ts +28 -0
- package/dist/contracts/scalar-value-type.d.ts +3 -1
- package/dist/framework/contract-view.d.ts +41 -10
- package/dist/framework/framework-construction.error.d.ts +2 -1
- package/dist/framework/framework-construction.error.js +11 -1
- package/dist/framework/framework.d.ts +4 -4
- package/dist/framework/framework.factory.js +39 -11
- package/dist/framework/operation-facade.d.ts +29 -10
- package/dist/framework/operation-facade.js +2 -1
- package/dist/index.d.ts +10 -4
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +2 -0
- package/dist/operations/operation-arguments.d.ts +44 -28
- package/dist/operations/operation-call.d.ts +9 -5
- package/dist/operations/operation-result.d.ts +6 -4
- package/dist/operations/operation-validator-core.js +6 -0
- package/dist/policy/policy-admission.d.ts +22 -0
- package/dist/policy/policy-admission.js +409 -0
- package/dist/policy/policy-after-update.runtime.d.ts +29 -0
- package/dist/policy/policy-after-update.runtime.js +132 -0
- package/dist/policy/policy-condition.d.ts +15 -0
- package/dist/policy/policy-condition.js +36 -0
- package/dist/policy/policy-engine.d.ts +118 -0
- package/dist/policy/policy-engine.js +1 -0
- package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
- package/dist/policy/policy-nested-write.runtime.js +453 -0
- package/dist/policy/policy-read.runtime.d.ts +81 -0
- package/dist/policy/policy-read.runtime.js +503 -0
- package/dist/policy/policy-request.d.ts +41 -0
- package/dist/policy/policy-request.js +63 -0
- package/dist/policy/policy-session.d.ts +93 -0
- package/dist/policy/policy-session.js +235 -0
- package/dist/policy/policy-startup.validator.d.ts +8 -0
- package/dist/policy/policy-startup.validator.js +104 -0
- package/dist/policy/policy-write.runtime.d.ts +37 -0
- package/dist/policy/policy-write.runtime.js +409 -0
- package/dist/prebuilt/core-version.generated.d.ts +2 -0
- package/dist/prebuilt/core-version.generated.js +1 -0
- package/dist/prebuilt/prebuilt-changes.d.ts +28 -0
- package/dist/prebuilt/prebuilt-changes.js +65 -0
- package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
- package/dist/prebuilt/prebuilt-contracts.js +1 -0
- package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
- package/dist/prebuilt/prebuilt-contracts.loader.js +77 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
- package/dist/protocol/absent-route.responder.d.ts +4 -4
- package/dist/protocol/http-request.decoder.d.ts +2 -2
- package/dist/protocol/http-request.reader.d.ts +2 -2
- package/dist/protocol/protocol-headers.d.ts +3 -3
- package/dist/runtime/computed-read-runtime.d.ts +6 -1
- package/dist/runtime/computed-read-runtime.js +226 -52
- package/dist/runtime/context-executor.runtime.d.ts +2 -2
- package/dist/runtime/context-executor.runtime.js +4 -2
- package/dist/runtime/create-requirement.js +1 -1
- package/dist/runtime/diagnostics-reporter.d.ts +5 -5
- package/dist/runtime/execution-options.d.ts +8 -0
- package/dist/runtime/framework-warning.emitter.d.ts +6 -0
- package/dist/runtime/framework-warning.emitter.js +8 -0
- package/dist/runtime/level-lifecycle.runtime.js +3 -2
- package/dist/runtime/operation-code.guidance.js +3 -3
- package/dist/runtime/operation-request.d.ts +16 -9
- package/dist/runtime/operation-runtime.d.ts +15 -7
- package/dist/runtime/operation-runtime.js +139 -14
- package/dist/runtime/owned-execution.registry.d.ts +2 -2
- package/dist/runtime/readonly-value.d.ts +5 -1
- package/dist/runtime/readonly-value.js +67 -28
- package/dist/runtime/referential-action.d.ts +1 -1
- package/dist/transactions/sql-statement.d.ts +2 -2
- package/dist/transactions/transaction-plan-assembler.d.ts +2 -2
- package/dist/transactions/transaction-plan-validator.d.ts +5 -5
- package/dist/transactions/transaction-request.d.ts +8 -8
- package/dist/transactions/transaction-runtime.d.ts +5 -2
- package/dist/transactions/transaction-runtime.js +33 -6
- package/package.json +2 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
2
|
+
import type { AvAdapterOperationExecutor } from "../adapters/adapter-operation-executor.interface.js";
|
|
3
|
+
import type { ResolvedPolicyConfig } from "../config/config.resolver.js";
|
|
4
|
+
import type { AvRequestOrigin } from "../config/config-layer.js";
|
|
5
|
+
import type { AvContractCore } from "../contracts/contract.js";
|
|
6
|
+
import type { AdapterBoundary } from "../runtime/error-boundary.js";
|
|
7
|
+
import type { PolicyCondition } from "./policy-condition.js";
|
|
8
|
+
import type { AvPolicyEngine, AvPolicyWhere } from "./policy-engine.js";
|
|
9
|
+
/**
|
|
10
|
+
* What `fieldsOf` is asked about, beside the question every method states:
|
|
11
|
+
* reading, or writing by one action. Read from the engine's own signature,
|
|
12
|
+
* which writes the union inline (see `AvPolicyEngine.fieldsOf`).
|
|
13
|
+
*/
|
|
14
|
+
export type PolicyFieldsAccess = Parameters<AvPolicyEngine["fieldsOf"]>[0] extends infer Question ? Question extends unknown ? Pick<Question, Extract<keyof Question, "access" | "action">> : never : never;
|
|
15
|
+
/** An action a policy question names. */
|
|
16
|
+
export type PolicyAction = "read" | "create" | "update" | "delete";
|
|
17
|
+
/** `fieldsOf`'s answer, held: the names read into sets once. */
|
|
18
|
+
export type PolicyFields = {
|
|
19
|
+
/** `always` was `"all"`: every field that is not governed. */
|
|
20
|
+
readonly all: boolean;
|
|
21
|
+
/** The fields named in `always`, and those granted on every row. */
|
|
22
|
+
readonly listed: ReadonlySet<string>;
|
|
23
|
+
/** Each field readable on some rows only, and the condition those rows meet. */
|
|
24
|
+
readonly perRow: ReadonlyMap<string, AvPolicyWhere>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A policy answer core cannot use: a fragment the Core Contract refuses, or an
|
|
28
|
+
* answer of the wrong shape. The engine is server code, so this is the server's
|
|
29
|
+
* defect (`A3000`), never the caller's.
|
|
30
|
+
*/
|
|
31
|
+
export declare class PolicyDefectError extends Error {
|
|
32
|
+
readonly name = "PolicyDefectError";
|
|
33
|
+
}
|
|
34
|
+
/** Where `exists` runs: an executor, and the boundary its failures are classified by. */
|
|
35
|
+
export type PolicyExecution = {
|
|
36
|
+
readonly executor: AvAdapterOperationExecutor;
|
|
37
|
+
readonly boundary: AdapterBoundary;
|
|
38
|
+
};
|
|
39
|
+
/** One request's policy questions and answers. */
|
|
40
|
+
export type PolicySession = {
|
|
41
|
+
readonly principal: unknown;
|
|
42
|
+
readonly origin: AvRequestOrigin;
|
|
43
|
+
/** Where its `exists` runs now. */
|
|
44
|
+
readonly execution: PolicyExecution;
|
|
45
|
+
/** Whether the principal may run `action` on `resource` (`can`). */
|
|
46
|
+
can(resource: string, action: PolicyAction, row?: Readonly<Record<string, unknown>>): Promise<boolean>;
|
|
47
|
+
/** The rows the principal may `action` (`scopeOf`), checked. */
|
|
48
|
+
scope(resource: string, action: "read" | "update" | "delete"): Promise<PolicyCondition>;
|
|
49
|
+
/**
|
|
50
|
+
* The fields the principal may read, or write by one action (`fieldsOf`),
|
|
51
|
+
* checked.
|
|
52
|
+
*/
|
|
53
|
+
fields(resource: string, access: PolicyFieldsAccess): Promise<PolicyFields>;
|
|
54
|
+
/** Whether a row of `resource` meets `where`: core's own question, unchecked. */
|
|
55
|
+
exists(resource: string, where: AvPolicyWhere): Promise<boolean>;
|
|
56
|
+
/**
|
|
57
|
+
* The rows of `resource` meeting `where`, the scalar fields `select` names
|
|
58
|
+
* only: core's own read, unchecked, never cached (a write may change them).
|
|
59
|
+
*/
|
|
60
|
+
rows(resource: string, where: AvPolicyWhere, select: readonly string[]): Promise<readonly Readonly<Record<string, unknown>>[]>;
|
|
61
|
+
/** Whether the engine lists `action` on `resource` in `afterChecks`. */
|
|
62
|
+
checksAfter(resource: string, action: "update"): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Notes a field a create sends that the principal may write on some rows
|
|
65
|
+
* only, with its grant: admission sees what the caller sent, and the row it
|
|
66
|
+
* creates is checked against the grant after the insert.
|
|
67
|
+
*/
|
|
68
|
+
grantCreate(field: string, grant: AvPolicyWhere): void;
|
|
69
|
+
/** The grants admission noted for the fields a create sends, by field. */
|
|
70
|
+
createGrants(): ReadonlyMap<string, AvPolicyWhere>;
|
|
71
|
+
/** The same session, its `exists` running on `execution`. */
|
|
72
|
+
on(execution: PolicyExecution): PolicySession;
|
|
73
|
+
/**
|
|
74
|
+
* The same session with no `exists` answer kept: a write changed the rows,
|
|
75
|
+
* so a question asked before it may answer differently now.
|
|
76
|
+
*/
|
|
77
|
+
renewed(): PolicySession;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Opens the policy session of one request: `principal` under `origin`, its
|
|
81
|
+
* `exists` on `execution` until {@link PolicySession.on} moves it.
|
|
82
|
+
*/
|
|
83
|
+
export declare function openPolicySession(policy: ResolvedPolicyConfig, core: AvContractCore, model: AvAdapterModel, principal: unknown, origin: AvRequestOrigin, execution: PolicyExecution): PolicySession;
|
|
84
|
+
/**
|
|
85
|
+
* Whether `fields` lets the principal use `field` of `resource` on every row it
|
|
86
|
+
* reaches: listed in `always`, or covered by `"all"` when it is not governed.
|
|
87
|
+
*/
|
|
88
|
+
export declare function alwaysReadable(core: AvContractCore, resource: string, fields: PolicyFields, field: string): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* How the principal may use `field`: on every row it reaches, on the rows a
|
|
91
|
+
* grant names, or not at all.
|
|
92
|
+
*/
|
|
93
|
+
export declare function fieldAccess(core: AvContractCore, resource: string, fields: PolicyFields, field: string): "always" | AvPolicyWhere | "never";
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { adapterPrimaryIdentifier } from "../adapters/adapter-identifier.js";
|
|
2
|
+
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
3
|
+
import { validateOperationArguments } from "../operations/operation-validator-core.js";
|
|
4
|
+
import { normalizeOperationScalarValues } from "../runtime/scalar-runtime.js";
|
|
5
|
+
export class PolicyDefectError extends Error {
|
|
6
|
+
name = "PolicyDefectError";
|
|
7
|
+
}
|
|
8
|
+
function isEmptyRecord(value) {
|
|
9
|
+
return (value !== null &&
|
|
10
|
+
typeof value === "object" &&
|
|
11
|
+
!Array.isArray(value) &&
|
|
12
|
+
Object.keys(value).length === 0);
|
|
13
|
+
}
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function checkedFragment(core, resource, where, what) {
|
|
18
|
+
if (!isRecord(where)) {
|
|
19
|
+
throw new PolicyDefectError(`${what} must be a where object.`);
|
|
20
|
+
}
|
|
21
|
+
const request = {
|
|
22
|
+
resource,
|
|
23
|
+
family: "find",
|
|
24
|
+
variant: "many",
|
|
25
|
+
arguments: { where },
|
|
26
|
+
};
|
|
27
|
+
const validation = validateOperationArguments(core, request);
|
|
28
|
+
const normalized = validation.ok
|
|
29
|
+
? normalizeOperationScalarValues(core, {
|
|
30
|
+
...request,
|
|
31
|
+
arguments: validation.arguments,
|
|
32
|
+
}, "application")
|
|
33
|
+
: validation;
|
|
34
|
+
if (!normalized.ok) {
|
|
35
|
+
const issues = normalized.issues
|
|
36
|
+
.map((issue) => `${issue.code} at ${(issue.path ?? []).slice(2).join(".") || "where"}: ${issue.message}`)
|
|
37
|
+
.join("; ");
|
|
38
|
+
throw new PolicyDefectError(`${what} is no where of Resource "${resource}" in the Core Contract (${issues}).`);
|
|
39
|
+
}
|
|
40
|
+
return normalized.arguments.where;
|
|
41
|
+
}
|
|
42
|
+
function conditionOf(core, resource, answer, what) {
|
|
43
|
+
if (typeof answer === "boolean") {
|
|
44
|
+
return answer;
|
|
45
|
+
}
|
|
46
|
+
if (isEmptyRecord(answer)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return checkedFragment(core, resource, answer, what);
|
|
50
|
+
}
|
|
51
|
+
function fieldsAnswer(core, resource, answer) {
|
|
52
|
+
const what = `fieldsOf(${resource})`;
|
|
53
|
+
if (!isRecord(answer)) {
|
|
54
|
+
throw new PolicyDefectError(`${what} must answer { always, perRow }.`);
|
|
55
|
+
}
|
|
56
|
+
const { always, perRow } = answer;
|
|
57
|
+
if (always !== "all" &&
|
|
58
|
+
!(Array.isArray(always) && always.every((name) => typeof name === "string"))) {
|
|
59
|
+
throw new PolicyDefectError(`${what}.always must be "all" or a list of field names.`);
|
|
60
|
+
}
|
|
61
|
+
if (!isRecord(perRow)) {
|
|
62
|
+
throw new PolicyDefectError(`${what}.perRow must be an object.`);
|
|
63
|
+
}
|
|
64
|
+
const listed = new Set(always === "all" ? [] : always);
|
|
65
|
+
const grants = new Map();
|
|
66
|
+
for (const [field, grant] of Object.entries(perRow)) {
|
|
67
|
+
if (listed.has(field) || grant === undefined) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const condition = conditionOf(core, resource, grant, `${what}.perRow.${field}`);
|
|
71
|
+
if (condition === true) {
|
|
72
|
+
listed.add(field);
|
|
73
|
+
}
|
|
74
|
+
else if (condition !== false) {
|
|
75
|
+
grants.set(field, condition);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { all: always === "all", listed, perRow: grants };
|
|
79
|
+
}
|
|
80
|
+
function questionKey(resource, where) {
|
|
81
|
+
try {
|
|
82
|
+
return `${resource}\u0000${JSON.stringify(where, (_key, value) => typeof value === "bigint" ? `#bigint(${value})` : value)}`;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function existsOperation(core, model, resource, where) {
|
|
89
|
+
const contract = lookupResource(core, resource);
|
|
90
|
+
const identifier = adapterPrimaryIdentifier(model, resource);
|
|
91
|
+
const first = contract?.operations.find?.first !== undefined;
|
|
92
|
+
return first && identifier.length > 0
|
|
93
|
+
? {
|
|
94
|
+
resource,
|
|
95
|
+
family: "find",
|
|
96
|
+
variant: "first",
|
|
97
|
+
arguments: { where, select: [...identifier] },
|
|
98
|
+
}
|
|
99
|
+
: {
|
|
100
|
+
resource,
|
|
101
|
+
family: "find",
|
|
102
|
+
variant: "count",
|
|
103
|
+
arguments: { where },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function openPolicySession(policy, core, model, principal, origin, execution) {
|
|
107
|
+
const caches = {
|
|
108
|
+
scopes: new Map(),
|
|
109
|
+
createGrants: new Map(),
|
|
110
|
+
fields: new Map(),
|
|
111
|
+
exists: new WeakMap(),
|
|
112
|
+
};
|
|
113
|
+
return sessionOn(policy, core, model, principal, origin, execution, caches);
|
|
114
|
+
}
|
|
115
|
+
function sessionOn(policy, core, model, principal, origin, execution, caches) {
|
|
116
|
+
const { engine } = policy;
|
|
117
|
+
const exists = (resource, where) => {
|
|
118
|
+
const key = questionKey(resource, where);
|
|
119
|
+
let answers = caches.exists.get(execution.executor);
|
|
120
|
+
if (answers === undefined) {
|
|
121
|
+
answers = new Map();
|
|
122
|
+
caches.exists.set(execution.executor, answers);
|
|
123
|
+
}
|
|
124
|
+
const known = key === undefined ? undefined : answers.get(key);
|
|
125
|
+
if (known !== undefined) {
|
|
126
|
+
return known;
|
|
127
|
+
}
|
|
128
|
+
const operation = existsOperation(core, model, resource, where);
|
|
129
|
+
const answer = execution.boundary
|
|
130
|
+
.execute(() => execution.executor.execute(operation))
|
|
131
|
+
.then((result) => typeof result === "number" ? result > 0 : result != null);
|
|
132
|
+
if (key !== undefined) {
|
|
133
|
+
answers.set(key, answer);
|
|
134
|
+
}
|
|
135
|
+
return answer;
|
|
136
|
+
};
|
|
137
|
+
const engineExists = async (resource, where) => {
|
|
138
|
+
if (typeof resource !== "string" ||
|
|
139
|
+
lookupResource(core, resource) === undefined) {
|
|
140
|
+
throw new PolicyDefectError(`exists was asked about "${String(resource)}", which is no Resource of the Core Contract.`);
|
|
141
|
+
}
|
|
142
|
+
const condition = conditionOf(core, resource, where, "exists's where");
|
|
143
|
+
if (typeof condition === "boolean") {
|
|
144
|
+
return exists(resource, {});
|
|
145
|
+
}
|
|
146
|
+
return exists(resource, condition);
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
principal,
|
|
150
|
+
origin,
|
|
151
|
+
execution,
|
|
152
|
+
async can(resource, action, row) {
|
|
153
|
+
const answer = await engine.can({
|
|
154
|
+
principal,
|
|
155
|
+
origin,
|
|
156
|
+
resource,
|
|
157
|
+
action,
|
|
158
|
+
...(row === undefined ? {} : { row }),
|
|
159
|
+
exists: engineExists,
|
|
160
|
+
});
|
|
161
|
+
if (typeof answer !== "boolean") {
|
|
162
|
+
throw new PolicyDefectError(`can(${resource}, ${action}) must answer true or false.`);
|
|
163
|
+
}
|
|
164
|
+
return answer;
|
|
165
|
+
},
|
|
166
|
+
scope(resource, action) {
|
|
167
|
+
const key = `${resource}\u0000${action}`;
|
|
168
|
+
let scope = caches.scopes.get(key);
|
|
169
|
+
if (scope === undefined) {
|
|
170
|
+
scope = Promise.resolve()
|
|
171
|
+
.then(() => engine.scopeOf({ principal, origin, resource, action }))
|
|
172
|
+
.then((answer) => conditionOf(core, resource, answer, `scopeOf(${resource}, ${action})`));
|
|
173
|
+
caches.scopes.set(key, scope);
|
|
174
|
+
}
|
|
175
|
+
return scope;
|
|
176
|
+
},
|
|
177
|
+
fields(resource, access) {
|
|
178
|
+
const key = `${resource}\u0000${access.access}\u0000${access.access === "write" ? access.action : ""}`;
|
|
179
|
+
let fields = caches.fields.get(key);
|
|
180
|
+
if (fields === undefined) {
|
|
181
|
+
fields = Promise.resolve()
|
|
182
|
+
.then(() => engine.fieldsOf({ principal, origin, resource, ...access }))
|
|
183
|
+
.then((answer) => fieldsAnswer(core, resource, answer));
|
|
184
|
+
caches.fields.set(key, fields);
|
|
185
|
+
}
|
|
186
|
+
return fields;
|
|
187
|
+
},
|
|
188
|
+
exists,
|
|
189
|
+
async rows(resource, where, select) {
|
|
190
|
+
if (lookupResource(core, resource)?.operations.find?.many === undefined) {
|
|
191
|
+
throw new PolicyDefectError(`The policy needs the rows of Resource "${resource}", and its Core Contract offers no find.many.`);
|
|
192
|
+
}
|
|
193
|
+
const operation = {
|
|
194
|
+
resource,
|
|
195
|
+
family: "find",
|
|
196
|
+
variant: "many",
|
|
197
|
+
arguments: { where, select: [...select] },
|
|
198
|
+
};
|
|
199
|
+
const answer = await execution.boundary.execute(() => execution.executor.execute(operation));
|
|
200
|
+
return Array.isArray(answer)
|
|
201
|
+
? answer.filter((row) => isRecord(row))
|
|
202
|
+
: [];
|
|
203
|
+
},
|
|
204
|
+
grantCreate(field, grant) {
|
|
205
|
+
caches.createGrants.set(field, grant);
|
|
206
|
+
},
|
|
207
|
+
createGrants() {
|
|
208
|
+
return caches.createGrants;
|
|
209
|
+
},
|
|
210
|
+
checksAfter(resource, action) {
|
|
211
|
+
return (Object.hasOwn(policy.afterChecks, resource) &&
|
|
212
|
+
policy.afterChecks[resource]?.includes(action) === true);
|
|
213
|
+
},
|
|
214
|
+
on(next) {
|
|
215
|
+
return sessionOn(policy, core, model, principal, origin, next, caches);
|
|
216
|
+
},
|
|
217
|
+
renewed() {
|
|
218
|
+
return sessionOn(policy, core, model, principal, origin, execution, {
|
|
219
|
+
...caches,
|
|
220
|
+
exists: new WeakMap(),
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export function alwaysReadable(core, resource, fields, field) {
|
|
226
|
+
return (fields.listed.has(field) ||
|
|
227
|
+
(fields.all &&
|
|
228
|
+
lookupField(lookupResource(core, resource), field)?.governed !== true));
|
|
229
|
+
}
|
|
230
|
+
export function fieldAccess(core, resource, fields, field) {
|
|
231
|
+
if (alwaysReadable(core, resource, fields, field)) {
|
|
232
|
+
return "always";
|
|
233
|
+
}
|
|
234
|
+
return fields.perRow.get(field) ?? "never";
|
|
235
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResolvedPolicyConfig } from "../config/config.resolver.js";
|
|
2
|
+
import type { AvConfigDiagnostic } from "../config/config-diagnostic.js";
|
|
3
|
+
import type { AvContractCore } from "../contracts/contract.js";
|
|
4
|
+
/**
|
|
5
|
+
* The policy's governed set, rule paths and after checks checked against the
|
|
6
|
+
* Core Contract: every problem, naming the Resource or the rule, or none.
|
|
7
|
+
*/
|
|
8
|
+
export declare function policyStartupDiagnostics(core: AvContractCore, policy: ResolvedPolicyConfig): readonly AvConfigDiagnostic[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
2
|
+
const TO_MANY_OPERATORS = new Set(["some", "every", "none"]);
|
|
3
|
+
function invalid(path, message) {
|
|
4
|
+
return { code: "CONFIG_INVALID_VALUE", path, message };
|
|
5
|
+
}
|
|
6
|
+
function governedDiagnostics(core, governed) {
|
|
7
|
+
const diagnostics = [];
|
|
8
|
+
for (const [resourceName, names] of Object.entries(governed)) {
|
|
9
|
+
const path = `policy.engine.governed.${resourceName}`;
|
|
10
|
+
const resource = lookupResource(core, resourceName);
|
|
11
|
+
if (resource === undefined) {
|
|
12
|
+
diagnostics.push(invalid(path, `"${resourceName}" is no Resource of the Core Contract`));
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
for (const name of names) {
|
|
16
|
+
if (lookupField(resource, name) === undefined) {
|
|
17
|
+
diagnostics.push(invalid(path, `"${name}" is no field or relation of Resource "${resourceName}" in the Core Contract`));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return diagnostics;
|
|
22
|
+
}
|
|
23
|
+
function afterCheckDiagnostics(core, afterChecks) {
|
|
24
|
+
return Object.keys(afterChecks).flatMap((resourceName) => {
|
|
25
|
+
const path = `policy.engine.afterChecks.${resourceName}`;
|
|
26
|
+
return lookupResource(core, resourceName) === undefined
|
|
27
|
+
? [invalid(path, `"${resourceName}" is no Resource of the Core Contract`)]
|
|
28
|
+
: [];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function pathFault(core, path) {
|
|
32
|
+
const [resourceName, ...segments] = path;
|
|
33
|
+
let resource = resourceName === undefined ? undefined : lookupResource(core, resourceName);
|
|
34
|
+
if (resource === undefined) {
|
|
35
|
+
return `"${resourceName ?? ""}" is no Resource of the Core Contract`;
|
|
36
|
+
}
|
|
37
|
+
let current = resourceName;
|
|
38
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
39
|
+
const segment = segments[index];
|
|
40
|
+
const field = lookupField(resource, segment);
|
|
41
|
+
if (field === undefined) {
|
|
42
|
+
return `"${segment}" is no field or relation of Resource "${current}"`;
|
|
43
|
+
}
|
|
44
|
+
const filters = field.capabilities.filter ?? [];
|
|
45
|
+
if (field.kind === "scalar") {
|
|
46
|
+
if (filters.length === 0) {
|
|
47
|
+
return `field "${current}.${segment}" cannot be filtered`;
|
|
48
|
+
}
|
|
49
|
+
const operator = segments[index + 1];
|
|
50
|
+
if (operator !== undefined && !filters.includes(operator)) {
|
|
51
|
+
return `field "${current}.${segment}" has no filter operator "${operator}"`;
|
|
52
|
+
}
|
|
53
|
+
if (index + 2 < segments.length) {
|
|
54
|
+
return `the path goes on past scalar field "${current}.${segment}"`;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const target = lookupResource(core, field.target);
|
|
59
|
+
if (target === undefined) {
|
|
60
|
+
return `relation "${current}.${segment}" leads to no Resource of the Core Contract`;
|
|
61
|
+
}
|
|
62
|
+
if (field.cardinality === "many") {
|
|
63
|
+
const operator = segments[index + 1];
|
|
64
|
+
if (operator === undefined || !TO_MANY_OPERATORS.has(operator)) {
|
|
65
|
+
return `to-many relation "${current}.${segment}" must be followed by some, every or none`;
|
|
66
|
+
}
|
|
67
|
+
if (!filters.includes(operator)) {
|
|
68
|
+
return `relation "${current}.${segment}" has no "${operator}" filter`;
|
|
69
|
+
}
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
else if (index + 1 === segments.length) {
|
|
73
|
+
return filters.includes("null")
|
|
74
|
+
? undefined
|
|
75
|
+
: `relation "${current}.${segment}" has no null filter`;
|
|
76
|
+
}
|
|
77
|
+
else if (!filters.includes("match")) {
|
|
78
|
+
return `relation "${current}.${segment}" cannot filter its target directly`;
|
|
79
|
+
}
|
|
80
|
+
resource = target;
|
|
81
|
+
current = field.target;
|
|
82
|
+
}
|
|
83
|
+
return `the path ends at Resource "${current}", not at a field`;
|
|
84
|
+
}
|
|
85
|
+
function pathDiagnostics(core, paths) {
|
|
86
|
+
const diagnostics = [];
|
|
87
|
+
for (const [rule, list] of Object.entries(paths)) {
|
|
88
|
+
const at = `policy.engine.paths.${rule}`;
|
|
89
|
+
for (const path of list) {
|
|
90
|
+
const fault = pathFault(core, path);
|
|
91
|
+
if (fault !== undefined) {
|
|
92
|
+
diagnostics.push(invalid(at, `rule "${rule}" filters on "${path.join(".")}": ${fault}`));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return diagnostics;
|
|
97
|
+
}
|
|
98
|
+
export function policyStartupDiagnostics(core, policy) {
|
|
99
|
+
return [
|
|
100
|
+
...governedDiagnostics(core, policy.governed),
|
|
101
|
+
...pathDiagnostics(core, policy.paths),
|
|
102
|
+
...afterCheckDiagnostics(core, policy.afterChecks),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 } from "../runtime/computed-read-runtime.js";
|
|
5
|
+
import type { AvOperationFailureResponse } from "../runtime/operation-response.js";
|
|
6
|
+
import { type PolicyRead } from "./policy-read.runtime.js";
|
|
7
|
+
import type { PolicySession } from "./policy-session.js";
|
|
8
|
+
/** A write's policy: what the adapter runs, and how a unique write's miss answers. */
|
|
9
|
+
export type PolicyWrite = PolicyRead & {
|
|
10
|
+
/** A unique write that matched nothing: `A4002` for a row the caller can read, `A2003` otherwise. */
|
|
11
|
+
readonly miss?: () => Promise<AvOperationFailureResponse>;
|
|
12
|
+
/**
|
|
13
|
+
* A unique update holding nested writes whose adapter call failed: the miss's
|
|
14
|
+
* answer when its row is outside the scopes (an adapter may fail the nested
|
|
15
|
+
* write there instead of answering the miss), `undefined` when the failure
|
|
16
|
+
* stands.
|
|
17
|
+
*/
|
|
18
|
+
readonly failedMiss?: () => Promise<AvOperationFailureResponse | undefined>;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Whether a write the policy checks needs an interactive transaction to roll
|
|
22
|
+
* it back in. Where it has none, {@link enforceWritePolicy} refuses exactly
|
|
23
|
+
* these writes before they run. `planned` plans the request's read, asked only
|
|
24
|
+
* when the answer depends on the rows it returns.
|
|
25
|
+
*/
|
|
26
|
+
export declare function writeNeedsTransaction(session: PolicySession, core: AvContractCore, request: AvOperationIdentity & {
|
|
27
|
+
readonly resource: string;
|
|
28
|
+
}, planned: () => PlannedReadOperation, adapterArguments: unknown): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* One write's policy (see the module comment): the refusal, the answer when it
|
|
31
|
+
* reaches no row, or what the adapter runs and how its answer is checked.
|
|
32
|
+
* `inTransaction` says whether the write runs in an interactive transaction a
|
|
33
|
+
* failed check rolls back.
|
|
34
|
+
*/
|
|
35
|
+
export declare function enforceWritePolicy(session: PolicySession, core: AvContractCore, model: AvAdapterModel, request: AvOperationIdentity & {
|
|
36
|
+
readonly resource: string;
|
|
37
|
+
}, plan: PlannedReadOperation, adapterArguments: unknown, inTransaction: boolean): Promise<PolicyWrite>;
|