@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
@@ -66,12 +66,12 @@ export const OPERATION_CODE_GUIDANCE = {
66
66
  },
67
67
  A2003: {
68
68
  message: "Not found.",
69
- cause: "A unique operation (find.unique, update.unique, delete.unique) matched no row, or a $connect names a record that does not exist. The operation fails, so no after hook runs.",
69
+ cause: "A unique operation (find.unique, update.unique, delete.unique) matched no row, or a $connect names a record that does not exist. The operation fails, so no after hook runs. Under a policy, a row outside the caller's read scope is a miss too, and so is a single-row update or delete of a row the caller cannot read, so the answer does not reveal that the row exists. A row the caller can read but not change answers A4002.",
70
70
  fix: "Catch AvNotFoundError, or use a first-style operation, which answers A1001 with null. Check the $connect identifier.",
71
71
  },
72
72
  A2004: {
73
73
  message: "Validation failed.",
74
- cause: 'The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator\'s issue also has check and params. The validators judge the values after the pipes and write compute, so a computed value they refuse is here too, at its field. A required create field left out is here (V1000). So is a value the database itself refused: too long for its column or out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]` and its message saying what the database refused. So is a record written through a relation that writes that relation, or a field holding its reference, back (V1012, at that field), where the adapter admits no such write.',
74
+ cause: 'The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator\'s issue also has check and params. The validators judge the values after the pipes and write compute, so a computed value they refuse is here too, at its field. A required create field left out is here (V1000). So is a value the database itself refused: too long for its column or its index, or out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]`, or at the value where the adapter can place it (a bigint its driver cannot bind), and its message saying what the database refused. So is a record written through a relation that writes that relation, or a field holding its reference, back (V1012, at that field), where the adapter admits no such write.',
75
75
  fix: "Fix the argument each issue's path points at, as its V-code says. A field the server must fill takes a compute.create stage. For a value the database refused, send one its column holds. For a nested write that writes its relation back, leave that field out: the parent supplies it.",
76
76
  },
77
77
  A2005: {
@@ -161,7 +161,7 @@ export const OPERATION_CODE_GUIDANCE = {
161
161
  },
162
162
  A4002: {
163
163
  message: "Policy denied.",
164
- cause: 'A guard or hook threw new AvError("A4002", ...) with a policy reason of your own.',
165
- fix: "Read cause.message for the policy's reason.",
164
+ cause: "The configuration's policy refused the request: its engine does not allow the operation, a field the request names, writes or returns, a row it reaches, a row it creates, or the row a change leaves (moved out of what the caller reads, or failing its grant's after condition, which rolls the write back); on a database without interactive transactions, a write the policy can only check after it runs; or a stage threw new AvError(\"A4002\", ...).",
165
+ fix: "Read cause.message, which names what the policy refused. Call as a principal the policy allows, leave the refused field or relation out (a governed one is named only where every row grants it), or change only rows the caller may change and still read afterwards.",
166
166
  },
167
167
  };
@@ -1,4 +1,5 @@
1
1
  import type { AvOperationIdentity } from "../operations/operation-identity.js";
2
+ import type { PolicySession } from "../policy/policy-session.js";
2
3
  import type { AvTransportContext } from "./pipeline.js";
3
4
  import type { AvState } from "./request-state.js";
4
5
  /**
@@ -63,5 +64,11 @@ export type OperationExecutionRequest = WithoutArguments<FrameworkOperationReque
63
64
  */
64
65
  export type StatefulOperationRequest = OperationExecutionRequest & {
65
66
  readonly state: AvState;
67
+ /**
68
+ * The request's policy session, when the policy checks it: a client's or an
69
+ * agent's request, or an application call made `as` a principal. Absent, nothing
70
+ * is checked: an application call, or a call a stage made through `ctx`.
71
+ */
72
+ readonly policy?: PolicySession;
66
73
  };
67
74
  export {};
