@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
|
@@ -81,15 +81,28 @@ function cloneAdapterField(field) {
|
|
|
81
81
|
...(Array.isArray(field.referenceFields) && field.referenceFields.length > 0
|
|
82
82
|
? { referenceFields: [...field.referenceFields] }
|
|
83
83
|
: {}),
|
|
84
|
+
...(Array.isArray(field.referenceFields) &&
|
|
85
|
+
Array.isArray(field.references) &&
|
|
86
|
+
field.references.length > 0 &&
|
|
87
|
+
field.references.length === field.referenceFields.length
|
|
88
|
+
? { references: [...field.references] }
|
|
89
|
+
: {}),
|
|
84
90
|
};
|
|
85
91
|
}
|
|
86
|
-
function pruneHiddenPairings(projection) {
|
|
92
|
+
function pruneHiddenPairings(projection, references) {
|
|
87
93
|
const resources = projection.resources;
|
|
88
94
|
for (const [name, resource] of Object.entries(resources)) {
|
|
89
95
|
for (const field of Object.values(resource.fields)) {
|
|
90
96
|
if (field.kind !== "relation") {
|
|
91
97
|
continue;
|
|
92
98
|
}
|
|
99
|
+
if (Array.isArray(field.references) &&
|
|
100
|
+
(references === "none" ||
|
|
101
|
+
!wholePairing(resource.fields, Object.hasOwn(resources, String(field.target))
|
|
102
|
+
? resources[String(field.target)]?.fields
|
|
103
|
+
: undefined, field.referenceFields, field.references))) {
|
|
104
|
+
delete field.references;
|
|
105
|
+
}
|
|
93
106
|
if (Array.isArray(field.referenceFields)) {
|
|
94
107
|
const shown = field.referenceFields.filter((reference) => typeof reference === "string" &&
|
|
95
108
|
Object.hasOwn(resource.fields, reference) &&
|
|
@@ -113,6 +126,16 @@ function pruneHiddenPairings(projection) {
|
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
128
|
}
|
|
129
|
+
function wholePairing(fields, targetFields, referenceFields, references) {
|
|
130
|
+
const shownIn = (within, name) => typeof name === "string" &&
|
|
131
|
+
within !== undefined &&
|
|
132
|
+
Object.hasOwn(within, name) &&
|
|
133
|
+
within[name]?.kind === "scalar";
|
|
134
|
+
return (Array.isArray(referenceFields) &&
|
|
135
|
+
referenceFields.length === references.length &&
|
|
136
|
+
referenceFields.every((name) => shownIn(fields, name)) &&
|
|
137
|
+
references.every((name) => shownIn(targetFields, name)));
|
|
138
|
+
}
|
|
116
139
|
function cloneOperations(operations) {
|
|
117
140
|
return cloneValue(operations);
|
|
118
141
|
}
|
|
@@ -1002,18 +1025,40 @@ function projectCore(model, config, diagnostics) {
|
|
|
1002
1025
|
baseline.transactions = resolveTransactions(model, "root", scope, diagnostics);
|
|
1003
1026
|
checkResourceTree(model, baseline, "root", config.trees.root, config.definitions, diagnostics);
|
|
1004
1027
|
applyFieldDefinitions(model, baseline, config.definitions, diagnostics);
|
|
1028
|
+
markGovernedFields(baseline, config.policy?.governed);
|
|
1005
1029
|
applyCompute(baseline, scope.compute);
|
|
1006
1030
|
checkLayerValidators(baseline.resources, "root", config.trees.root, diagnostics);
|
|
1007
1031
|
const validators = compileValidatorTable(baseline.resources, scope.pipelines);
|
|
1008
1032
|
const projection = projectExposure({ layer: "root", style: exposureStyle(scope), diagnostics }, baseline, baseline, scope.exemptions);
|
|
1009
1033
|
qualifyFilterModes(projection);
|
|
1010
|
-
pruneHiddenPairings(projection);
|
|
1034
|
+
pruneHiddenPairings(projection, "shown");
|
|
1011
1035
|
projectCreateRequirements(model, projection.resources);
|
|
1012
1036
|
checkHiddenRequirements(model, "root", baseline.resources, baseline.resources, projection.resources, diagnostics);
|
|
1013
1037
|
applyValidators(projection.resources, validators);
|
|
1014
1038
|
validateProjection(projection, "root", diagnostics);
|
|
1015
1039
|
return { baseline, projection, validators };
|
|
1016
1040
|
}
|
|
1041
|
+
function markGovernedFields(baseline, governed) {
|
|
1042
|
+
if (governed === undefined) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
for (const [resourceName, names] of Object.entries(governed)) {
|
|
1046
|
+
const resource = Object.hasOwn(baseline.resources, resourceName)
|
|
1047
|
+
? baseline.resources[resourceName]
|
|
1048
|
+
: undefined;
|
|
1049
|
+
if (resource === undefined) {
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
for (const name of names) {
|
|
1053
|
+
const field = Object.hasOwn(resource.fields, name)
|
|
1054
|
+
? resource.fields[name]
|
|
1055
|
+
: undefined;
|
|
1056
|
+
if (field !== undefined) {
|
|
1057
|
+
field.governed = true;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1017
1062
|
function projectDerivedScope(model, scopeName, core, parentView, config, diagnostics) {
|
|
1018
1063
|
const scope = config[scopeName];
|
|
1019
1064
|
const baseline = cloneValue(core.baseline);
|
|
@@ -1037,7 +1082,7 @@ function projectDerivedScope(model, scopeName, core, parentView, config, diagnos
|
|
|
1037
1082
|
...(scopeName === "agent" ? { parent: config.agent.parent } : {}),
|
|
1038
1083
|
}, baseline, parent, scope.exemptions);
|
|
1039
1084
|
qualifyFilterModes(projection);
|
|
1040
|
-
pruneHiddenPairings(projection);
|
|
1085
|
+
pruneHiddenPairings(projection, "none");
|
|
1041
1086
|
projectCreateRequirements(model, projection.resources);
|
|
1042
1087
|
checkHiddenRequirements(model, scopeName, baseline.resources, parent.resources, projection.resources, diagnostics);
|
|
1043
1088
|
applyValidators(projection.resources, validators);
|
|
@@ -1095,18 +1140,57 @@ export async function compileContracts(model, config) {
|
|
|
1095
1140
|
};
|
|
1096
1141
|
}
|
|
1097
1142
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1143
|
+
const contracts = { core, application, client, agent };
|
|
1144
|
+
bindRuntimePieces(contracts, compilerConfig.definitions, {
|
|
1145
|
+
core: coreCompilation.validators,
|
|
1146
|
+
application: applicationProjection.validators,
|
|
1147
|
+
client: clientProjection.validators,
|
|
1148
|
+
agent: agentProjection.validators,
|
|
1149
|
+
});
|
|
1150
|
+
return { ok: true, contracts };
|
|
1151
|
+
}
|
|
1152
|
+
function bindRuntimePieces(contracts, definitions, validators) {
|
|
1153
|
+
for (const origin of ["core", "application", "client", "agent"]) {
|
|
1154
|
+
bindShapedFields(contracts[origin], definitions);
|
|
1155
|
+
bindFieldValidators(contracts[origin], validators[origin]);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
export function bindPrebuiltContracts(model, config, contracts) {
|
|
1159
|
+
const compilerConfig = config;
|
|
1160
|
+
const fields = validatedFieldsView(model, compilerConfig.definitions);
|
|
1161
|
+
bindRuntimePieces(contracts, compilerConfig.definitions, {
|
|
1162
|
+
core: compileValidatorTable(fields, compilerConfig.root.pipelines),
|
|
1163
|
+
application: compileValidatorTable(fields, compilerConfig.application.pipelines),
|
|
1164
|
+
client: compileValidatorTable(fields, compilerConfig.client.pipelines),
|
|
1165
|
+
agent: compileValidatorTable(fields, compilerConfig.agent.pipelines),
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
function validatedFieldsView(model, definitions) {
|
|
1169
|
+
const resources = { ...model.resources };
|
|
1170
|
+
for (const [resourceName, resourceDefinitions] of Object.entries(definitions)) {
|
|
1171
|
+
const resource = Object.hasOwn(model.resources, resourceName)
|
|
1172
|
+
? model.resources[resourceName]
|
|
1173
|
+
: undefined;
|
|
1174
|
+
if (resource === undefined) {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
let added;
|
|
1178
|
+
for (const [fieldName, definition] of Object.entries(resourceDefinitions)) {
|
|
1179
|
+
if (Object.hasOwn(resource.fields, fieldName)) {
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
added ??= {};
|
|
1183
|
+
added[fieldName] = {
|
|
1184
|
+
kind: "scalar",
|
|
1185
|
+
type: definition.type,
|
|
1186
|
+
list: definition.list,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
if (added !== undefined) {
|
|
1190
|
+
resources[resourceName] = { fields: { ...resource.fields, ...added } };
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return resources;
|
|
1110
1194
|
}
|
|
1111
1195
|
async function identifiedContract(projection) {
|
|
1112
1196
|
const members = canonicalizeContractMembers({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { emitFrameworkWarning } from "../runtime/framework-warning.emitter.js";
|
|
1
2
|
import { computeOfLayer } from "./field-compute.resolver.js";
|
|
2
3
|
function layerComputedFields(resources) {
|
|
3
4
|
const result = {};
|
|
@@ -68,13 +69,5 @@ export function warnOnStaleComputedFields(generated, configured) {
|
|
|
68
69
|
const message = `generated types are stale: run aventara:prepare. The computed fields the pre-compute stages are typed with differ from the configuration's:\n${drift
|
|
69
70
|
.map((line) => ` - ${line}`)
|
|
70
71
|
.join("\n")}`;
|
|
71
|
-
|
|
72
|
-
if (typeof emitWarning === "function") {
|
|
73
|
-
emitWarning(message, {
|
|
74
|
-
type: "AventaraWarning",
|
|
75
|
-
code: STALE_GENERATED_TYPES_WARNING,
|
|
76
|
-
});
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
globalThis.console?.warn(`AventaraWarning: ${message}`);
|
|
72
|
+
emitFrameworkWarning(message, STALE_GENERATED_TYPES_WARNING);
|
|
80
73
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export const CONFIG_DIAGNOSTIC_GUIDANCE = {
|
|
2
2
|
CONFIG_REQUIRED: {
|
|
3
3
|
message: "A required configuration key is missing.",
|
|
4
|
-
cause: "A key that must be present is absent: api, adapter, or an overlay's exposed when the overlay sets exemptions.",
|
|
4
|
+
cause: "A key that must be present is absent: api, adapter, or an overlay's exposed when the overlay sets exemptions, or policy.engine or policy.principal when policy is set.",
|
|
5
5
|
fix: "Add the key at the diagnostic's path.",
|
|
6
6
|
},
|
|
7
7
|
CONFIG_UNKNOWN_PROPERTY: {
|
|
8
8
|
message: "Unknown configuration key.",
|
|
9
|
-
cause: "A key the configuration does not define at that place: a typo, or a key written at the wrong level of the tree.",
|
|
9
|
+
cause: "A key the configuration does not define at that place: a typo, or a key written at the wrong level of the tree. `policy` and `prebuilt` are root-only keys; in an overlay they read as unknown.",
|
|
10
10
|
fix: 'Remove it, or move it where it belongs (the configuration reference\'s "Where a key can go").',
|
|
11
11
|
},
|
|
12
12
|
CONFIG_INVALID_TYPE: {
|
|
@@ -16,7 +16,7 @@ export const CONFIG_DIAGNOSTIC_GUIDANCE = {
|
|
|
16
16
|
},
|
|
17
17
|
CONFIG_INVALID_VALUE: {
|
|
18
18
|
message: "A configuration value is not allowed.",
|
|
19
|
-
cause: 'A malformed api or mcp path, a negative limit, an unknown transactionsStyle, a format that is not a named zod format,
|
|
19
|
+
cause: 'A malformed api or mcp path, a negative limit, an unknown transactionsStyle, a format that is not a named zod format, forceTransaction under an effective transactionsStyle "none", an after check on any action but update, or a policy engine\'s governed, paths or afterChecks naming a Resource, field or path the Core Contract does not have (the message names the Resource or the rule).',
|
|
20
20
|
fix: "Use a value the message allows.",
|
|
21
21
|
},
|
|
22
22
|
CONFIG_MOVED_PROPERTY: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AvAdapter } from "../adapters/adapter.interface.js";
|
|
2
2
|
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
3
3
|
import type { AvTransactionCapability } from "../contracts/transaction-capability.js";
|
|
4
|
+
import type { AvPolicyEngine } from "../policy/policy-engine.js";
|
|
4
5
|
import type { AvDiagnosticsSink } from "../runtime/diagnostics-reporter.js";
|
|
5
6
|
import type { AvMisuseSink } from "../runtime/misuse-reporter.js";
|
|
6
7
|
import type { AvConfigAgentParent } from "./config-layer.js";
|
|
@@ -8,6 +9,17 @@ import { type ResolvedComputeConfig } from "./field-compute.resolver.js";
|
|
|
8
9
|
import type { AvConfig, AvConfigLimits } from "./framework-config.js";
|
|
9
10
|
import type { AvConfigFieldDefinition } from "./framework-field-definition.js";
|
|
10
11
|
import { type ResolvedPipelineTree } from "./pipeline-tree.resolver.js";
|
|
12
|
+
/** The root's `policy`, its engine's passive lists read once. */
|
|
13
|
+
export type ResolvedPolicyConfig = {
|
|
14
|
+
readonly engine: AvPolicyEngine;
|
|
15
|
+
readonly principal: NonNullable<AvConfig["policy"]>["principal"];
|
|
16
|
+
/** The governed fields and relations, by Resource. */
|
|
17
|
+
readonly governed: Readonly<Record<string, readonly string[]>>;
|
|
18
|
+
/** The rule paths, by rule name, each a list of segments. */
|
|
19
|
+
readonly paths: Readonly<Record<string, readonly (readonly string[])[]>>;
|
|
20
|
+
/** The writes whose grants carry an `after` condition, by Resource. */
|
|
21
|
+
readonly afterChecks: Readonly<Record<string, readonly string[]>>;
|
|
22
|
+
};
|
|
11
23
|
/** The root's field definitions, by Resource and field name. */
|
|
12
24
|
export type ResolvedFieldDefinitions = Readonly<Record<string, Readonly<Record<string, AvConfigFieldDefinition>>>>;
|
|
13
25
|
/** One resolved layer of the configuration: root, application, client or agent. */
|
|
@@ -63,6 +75,8 @@ export type ResolvedFrameworkConfig<M extends AvAdapterModel = AvAdapterModel> =
|
|
|
63
75
|
readonly diagnostics: AvDiagnosticsSink | undefined;
|
|
64
76
|
/** The misuse sink, from the root configuration only. */
|
|
65
77
|
readonly misuse: AvMisuseSink | undefined;
|
|
78
|
+
/** The policy, from the root configuration only; absent, nothing is checked. */
|
|
79
|
+
readonly policy: ResolvedPolicyConfig | undefined;
|
|
66
80
|
/** Every field definition, from the root only; the same for all three layers. */
|
|
67
81
|
readonly definitions: ResolvedFieldDefinitions;
|
|
68
82
|
/** Each layer's `resources` tree as authored: the compiler checks its names. */
|
|
@@ -140,6 +140,36 @@ function resolveMcp(mcp) {
|
|
|
140
140
|
}),
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
|
+
function frozenLists(value) {
|
|
144
|
+
if (!isPlainRecord(value)) {
|
|
145
|
+
return Object.freeze({});
|
|
146
|
+
}
|
|
147
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, names]) => [
|
|
148
|
+
key,
|
|
149
|
+
Object.freeze([...names]),
|
|
150
|
+
])));
|
|
151
|
+
}
|
|
152
|
+
function frozenPathLists(value) {
|
|
153
|
+
if (!isPlainRecord(value)) {
|
|
154
|
+
return Object.freeze({});
|
|
155
|
+
}
|
|
156
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, paths]) => [
|
|
157
|
+
key,
|
|
158
|
+
Object.freeze(paths.map((path) => Object.freeze([...path]))),
|
|
159
|
+
])));
|
|
160
|
+
}
|
|
161
|
+
function resolvePolicy(policy) {
|
|
162
|
+
if (policy === undefined) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
engine: policy.engine,
|
|
167
|
+
principal: policy.principal,
|
|
168
|
+
governed: frozenLists(policy.engine.governed),
|
|
169
|
+
paths: frozenPathLists(policy.engine.paths),
|
|
170
|
+
afterChecks: frozenLists(policy.engine.afterChecks),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
143
173
|
export function resolveFrameworkConfig(config) {
|
|
144
174
|
const root = config;
|
|
145
175
|
const application = config.application;
|
|
@@ -158,6 +188,7 @@ export function resolveFrameworkConfig(config) {
|
|
|
158
188
|
adapter: config.adapter,
|
|
159
189
|
diagnostics: config.diagnostics,
|
|
160
190
|
misuse: config.misuse,
|
|
191
|
+
policy: resolvePolicy(config.policy),
|
|
161
192
|
definitions: definitionsOf(root.resources),
|
|
162
193
|
trees: {
|
|
163
194
|
root: root.resources,
|
|
@@ -5,6 +5,8 @@ const ROOT_ONLY_PROPERTIES = new Set([
|
|
|
5
5
|
"api",
|
|
6
6
|
"mcp",
|
|
7
7
|
"adapter",
|
|
8
|
+
"prebuilt",
|
|
9
|
+
"policy",
|
|
8
10
|
"diagnostics",
|
|
9
11
|
"misuse",
|
|
10
12
|
"application",
|
|
@@ -618,6 +620,14 @@ export function validateFrameworkConfig(value) {
|
|
|
618
620
|
else {
|
|
619
621
|
validateAdapter(value.adapter, diagnostics);
|
|
620
622
|
}
|
|
623
|
+
if (Object.hasOwn(value, "prebuilt") &&
|
|
624
|
+
value.prebuilt !== undefined &&
|
|
625
|
+
(value.prebuilt === null || typeof value.prebuilt !== "object")) {
|
|
626
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "prebuilt", "prebuilt must be the generated prebuilt Contracts object when provided"));
|
|
627
|
+
}
|
|
628
|
+
if (Object.hasOwn(value, "policy") && value.policy !== undefined) {
|
|
629
|
+
validatePolicy(value.policy, diagnostics);
|
|
630
|
+
}
|
|
621
631
|
if (Object.hasOwn(value, "diagnostics") &&
|
|
622
632
|
value.diagnostics !== undefined &&
|
|
623
633
|
typeof value.diagnostics !== "function") {
|
|
@@ -639,6 +649,78 @@ export function validateFrameworkConfig(value) {
|
|
|
639
649
|
}
|
|
640
650
|
return diagnostics;
|
|
641
651
|
}
|
|
652
|
+
const POLICY_PROPERTIES = new Set(["engine", "principal"]);
|
|
653
|
+
function validatePolicy(value, diagnostics) {
|
|
654
|
+
if (!isPlainRecord(value)) {
|
|
655
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "policy", "policy must be { engine, principal }"));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
for (const key of Object.keys(value)) {
|
|
659
|
+
if (!POLICY_PROPERTIES.has(key)) {
|
|
660
|
+
diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", `policy.${key}`, `unknown policy property: ${key}`));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (!Object.hasOwn(value, "principal")) {
|
|
664
|
+
diagnostics.push(diagnostic("CONFIG_REQUIRED", "policy.principal", "policy.principal is required"));
|
|
665
|
+
}
|
|
666
|
+
else if (typeof value.principal !== "function") {
|
|
667
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "policy.principal", "policy.principal must be a function"));
|
|
668
|
+
}
|
|
669
|
+
if (!Object.hasOwn(value, "engine")) {
|
|
670
|
+
diagnostics.push(diagnostic("CONFIG_REQUIRED", "policy.engine", "policy.engine is required"));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const engine = value.engine;
|
|
674
|
+
if (engine === null || typeof engine !== "object") {
|
|
675
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "policy.engine", "policy.engine must be an object"));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const members = engine;
|
|
679
|
+
for (const question of ["can", "scopeOf", "fieldsOf"]) {
|
|
680
|
+
if (typeof members[question] !== "function") {
|
|
681
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `policy.engine.${question}`, `policy.engine.${question} must be a function`));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
validatePolicyLists(members.governed, "governed", { all: "string lists", one: "a list of strings" }, isStringList, diagnostics);
|
|
685
|
+
validatePolicyLists(members.paths, "paths", {
|
|
686
|
+
all: "path lists",
|
|
687
|
+
one: "a list of paths, each a list of strings",
|
|
688
|
+
}, (paths) => Array.isArray(paths) && paths.every(isStringList), diagnostics);
|
|
689
|
+
validatePolicyLists(members.afterChecks, "afterChecks", { all: "action lists", one: "a list of actions" }, isStringList, diagnostics);
|
|
690
|
+
validateAfterCheckActions(members.afterChecks, diagnostics);
|
|
691
|
+
}
|
|
692
|
+
function validateAfterCheckActions(lists, diagnostics) {
|
|
693
|
+
if (!isPlainRecord(lists)) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
for (const [resource, actions] of Object.entries(lists)) {
|
|
697
|
+
if (!isStringList(actions)) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
for (const action of actions) {
|
|
701
|
+
if (action !== "update") {
|
|
702
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", `policy.engine.afterChecks.${resource}`, `policy.engine.afterChecks.${resource}: "${action}" is no action an after check runs on; only "update" is`));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function isStringList(value) {
|
|
708
|
+
return (Array.isArray(value) && value.every((name) => typeof name === "string"));
|
|
709
|
+
}
|
|
710
|
+
function validatePolicyLists(lists, list, entry, isEntry, diagnostics) {
|
|
711
|
+
if (lists === undefined) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (!isPlainRecord(lists)) {
|
|
715
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `policy.engine.${list}`, `policy.engine.${list} must be an object of ${entry.all} when provided`));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
for (const [key, value] of Object.entries(lists)) {
|
|
719
|
+
if (!isEntry(value)) {
|
|
720
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `policy.engine.${list}.${key}`, `policy.engine.${list}.${key} must be ${entry.one}`));
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
642
724
|
function validateMcp(value, api, diagnostics) {
|
|
643
725
|
if (typeof value === "string") {
|
|
644
726
|
validateMcpPath(value, "mcp", api, diagnostics);
|
|
@@ -238,6 +238,11 @@ type RootSchema<M extends AvAdapterModel, Cfg> = LayerKeysSchema & {
|
|
|
238
238
|
readonly api: Leaf;
|
|
239
239
|
readonly mcp: McpSchema;
|
|
240
240
|
readonly adapter: Leaf;
|
|
241
|
+
readonly prebuilt: Leaf;
|
|
242
|
+
readonly policy: {
|
|
243
|
+
readonly engine: Leaf;
|
|
244
|
+
readonly principal: Leaf;
|
|
245
|
+
};
|
|
241
246
|
readonly diagnostics: Leaf;
|
|
242
247
|
readonly misuse: Leaf;
|
|
243
248
|
readonly resources: RootResourcesSchema<M>;
|
|
@@ -2,10 +2,13 @@ import type { AvAdapter } from "../adapters/adapter.interface.js";
|
|
|
2
2
|
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
3
3
|
import type { AvContractLimits } from "../contracts/contract-limits.js";
|
|
4
4
|
import type { AvTransactionCapability } from "../contracts/transaction-capability.js";
|
|
5
|
+
import type { AvPolicyEngine } from "../policy/policy-engine.js";
|
|
6
|
+
import type { AvPrebuiltContracts } from "../prebuilt/prebuilt-contracts.js";
|
|
5
7
|
import type { AvDiagnosticsSink } from "../runtime/diagnostics-reporter.js";
|
|
6
8
|
import type { AvMisuseSink } from "../runtime/misuse-reporter.js";
|
|
9
|
+
import type { AvState } from "../runtime/request-state.js";
|
|
7
10
|
import type { AvConfigAny } from "./any-framework-config.js";
|
|
8
|
-
import type { AvConfigLayerName } from "./config-layer.js";
|
|
11
|
+
import type { AvConfigLayerName, AvRequestOrigin } from "./config-layer.js";
|
|
9
12
|
import type { AvConfigExemptions } from "./framework-exemptions-config.js";
|
|
10
13
|
import type { AvConfigLayerKind, AvConfigResources, LayerOfKind } from "./framework-resources-config.js";
|
|
11
14
|
import type { AvPipeline } from "./pipeline-config.js";
|
|
@@ -71,6 +74,32 @@ export type AvConfig<M extends AvAdapterModel = AvAdapterModel, Cfg = AvConfigAn
|
|
|
71
74
|
*/
|
|
72
75
|
readonly mcp?: AvConfigMcp;
|
|
73
76
|
readonly adapter: AvAdapter<M>;
|
|
77
|
+
/**
|
|
78
|
+
* Contracts compiled ahead of time (the generated `aventaraContracts`), for a
|
|
79
|
+
* fast cold start on serverless and edge platforms. Used only when they were
|
|
80
|
+
* compiled from this configuration and the adapter's model by this core;
|
|
81
|
+
* otherwise the Contracts are compiled at startup, as without it, and a
|
|
82
|
+
* `AVENTARA_PREBUILT_STALE` warning says why.
|
|
83
|
+
*/
|
|
84
|
+
readonly prebuilt?: AvPrebuiltContracts;
|
|
85
|
+
/**
|
|
86
|
+
* The policy engine every client and agent request is checked against, and
|
|
87
|
+
* how a request's principal is derived from its `ctx.$state`. Application
|
|
88
|
+
* calls bypass it unless they name a principal (`{ as }`); calls a stage
|
|
89
|
+
* makes through `ctx` always do.
|
|
90
|
+
*/
|
|
91
|
+
readonly policy?: {
|
|
92
|
+
readonly engine: AvPolicyEngine;
|
|
93
|
+
/**
|
|
94
|
+
* The request's principal, from its `ctx.$state` and its layer; `null` for
|
|
95
|
+
* an anonymous caller. It may answer with a promise (rc.5), which is
|
|
96
|
+
* awaited; a throw or a rejection answers `A3000`.
|
|
97
|
+
*/
|
|
98
|
+
readonly principal: (facts: {
|
|
99
|
+
readonly $state: AvState;
|
|
100
|
+
readonly $origin: AvRequestOrigin;
|
|
101
|
+
}) => unknown;
|
|
102
|
+
};
|
|
74
103
|
readonly diagnostics?: AvDiagnosticsSink;
|
|
75
104
|
/** Told about every caller value refused as unreadable or not plain data. */
|
|
76
105
|
readonly misuse?: AvMisuseSink;
|
|
@@ -498,6 +498,10 @@ export declare const CLIENT_CONTRACT_STRUCTURE: {
|
|
|
498
498
|
readonly minimum: 0;
|
|
499
499
|
readonly unique: false;
|
|
500
500
|
};
|
|
501
|
+
readonly governed: {
|
|
502
|
+
readonly is: "literal";
|
|
503
|
+
readonly values: readonly [true];
|
|
504
|
+
};
|
|
501
505
|
};
|
|
502
506
|
};
|
|
503
507
|
readonly relation: {
|
|
@@ -694,6 +698,10 @@ export declare const CLIENT_CONTRACT_STRUCTURE: {
|
|
|
694
698
|
readonly minimum: 1;
|
|
695
699
|
readonly unique: false;
|
|
696
700
|
};
|
|
701
|
+
readonly governed: {
|
|
702
|
+
readonly is: "literal";
|
|
703
|
+
readonly values: readonly [true];
|
|
704
|
+
};
|
|
697
705
|
};
|
|
698
706
|
};
|
|
699
707
|
};
|
|
@@ -237,6 +237,7 @@ const SCALAR_FIELD = object({
|
|
|
237
237
|
format: STRING,
|
|
238
238
|
json: { is: "jsonSchema" },
|
|
239
239
|
validators: list(FIELD_CHECK),
|
|
240
|
+
governed: literal([true]),
|
|
240
241
|
});
|
|
241
242
|
const RELATION_FIELD = object({
|
|
242
243
|
kind: literal(["relation"]),
|
|
@@ -257,7 +258,11 @@ const RELATION_FIELD = object({
|
|
|
257
258
|
update: RELATION_MUTATION,
|
|
258
259
|
nestedWriteFilter: literal(["scalar"]),
|
|
259
260
|
}),
|
|
260
|
-
}, {
|
|
261
|
+
}, {
|
|
262
|
+
inverse: STRING,
|
|
263
|
+
referenceFields: nonEmptyList(STRING),
|
|
264
|
+
governed: literal([true]),
|
|
265
|
+
});
|
|
261
266
|
const OPERATION_AVAILABILITY = union(literal([true]), object({ projection: literal(PROJECTION_SCOPES) }, { write: literal(WRITE_SCOPES) }), object({ write: literal(WRITE_SCOPES) }, { projection: literal(PROJECTION_SCOPES) }));
|
|
262
267
|
const RESOURCE = object({
|
|
263
268
|
fields: registry({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AvContractLimits } from "./contract-limits.js";
|
|
2
2
|
import type { AvContractClientProtocol } from "./contract-protocol.js";
|
|
3
3
|
import type { AvEnumDefinition } from "./enum-definition.js";
|
|
4
|
+
import type { AvCoreField } from "./field.js";
|
|
4
5
|
import type { ReferenceWriteLimit } from "./reference-write-limit.js";
|
|
5
6
|
import type { AvResource } from "./resource.js";
|
|
6
7
|
import type { AvScalarRegistry } from "./scalar.js";
|
|
@@ -24,8 +25,21 @@ export interface AvContract {
|
|
|
24
25
|
*/
|
|
25
26
|
readonly referenceWrites?: ReferenceWriteLimit;
|
|
26
27
|
}
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* The Contract compiled from the root configuration. Its relation fields also
|
|
30
|
+
* state the target's fields their reference names (`references`), which no
|
|
31
|
+
* other Contract carries.
|
|
32
|
+
*/
|
|
28
33
|
export interface AvContractCore extends AvContract {
|
|
34
|
+
/** The Resources, by name. */
|
|
35
|
+
readonly resources: Readonly<Record<string, AvCoreResource>>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A Resource as the Core Contract states it: its fields are
|
|
39
|
+
* {@link AvCoreField}s, so its relation fields carry `references`.
|
|
40
|
+
*/
|
|
41
|
+
export interface AvCoreResource extends AvResource<AvContractCore> {
|
|
42
|
+
readonly fields: Readonly<Record<string, AvCoreField<AvContractCore>>>;
|
|
29
43
|
}
|
|
30
44
|
/** The Contract in-process calls run under (`framework.application`). */
|
|
31
45
|
export interface AvContractApplication extends AvContract {
|
|
@@ -138,6 +138,12 @@ export type AvScalarField<C extends AvContract> = {
|
|
|
138
138
|
* `list: true` length check, which applies to the list.
|
|
139
139
|
*/
|
|
140
140
|
readonly validators?: readonly AvFieldCheck[];
|
|
141
|
+
/**
|
|
142
|
+
* `true` when the policy governs the field: it is left out of every default
|
|
143
|
+
* projection, and a caller that wants it names it (`select: ["$defaults",
|
|
144
|
+
* "email"]`).
|
|
145
|
+
*/
|
|
146
|
+
readonly governed?: true;
|
|
141
147
|
};
|
|
142
148
|
/** A relation field; the `target` Resource describes the related row. */
|
|
143
149
|
export type AvRelationField<C extends AvContract> = {
|
|
@@ -162,6 +168,28 @@ export type AvRelationField<C extends AvContract> = {
|
|
|
162
168
|
* holds no reference, and where the layer shows none of them.
|
|
163
169
|
*/
|
|
164
170
|
readonly referenceFields?: readonly [string, ...string[]];
|
|
171
|
+
/**
|
|
172
|
+
* `true` when the policy engine's `governed` set names the relation: including
|
|
173
|
+
* it on rows the caller may not read it on refuses the request.
|
|
174
|
+
*/
|
|
175
|
+
readonly governed?: true;
|
|
165
176
|
};
|
|
166
177
|
/** One field of a Resource: a scalar or a relation. */
|
|
167
178
|
export type AvField<C extends AvContract> = AvScalarField<C> | AvRelationField<C>;
|
|
179
|
+
/**
|
|
180
|
+
* A relation field as the Core Contract states it: an {@link AvRelationField}
|
|
181
|
+
* and, on the end that holds the reference, the target's fields it names.
|
|
182
|
+
*/
|
|
183
|
+
export type AvCoreRelationField<C extends AvContract> = AvRelationField<C> & {
|
|
184
|
+
/**
|
|
185
|
+
* The target's scalar fields that `referenceFields` name, pair by pair in the
|
|
186
|
+
* same order (`Post.author`: `referenceFields: ["authorId"]`,
|
|
187
|
+
* `references: ["id"]`). Present only beside a `referenceFields` the
|
|
188
|
+
* Contract states whole, and only when every one of them is a scalar field
|
|
189
|
+
* the Core Contract shows on the target; absent on the end that holds no
|
|
190
|
+
* reference, where the other end (`inverse`) states the pairs.
|
|
191
|
+
*/
|
|
192
|
+
readonly references?: readonly [string, ...string[]];
|
|
193
|
+
};
|
|
194
|
+
/** One field of a Resource as the Core Contract states it. */
|
|
195
|
+
export type AvCoreField<C extends AvContract> = AvScalarField<C> | AvCoreRelationField<C>;
|
|
@@ -78,5 +78,7 @@ type ListValue<List, Atom> = [List] extends [false] ? Atom : [List] extends [tru
|
|
|
78
78
|
* - `list: true` → `readonly T[]`, whose entries are never `null`;
|
|
79
79
|
* - `nullable: true` → `| null`, on the whole value, list or not.
|
|
80
80
|
*/
|
|
81
|
-
export type AvScalarValueType<C extends AvContract, Field extends AvScalarField<C>, Forms extends ScalarForms = AvApplicationScalarForms> =
|
|
81
|
+
export type AvScalarValueType<C extends AvContract, Field extends AvScalarField<C>, Forms extends ScalarForms = AvApplicationScalarForms> = [
|
|
82
|
+
ListValue<Field["list"], FieldAtom<C, Field, Forms>> | (true extends Field["nullable"] ? null : never)
|
|
83
|
+
] extends [infer Value] ? Value : never;
|
|
82
84
|
export {};
|