@@ -7,6 +7,7 @@ import type { ResolvedFrameworkConfig } from "../config/config.resolver.js";
7
7
  import type { AvRequestOrigin, RemoteRequestOrigin } from "../config/config-layer.js";
8
8
  import type { AvContract } from "../contracts/contract.js";
9
9
  import type { OperationValidationOptions, SemanticOperationArguments } from "../operations/operation-validator.js";
10
+ import { type PolicyCaller } from "../policy/policy-request.js";
10
11
  import type { FrameworkTransactionRequest } from "../transactions/transaction-request.js";
11
12
  import { type ArgumentsSource } from "./caller-data.door.js";
12
13
  import { type ContextExecutor, type ExecutorScope } from "./context-executor.runtime.js";
@@ -65,6 +66,11 @@ export declare function withRequestState<Request extends {
65
66
  }>(request: Request): Request & {
66
67
  readonly state: AvState;
67
68
  };
69
+ /**
70
+ * `request` with its policy session when the policy checks it, or the failure
71
+ * deriving its principal answered.
72
+ */
73
+ export declare function withRequestPolicy<M extends AvAdapterModel>(dependencies: Pick<OperationRuntimeDependencies<M>, "adapter" | "config" | "contracts">, request: StatefulOperationRequest, caller: PolicyCaller | undefined, report: DiagnosticsReporter): StatefulOperationRequest | AvOperationFailureResponse;
68
74
  /**
69
75
  * Builds what every operation's pipeline shares, the ctx executor included: its
70
76
  * raw calls run the application's operation path with every developer stage
@@ -79,7 +85,9 @@ export declare function createPipelineDependencies<M extends AvAdapterModel>(dep
79
85
  * request's arguments are the caller's own objects, copied at the door, or data
80
86
  * no caller holds (see {@link ArgumentsSource}).
81
87
  */
82
- export type FrameworkOperationExecutor = (request: FrameworkOperationRequest, report: DiagnosticsReporter, source: ArgumentsSource) => Promise<AvOperationResponse>;
88
+ export type FrameworkOperationExecutor = (request: FrameworkOperationRequest, report: DiagnosticsReporter, source: ArgumentsSource,
89
+ /** The principal an application call runs as (`{ as }`), when it names one. */
90
+ caller?: PolicyCaller) => Promise<AvOperationResponse>;
83
91
  /** Selects the Contract of the request's origin. */
84
92
  export declare function activeContract(contracts: AvContracts, request: FrameworkOperationRequest | FrameworkTransactionRequest): AvContract;
85
93
  /** Creates the transport-independent operation executor owned by core. */
@@ -3,6 +3,9 @@ import { createComputedFields } from "../config/field-compute.resolver.js";
3
3
  import { fieldStagesOf, isForcedOperation, nestedTargetLevels, operationLevels, rawPipelineTree, transactionLevelsOf, } from "../config/pipeline-tree.resolver.js";
4
4
  import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
5
5
  import { findOperationAvailabilityIssue, validateOperationArguments, } from "../operations/operation-validator.js";
6
+ import { enforceReadPolicy, } from "../policy/policy-read.runtime.js";
7
+ import { admitRequestPolicy, openRequestPolicy, } from "../policy/policy-request.js";
8
+ import { enforceWritePolicy, writeNeedsTransaction, } from "../policy/policy-write.runtime.js";
6
9
  import { transactionInfrastructureFailure } from "../transactions/transaction-response.js";
7
10
  import { settleTransaction } from "../transactions/transaction-settlement.runtime.js";
8
11
  import { TRANSACTION_PIPES, transactionFacts, } from "../transactions/transaction-stage.runtime.js";
@@ -15,7 +18,7 @@ import { createDiagnosticsReporter, } from "./diagnostics-reporter.js";
15
18
  import { AdapterBoundary, OPERATION_ARGUMENTS_PATH, resolveOperationFailure, sanitizeOperationFailure, } from "./error-boundary.js";
16
19
  import { planFieldStages } from "./field-stage.plan.js";
17
20
  import { guardSentFields, pipeSentFields, valueAt, writeRecordPathsOf, } from "./field-stage.runtime.js";
18
- import { fieldValidatorIssues } from "./field-validator.runtime.js";
21
+ import { fieldValidatorIssues, hiddenFieldRefusal, } from "./field-validator.runtime.js";
19
22
  import { thrownFailure } from "./framework-error.factory.js";
20
23
  import { AvInterceptorNextError } from "./interceptor-next.error.js";
21
24
  import { runLevelLifecycle, } from "./level-lifecycle.runtime.js";
@@ -40,6 +43,16 @@ function scopeOf(request, binding) {
40
43
  export function withRequestState(request) {
41
44
  return { ...request, state: requestStateOf(request.state) };
42
45
  }
46
+ export function withRequestPolicy(dependencies, request, caller, report) {
47
+ try {
48
+ const policy = openRequestPolicy(dependencies.config.policy, dependencies.contracts.core, request, caller, dependencies.adapter);
49
+ return policy === undefined ? request : { ...request, policy };
50
+ }
51
+ catch (error) {
52
+ report(error, "A3000");
53
+ return internalFailure();
54
+ }
55
+ }
43
56
  export function createPipelineDependencies(dependencies, model) {
44
57
  const misuse = createMisuseReporter(dependencies.config.misuse);
45
58
  const recordWrites = recordWriteFactsOf({
@@ -170,7 +183,7 @@ function outputAccepted(contract, request, projection, data, report) {
170
183
  }
171
184
  export function createOperationRuntime(dependencies, pipelineDependencies) {
172
185
  const misuse = createMisuseReporter(dependencies.config.misuse);
173
- return async (request, report, source) => {
186
+ return async (request, report, source, caller) => {
174
187
  let validated;
175
188
  try {
176
189
  validated = validateOperation(dependencies, request, source, misuse);
@@ -184,7 +197,10 @@ export function createOperationRuntime(dependencies, pipelineDependencies) {
184
197
  if (!validated.ok) {
185
198
  return validated.response;
186
199
  }
187
- const stateful = withRequestState(request);
200
+ const stateful = withRequestPolicy(dependencies, withRequestState(request), caller, report);
201
+ if (!("state" in stateful)) {
202
+ return stateful;
203
+ }
188
204
  const forced = isForcedOperation(activeScopeConfig(dependencies.config, request).pipelines, request);
189
205
  const admitted = await admitOperation(pipelineDependencies, stateful, validated.contract, validated.arguments, OPERATION_ARGUMENTS_PATH, report, {
190
206
  requestId: request.requestId,
@@ -194,8 +210,20 @@ export function createOperationRuntime(dependencies, pipelineDependencies) {
194
210
  if (!admitted.ok) {
195
211
  return admitted.response;
196
212
  }
197
- if (forced) {
198
- return executeForcedOperation(pipelineDependencies, dependencies.adapter, stateful, validated.contract, admitted.args, report);
213
+ let checkedWrite = false;
214
+ if (stateful.policy !== undefined &&
215
+ request.family !== "find" &&
216
+ validated.contract.transactions === "interactive" &&
217
+ !forced) {
218
+ try {
219
+ checkedWrite = await writeNeedsTransaction(stateful.policy, dependencies.contracts.core, stateful, () => planReadOperation(dependencies.contracts.core, dependencies.config.adapter.model, validated.contract, activeScopeConfig(dependencies.config, request).compute, stateful, admitted.args), admitted.args);
220
+ }
221
+ catch (error) {
222
+ return sanitizeOperationFailure(error, stateful.policy.execution.boundary.originOf(error), report, { path: OPERATION_ARGUMENTS_PATH, arguments: admitted.args });
223
+ }
224
+ }
225
+ if (forced || checkedWrite) {
226
+ return executeForcedOperation(pipelineDependencies, dependencies.adapter, stateful, validated.contract, admitted.args, report, forced);
199
227
  }
200
228
  return executeAdmittedOperation(pipelineDependencies, stateful, validated.contract, admitted.args, OPERATION_ARGUMENTS_PATH, { kind: "standalone", executor: dependencies.adapter }, report);
201
229
  };
@@ -346,6 +374,12 @@ export async function admitOperation(dependencies, request, contract, args, argu
346
374
  if (absent.length > 0) {
347
375
  return { ok: false, response: mapValidationOutcome(absent) };
348
376
  }
377
+ if (request.policy !== undefined) {
378
+ const refused = await admitRequestPolicy(request.policy, dependencies.contracts.core, contract, request, current, report, { path: argumentsPath, arguments: current });
379
+ if (refused !== undefined) {
380
+ return { ok: false, response: refused };
381
+ }
382
+ }
349
383
  try {
350
384
  return await admit();
351
385
  }
@@ -441,7 +475,7 @@ export async function prepareStep(dependencies, request, contract, args, argumen
441
475
  };
442
476
  }
443
477
  }
444
- async function executeForcedOperation(dependencies, adapter, request, contract, args, report) {
478
+ async function executeForcedOperation(dependencies, adapter, request, contract, args, report, forced) {
445
479
  const plan = [
446
480
  {
447
481
  resource: request.resource,
@@ -452,7 +486,9 @@ async function executeForcedOperation(dependencies, adapter, request, contract,
452
486
  ];
453
487
  const settled = await settleTransaction({
454
488
  adapter,
455
- levels: transactionLevelsOf(activeScopeConfig(dependencies.config, request).pipelines, request.resource),
489
+ levels: forced
490
+ ? transactionLevelsOf(activeScopeConfig(dependencies.config, request).pipelines, request.resource)
491
+ : [],
456
492
  plan,
457
493
  contextOf: (current, executor) => dependencies.contexts.transaction({
458
494
  requestId: request.requestId,
@@ -539,7 +575,30 @@ export async function executeAdmittedOperation(dependencies, request, contract,
539
575
  return mapValidationOutcome(refused);
540
576
  }
541
577
  }
578
+ const hiddenRefusal = hiddenFieldRefusal({ core: dependencies.contracts.core, active: contract }, dependencies.recordWrites.references, request, computed, argumentsPath);
579
+ if (hiddenRefusal !== undefined) {
580
+ throw hiddenRefusal;
581
+ }
542
582
  args = deepFreeze(computed);
583
+ let policyWrite;
584
+ if (request.policy !== undefined && request.family !== "find") {
585
+ const planned = planReadOperation(dependencies.contracts.core, dependencies.config.adapter.model, contract, scopeConfig.compute, request, args);
586
+ const plannedArguments = await stripVirtualMutationInput(contracts, request, planned.arguments);
587
+ try {
588
+ policyWrite = await enforceWritePolicy(request.policy.on({ executor, boundary: adapterBoundary }), dependencies.contracts.core, dependencies.config.adapter.model, request, planned, plannedArguments, binding.kind === "transaction");
589
+ }
590
+ catch (error) {
591
+ return sanitizeOperationFailure(error, adapterBoundary.originOf(error), report, { path: argumentsPath, arguments: args });
592
+ }
593
+ if (policyWrite.kind === "refused") {
594
+ return policyWrite.response;
595
+ }
596
+ }
597
+ let policyFailure;
598
+ const refuseWrite = (response) => {
599
+ policyFailure = response;
600
+ throw thrownFailure(response);
601
+ };
543
602
  let readMaterialized = false;
544
603
  let executedReadPlan;
545
604
  const materialize = async (data, plan) => {
@@ -548,24 +607,73 @@ export async function executeAdmittedOperation(dependencies, request, contract,
548
607
  return materializeReadOperation(contract, scopeConfig.compute, request, plan, data, computeContext);
549
608
  };
550
609
  const invoke = async () => {
551
- const readPlan = planReadOperation(dependencies.contracts.core, dependencies.config.adapter.model, contract, scopeConfig.compute, request, args);
552
- const adapterArguments = await stripVirtualMutationInput(contracts, request, readPlan.arguments);
553
- const identity = request;
554
- const adapterData = await adapterBoundary.execute(() => executor.execute({
610
+ let readPlan = planReadOperation(dependencies.contracts.core, dependencies.config.adapter.model, contract, scopeConfig.compute, request, args);
611
+ let adapterArguments = await stripVirtualMutationInput(contracts, request, readPlan.arguments);
612
+ let operation = request;
613
+ let settle;
614
+ let writeMiss;
615
+ if (policyWrite !== undefined && policyWrite.kind !== "refused") {
616
+ writeMiss = policyWrite.miss;
617
+ if (policyWrite.kind === "empty") {
618
+ if (writeMiss !== undefined && policyWrite.data === null) {
619
+ refuseWrite(await writeMiss());
620
+ }
621
+ return materialize(policyWrite.data, readPlan);
622
+ }
623
+ operation = policyWrite.identity;
624
+ adapterArguments = policyWrite.arguments;
625
+ readPlan = policyWrite.plan;
626
+ settle = policyWrite.settle;
627
+ }
628
+ else if (request.policy !== undefined && request.family === "find") {
629
+ const read = await enforceReadPolicy(request.policy.on({ executor, boundary: adapterBoundary }), dependencies.contracts.core, dependencies.config.adapter.model, request, readPlan, adapterArguments);
630
+ if (read.kind === "refused") {
631
+ throw thrownFailure(read.response);
632
+ }
633
+ if (read.kind === "empty") {
634
+ const miss = uniqueMissFailure(request, read.data);
635
+ if (miss !== undefined) {
636
+ throw thrownFailure(miss);
637
+ }
638
+ return materialize(read.data, readPlan);
639
+ }
640
+ operation = read.identity;
641
+ adapterArguments = read.arguments;
642
+ readPlan = read.plan;
643
+ settle = read.settle;
644
+ }
645
+ const { policy: _session, ...identity } = operation;
646
+ let adapterData = await adapterBoundary.execute(() => executor.execute({
555
647
  ...identity,
556
648
  resource: request.resource,
557
649
  arguments: adapterArguments,
558
650
  }));
651
+ if (writeMiss !== undefined && adapterData === null) {
652
+ refuseWrite(await writeMiss());
653
+ }
559
654
  const miss = uniqueMissFailure(request, adapterData);
560
655
  if (miss !== undefined) {
561
656
  throw thrownFailure(miss);
562
657
  }
658
+ if (settle !== undefined) {
659
+ const settled = await settle(adapterData);
660
+ if (!settled.ok) {
661
+ if (policyWrite !== undefined) {
662
+ refuseWrite(settled.response);
663
+ }
664
+ throw thrownFailure(settled.response);
665
+ }
666
+ adapterData = settled.data;
667
+ }
563
668
  return materialize(adapterData, readPlan);
564
669
  };
565
670
  const materialized = async (data) => readMaterialized
566
671
  ? data
567
672
  : materialize(data, planReadOperation(dependencies.contracts.core, dependencies.config.adapter.model, contract, scopeConfig.compute, request, args));
568
673
  const data = await materialized(await runLevelLifecycle(levels, context, invoke, materialized, reuse, () => new AvInterceptorNextError()));
674
+ if (policyFailure !== undefined) {
675
+ return policyFailure;
676
+ }
569
677
  if (!outputAccepted(contract, request, executedReadPlan?.projection, data, report)) {
570
678
  return internalFailure();
571
679
  }
@@ -32,8 +32,12 @@ export declare function deepFreeze<T>(value: T): T;
32
32
  * only reads is told — the validated arguments after validation, a result in an
33
33
  * after hook — so neither what is written nor the result the caller receives can
34
34
  * be changed through it.
35
+ *
36
+ * With `snapshot`, a copied container found to hold no `Date` or `Uint8Array`
37
+ * is remembered as such at once, so a later `frozenCopy` of the copy shares it
38
+ * without walking it: for a copy that is itself copied again for each stage.
35
39
  */
36
- export declare function frozenCopy<T>(value: T): AvDeepReadonly<T>;
40
+ export declare function frozenCopy<T>(value: T, snapshot?: boolean): AvDeepReadonly<T>;
37
41
  /**
38
42
  * A read-only view of `value`: reads see the live value, nested plain objects
39
43
  * and arrays as views too, and any write, definition or deletion throws a
@@ -37,38 +37,79 @@ function scalarCopy(value) {
37
37
  return undefined;
38
38
  }
39
39
  const SCALAR_FREE = new WeakSet();
40
- export function frozenCopy(value) {
40
+ function scalarFree(value) {
41
+ return Object.isFrozen(value) && SCALAR_FREE.has(value);
42
+ }
43
+ const WALKING = Symbol("walking");
44
+ function place(target, key, value, enumerable) {
45
+ if (enumerable && typeof key === "string" && !(key in target)) {
46
+ target[key] = value;
47
+ return;
48
+ }
49
+ Object.defineProperty(target, key, {
50
+ value,
51
+ enumerable,
52
+ writable: true,
53
+ configurable: true,
54
+ });
55
+ }
56
+ export function frozenCopy(value, snapshot = false) {
57
+ const scalar = scalarCopy(value);
58
+ if (scalar !== undefined) {
59
+ return scalar;
60
+ }
61
+ if (!isContainer(value) || scalarFree(value)) {
62
+ return value;
63
+ }
41
64
  const copies = new Map();
42
- const walking = new Set();
43
65
  let cycled = false;
66
+ let holdsScalar = false;
44
67
  const copy = (current) => {
68
+ if (current === null || typeof current !== "object") {
69
+ return current;
70
+ }
45
71
  const scalar = scalarCopy(current);
46
72
  if (scalar !== undefined) {
73
+ holdsScalar = true;
47
74
  return scalar;
48
75
  }
49
- if (!isContainer(current) || SCALAR_FREE.has(current)) {
76
+ if (!isContainer(current) || scalarFree(current)) {
50
77
  return current;
51
78
  }
52
- if (copies.has(current)) {
53
- cycled ||= walking.has(current);
54
- return copies.get(current);
79
+ const known = copies.get(current);
80
+ if (known === WALKING) {
81
+ cycled = true;
82
+ return current;
83
+ }
84
+ if (known !== undefined) {
85
+ holdsScalar ||= !scalarFree(known);
86
+ return known;
55
87
  }
56
- copies.set(current, current);
57
- walking.add(current);
58
- const entries = [];
59
- let changed = !DEEP_FROZEN.has(current);
88
+ copies.set(current, WALKING);
89
+ const outer = holdsScalar;
90
+ holdsScalar = false;
91
+ let changed = !(Object.isFrozen(current) && DEEP_FROZEN.has(current));
92
+ const array = Array.isArray(current);
93
+ const prototype = Object.getPrototypeOf(current);
94
+ const target = array
95
+ ? new Array(current.length)
96
+ : prototype === Object.prototype
97
+ ? {}
98
+ : Object.create(prototype);
60
99
  for (const key of Reflect.ownKeys(current)) {
100
+ if (array && key === "length") {
101
+ continue;
102
+ }
61
103
  const descriptor = Object.getOwnPropertyDescriptor(current, key);
62
- if (descriptor === undefined ||
63
- !("value" in descriptor) ||
64
- (Array.isArray(current) && key === "length")) {
104
+ if (descriptor === undefined || !("value" in descriptor)) {
65
105
  continue;
66
106
  }
67
107
  const next = copy(descriptor.value);
68
108
  changed ||= next !== descriptor.value;
69
- entries.push([key, descriptor, next]);
109
+ place(target, key, next, descriptor.enumerable === true);
70
110
  }
71
- walking.delete(current);
111
+ const held = holdsScalar;
112
+ holdsScalar = outer || held;
72
113
  if (!changed) {
73
114
  copies.set(current, current);
74
115
  if (!cycled) {
@@ -76,20 +117,18 @@ export function frozenCopy(value) {
76
117
  }
77
118
  return current;
78
119
  }
79
- const target = Array.isArray(current)
80
- ? new Array(current.length)
81
- : Object.create(Object.getPrototypeOf(current));
82
- for (const [key, descriptor, next] of entries) {
83
- Object.defineProperty(target, key, {
84
- value: next,
85
- enumerable: descriptor.enumerable ?? false,
86
- writable: true,
87
- configurable: true,
88
- });
120
+ Object.freeze(target);
121
+ DEEP_FROZEN.add(target);
122
+ if (cycled) {
123
+ for (const key of Reflect.ownKeys(target)) {
124
+ deepFreeze(target[key]);
125
+ }
126
+ }
127
+ if (snapshot && !held && !cycled) {
128
+ SCALAR_FREE.add(target);
89
129
  }
90
- const frozen = deepFreeze(target);
91
- copies.set(current, frozen);
92
- return frozen;
130
+ copies.set(current, target);
131
+ return target;
93
132
  };
94
133
  return copy(value);
95
134
  }
@@ -1,4 +1,5 @@
1
1
  import type { AvAdapterModel } from "../adapters/adapter-model.js";
2
+ import { type PolicyCaller } from "../policy/policy-request.js";
2
3
  import type { TransactionDiagnosticsChannel } from "../runtime/diagnostics-reporter.js";
3
4
  import type { OperationPipelineDependencies, OperationRuntimeDependencies } from "../runtime/operation-runtime.js";
4
5
  import type { PlanArguments } from "./transaction-plan-validator.js";
@@ -13,7 +14,9 @@ import type { AvTransactionResponse } from "./transaction-response.js";
13
14
  * runner's mixed plans), and `planArguments` whether the steps' arguments are
14
15
  * copied at the door; see `validateTransactionPlan`.
15
16
  */
16
- export type FrameworkTransactionExecutor = (request: FrameworkTransactionRequest, diagnostics: TransactionDiagnosticsChannel, origins: readonly TransactionOperationOrigin[] | undefined, planArguments: PlanArguments) => Promise<AvTransactionResponse>;
17
+ export type FrameworkTransactionExecutor = (request: FrameworkTransactionRequest, diagnostics: TransactionDiagnosticsChannel, origins: readonly TransactionOperationOrigin[] | undefined, planArguments: PlanArguments,
18
+ /** The principal an application run's steps run as (`{ as }`), when it names one. */
19
+ caller?: PolicyCaller) => Promise<AvTransactionResponse>;
17
20
  /**
18
21
  * Validates a whole plan through the Framework Validation Layer, then runs it inside
19
22
  * exactly one Adapter transaction. The touched Resources' transaction stages and
@@ -1,4 +1,5 @@
1
1
  import { transactionLevelsOf } from "../config/pipeline-tree.resolver.js";
2
+ import { openRequestPolicy, } from "../policy/policy-request.js";
2
3
  import { AdapterBoundary, planOperationArgumentsPath, planStepPath, sanitizeOperationFailure, } from "../runtime/error-boundary.js";
3
4
  import { internalFailure, operationSuccess, } from "../runtime/operation-response.js";
4
5
  import { admitOperation, EVERY_FIELD, executeAdmittedOperation, prepareStep, STEP_PIPES, withRequestState, } from "../runtime/operation-runtime.js";
@@ -20,13 +21,13 @@ export function createTransactionRuntime(dependencies, pipelineDependencies) {
20
21
  cascades: resolveCascadeClosure(adapter.referentialActions),
21
22
  sqlStatements: adapter.sqlStatements === true,
22
23
  };
23
- return async (seeded, diagnostics, origins, planArguments) => {
24
+ return async (seeded, diagnostics, origins, planArguments, caller) => {
24
25
  const request = withRequestState(seeded);
25
26
  const validation = await validateTransactionPlan(planDependencies, request, origins, planArguments);
26
27
  if (!validation.ok) {
27
28
  return validation.response;
28
29
  }
29
- return executeTransactionPlan(adapter, pipelineDependencies, request, validation.plan, request.state, diagnostics);
30
+ return executeTransactionPlan(dependencies, pipelineDependencies, request, validation.plan, request.state, diagnostics, caller);
30
31
  };
31
32
  }
32
33
  function planOrigin(request, plan) {
@@ -79,8 +80,21 @@ function standingReferences(core, operation, args, steps, subject) {
79
80
  }
80
81
  return { ok: false, ...refused };
81
82
  }
82
- async function executeTransactionPlan(adapter, dependencies, request, plan, state, diagnostics) {
83
- const stateful = (operation) => ({ ...operation.request, state });
83
+ async function executeTransactionPlan(runtime, dependencies, request, plan, state, diagnostics, caller) {
84
+ const { adapter } = runtime;
85
+ const steps = new Map();
86
+ const stateful = (operation) => {
87
+ const known = steps.get(operation);
88
+ if (known !== undefined) {
89
+ return known;
90
+ }
91
+ const policy = openRequestPolicy(runtime.config.policy, runtime.contracts.core, { origin: operation.request.origin, state }, caller, adapter);
92
+ const step = policy === undefined
93
+ ? { ...operation.request, state }
94
+ : { ...operation.request, state, policy };
95
+ steps.set(operation, step);
96
+ return step;
97
+ };
84
98
  const admitted = [];
85
99
  const standing = [];
86
100
  for (const step of plan.operations) {
@@ -90,7 +104,15 @@ async function executeTransactionPlan(adapter, dependencies, request, plan, stat
90
104
  continue;
91
105
  }
92
106
  const report = diagnostics.operation(step.request, step.index);
93
- const admission = await admitOperation(dependencies, stateful(step), step.contract, step.arguments, planOperationArgumentsPath(step.index), report, { requestId: request.requestId, transaction: undefined, state }, {
107
+ let stepRequest;
108
+ try {
109
+ stepRequest = stateful(step);
110
+ }
111
+ catch (error) {
112
+ report(error, "A3000");
113
+ return transactionOperationFailure(internalFailure(), step.index);
114
+ }
115
+ const admission = await admitOperation(dependencies, stepRequest, step.contract, step.arguments, planOperationArgumentsPath(step.index), report, { requestId: request.requestId, transaction: undefined, state }, {
94
116
  select: EVERY_FIELD,
95
117
  validation: { transactionReferences: true },
96
118
  beforeBegin: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/core",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.4",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Core runtime, contract compiler, and extension APIs for Aventara.",
6
6
  "type": "module",
@@ -45,7 +45,7 @@
45
45
  "access": "public"
46
46
  },
47
47
  "scripts": {
48
- "build": "node ../../scripts/build/build-output.cleaner.ts dist && tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist && node scripts/errors-markdown.generator.ts",
48
+ "build": "node ../../scripts/build/build-output.cleaner.ts dist && node scripts/core-version.generator.ts && tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist && node scripts/errors-markdown.generator.ts",
49
49
  "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
50
50
  "test": "vitest run --typecheck --config ../../vitest.config.mts --root .",
51
51
  "lint": "biome lint src scripts"