@aventara/core 1.0.0-rc.8 → 1.0.0-rc.9

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 (44) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/ERRORS.md +2 -2
  3. package/README.md +75 -6
  4. package/dist/compiler/contract-compiler-diagnostic.guidance.js +2 -2
  5. package/dist/config/config.resolver.d.ts +2 -0
  6. package/dist/config/config.resolver.js +7 -2
  7. package/dist/config/config.validator.js +36 -12
  8. package/dist/config/exact-framework-config.d.ts +11 -1
  9. package/dist/config/framework-config.d.ts +9 -0
  10. package/dist/contracts/contract-canonicalizer.js +2 -0
  11. package/dist/framework/contract-view.d.ts +9 -1
  12. package/dist/framework/framework-construction.error.js +2 -0
  13. package/dist/framework/framework.d.ts +22 -1
  14. package/dist/framework/framework.factory.js +29 -10
  15. package/dist/framework/operation-facade.d.ts +53 -6
  16. package/dist/index.d.ts +2 -1
  17. package/dist/internal.d.ts +1 -0
  18. package/dist/internal.js +1 -0
  19. package/dist/policy/access-provider.interface.d.ts +36 -0
  20. package/dist/policy/access-provider.interface.js +1 -0
  21. package/dist/policy/access-provider.registry.d.ts +9 -0
  22. package/dist/policy/access-provider.registry.js +10 -0
  23. package/dist/policy/policy-startup.validator.js +10 -10
  24. package/dist/prebuilt/core-version.generated.d.ts +1 -1
  25. package/dist/prebuilt/core-version.generated.js +1 -1
  26. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +16 -1
  27. package/dist/protocol/http-response.encoder.js +2 -2
  28. package/dist/runtime/caller-data.copier.js +3 -4
  29. package/dist/runtime/core-class.brand.d.ts +10 -0
  30. package/dist/runtime/core-class.brand.js +21 -0
  31. package/dist/runtime/decimal.d.ts +14 -0
  32. package/dist/runtime/decimal.js +21 -0
  33. package/dist/runtime/diagnostics-reporter.js +13 -1
  34. package/dist/runtime/framework-error.js +9 -0
  35. package/dist/runtime/readonly-value.d.ts +1 -1
  36. package/dist/runtime/readonly-value.js +2 -2
  37. package/dist/runtime/scalar-runtime.js +4 -5
  38. package/dist/transactions/operation-arguments-encoder.js +8 -3
  39. package/dist/transactions/sql-statement.validator.js +2 -2
  40. package/dist/transactions/transaction-integrity-error.js +2 -0
  41. package/dist/transactions/transaction-plan-assembler.d.ts +9 -2
  42. package/dist/transactions/transaction-plan-assembler.js +17 -2
  43. package/dist/transactions/transaction-slot-path-error.js +2 -0
  44. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # @aventara/core
2
2
 
3
+ ## 1.0.0-rc.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 205a379: `createFramework` takes `access`, an access provider: the policy engine and the request state from one object.
8
+ `AvAccessProvider` is the new main-entry type it is written against, with two members:
9
+
10
+ - `policy`: the engine every client and agent request is checked against, as `policy.engine` takes it.
11
+ - `state({ headers, request })`: seeds each request's `ctx.$state`. Its `me` is the principal the engine is asked
12
+ about, `null` for an anonymous caller.
13
+
14
+ Aventara Access provides one, and a hand-written provider works the same way (a till's PIN sign-in, say: see the
15
+ README). The host calls `state` once per request; server code that calls the Framework directly passes the state
16
+ itself. A governed field of the provider's engine is left out of the default projection's type, as with `policy`.
17
+ `access` together with `policy` is refused at startup (`CONFIG_INVALID_VALUE` at `policy`) and is a type error at
18
+ `policy`. A provider whose `policy` is no engine or whose `state` is no function is refused at startup, member by
19
+ member, and the engine's `governed`, `paths` and `afterChecks` are checked against the Core Contract under
20
+ `access.policy`. A prebuilt Contracts file counts the provider by its engine's governed set alone.
21
+
22
+ - a0bb05e: A Framework's `diagnostics` sink receives every report when the process holds more than one copy of `@aventara/core`
23
+ (Next.js giving each layer its own copy, a duplicate install, a monorepo). A protocol failure such as a result with no
24
+ wire form (`500 A3003`), or an MCP endpoint failure, raised through another copy than the one that built the Framework
25
+ now reaches the sink instead of being silently dropped. Copies of one major version share the sinks; nothing in the
26
+ public API or its types changed.
27
+ - 86ab319: Values and errors from `@aventara/core` are recognized across duplicate copies of the package in one process. A
28
+ bundler that gives each layer its own copy (Next.js instrumentation, route handlers and pages), a duplicate install, a
29
+ monorepo, Vite SSR, per-function bundles and test runners' module isolation all load core more than once.
30
+
31
+ - A decimal made by one copy is an `AvDecimal` to every other: `value instanceof AvDecimal` is `true`, the protocol
32
+ encodes it (a `find.many` selecting a decimal no longer answers `500 A3003` "Result serialization failed"), and a
33
+ Framework accepts it in arguments, `$sql` values and Adapter results.
34
+ - `AvError` and its subclasses (`AvValidationError`, `AvNotFoundError`, `AvConflictError`, `AvAuthError`,
35
+ `AvInternalError`, `AvProtocolError`, `AvContractMismatchError`), `AvConstructionError`,
36
+ `AvTransactionIntegrityError` and `AvTransactionSlotPathError` pass `instanceof` across copies, with the same
37
+ subclass rules: every one is an `AvError`, and each is only its own subclass. An `AvError` a pipeline throws with
38
+ another copy's class is answered as the outcome it names. A subclass you declare yourself is checked as before.
39
+ - Copies recognize each other within one major version only. Nothing in the public API or its types changed.
40
+
41
+ - fdf52e6: `framework.serverClient` and `framework.serverClientTx`: the client layer for server code, called with application
42
+ values, such as a Next.js Server Component's.
43
+
44
+ - `framework.serverClient.<Resource>.<family>.<variant>(args, options?)` is typed from the Client Contract's view, as
45
+ `framework.client` is: what the client layer hides is absent. Its arguments are the application forms (`Date`,
46
+ `bigint`, `AvDecimal`, `Uint8Array`), the ones `framework.application` takes, and their wire strings are type errors.
47
+ Its results are `framework.client`'s. At runtime a value already in its wire form is accepted too, as
48
+ `framework.client` reads it.
49
+ - Each call copies its arguments, encodes the copy into the wire forms against the Client Contract, and runs the request
50
+ `framework.client` would run for those wire forms: the same client pipelines, validation, exposure and policy, the
51
+ same answers and the same `AvError` classes. `framework.client` itself is unchanged and keeps the wire forms.
52
+ - `framework.serverClientTx` builds client-origin transaction handles from application values. One
53
+ `framework.transaction([...])` may hold them beside `clientTx` handles, never beside `appTx` ones.
54
+ - New main-entry type names: `AvServerClientFacade`, `AvServerClientResourceFacade`, `AvServerClientFamilyFacade`,
55
+ `AvServerClientProjectingCall`, `AvServerClientPlainCall`, `AvServerClientTxFacade`,
56
+ `AvServerClientTxResourceFacade`, `AvServerClientTxFamilyFacade`, `AvServerClientTxProjectingCall`,
57
+ `AvServerClientTxPlainCall`.
58
+
3
59
  ## 1.0.0-rc.8
4
60
 
5
61
  1.0.0-rc.5, rc.6 and rc.7 were tagged but never published; 1.0.0-rc.8 contains everything listed under them and the changes below.
package/ERRORS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Aventara error codes
2
2
 
3
- Every code `@aventara/core` 1.0.0-rc.8 answers, reports in a validation issue, or refuses to start with. Generated from `AvErrorCatalog` (exported by `@aventara/core`); do not edit.
3
+ Every code `@aventara/core` 1.0.0-rc.9 answers, reports in a validation issue, or refuses to start with. Generated from `AvErrorCatalog` (exported by `@aventara/core`); do not edit.
4
4
 
5
5
  Branch on the code, never on the message. In code, `AvErrorCatalog[code]` gives the same entry: `{ code, kind, message, cause, fix, docs }`.
6
6
 
@@ -134,7 +134,7 @@ Each entry of `cause.issues[]` under `A2001`, `A2002`, `A2004`, `A2007` or `A200
134
134
  | [`COMPILER_INVALID_LIFECYCLE`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_lifecycle) | Invalid field lifecycle. | An added field declares a lifecycle other than VIRTUAL, a VIRTUAL field claims a database-generated lifecycle or a provider-side capability, or a definition redefines a discovered field's lifecycle. | Give an added field lifecycle: ["VIRTUAL"] and only the capabilities it can serve; computed lifecycles come from compute stages. |
135
135
  | [`COMPILER_MISSING_READ_COMPUTE`](https://aventara-framework.vercel.app/docs/error-codes#compiler_missing_read_compute) | A selectable virtual field has no compute.read. | A VIRTUAL field that can be selected has no pipelines.compute.read stage. | Add a compute.read stage, or make the field write-only with capabilities.select: false. |
136
136
  | [`COMPILER_INVALID_COMPUTE_DEPENDENCY`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_compute_dependency) | Invalid compute.read dependency. | A dependsOn entry that is not a stored scalar field of the same Resource. | List only stored scalar fields of the Resource in dependsOn. |
137
- | [`COMPILER_RESERVED_RESOURCE_NAME`](https://aventara-framework.vercel.app/docs/error-codes#compiler_reserved_resource_name) | Reserved Resource name. | A model name starts with $, which is reserved for the framework's own names (ctx.$sql, $ref). | Rename the model; the table it maps to can keep its name. |
137
+ | [`COMPILER_RESERVED_RESOURCE_NAME`](https://aventara-framework.vercel.app/docs/error-codes#compiler_reserved_resource_name) | Reserved Resource name. | A model name starts with $, which is reserved for the framework's own names (ctx.$sql, $ref), or, on a host that adds members beside the Resources, is one of those members' names (on Next.js: tx and transaction on avClient, appTx and transaction on avApp). | Rename the model; the table it maps to can keep its name. A host member's name is also left free when the layer it belongs to hides the Resource. |
138
138
  | [`COMPILER_MISSING_IDENTIFIER`](https://aventara-framework.vercel.app/docs/error-codes#compiler_missing_identifier) | A unique operation has no identifier. | A layer exposes a unique variant (find.unique, update.unique, delete.unique, upsert.unique) and no complete identifier of the Resource: a Pick that did not pick one, or an Omit that hid every one. | Expose an identifier field (id, or every field of one unique), or exempt the unique variants. |
139
139
  | [`COMPILER_HIDDEN_REQUIRED_FIELD`](https://aventara-framework.vercel.app/docs/error-codes#compiler_hidden_required_field) | A layer that creates a Resource hides a required field. | A layer still creates the Resource (a create or upsert variant, or a nested create) while a required field, or a required relation's reference field, is hidden or has its create capability exempted, and no compute.create fills it. | Expose the field, give it a compute.create stage, or exempt the layer's create operations. |
140
140
  | [`COMPILER_INVALID_VALIDATOR`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_validator) | A field validator cannot apply. | A validator that is not built with AvZ, does not fit the field's type (AvZ.maxLength on an int, AvZ.maxItems on a field that is not a list), or sits on a relation or Json field. | Use an AvZ check that fits the field's type; a Json field's checks go in its json shape. |
package/README.md CHANGED
@@ -118,6 +118,27 @@ try {
118
118
  generated client throws the same classes by the same table. `framework.execute` and `framework.executeTransaction`, the
119
119
  entries a transport calls, still answer the envelope.
120
120
 
121
+ ## The client layer, called with your own values
122
+
123
+ `framework.client` reads its arguments in the wire forms a decoded request body carries: a `datetime` is its ISO string,
124
+ a `bigint` its base-10 string. That suits a test that should behave like a remote caller. Server code that acts for a
125
+ remote caller, such as a Server Component or a Server Action, holds `Date`s and `bigint`s instead. It calls
126
+ `framework.serverClient`:
127
+
128
+ ```ts
129
+ const recent = await framework.serverClient.Post.find.many({
130
+ where: { createdAt: { gt: new Date(Date.now() - 86_400_000) } },
131
+ select: ["id", "title"],
132
+ });
133
+ ```
134
+
135
+ `serverClient` has the client layer's types and rules: what the client layer hides is absent, and the client
136
+ pipelines, validation, exposure and policy run. Its arguments are the application forms (`Date`, `bigint`,
137
+ `AvDecimal`, `Uint8Array`), the same ones `framework.application` takes, and its results are `framework.client`'s.
138
+ Each call copies its arguments, encodes them into the wire forms and runs the request `framework.client` would run.
139
+ `framework.serverClientTx` builds client-layer transaction handles the same way; one `framework.transaction([...])`
140
+ may hold them beside `clientTx` handles, never beside `appTx` ones.
141
+
121
142
  ## Every code, explained
122
143
 
123
144
  `AvErrorCatalog` holds every code this package answers, reports in a validation issue, or refuses to start with: its
@@ -199,15 +220,15 @@ findMany(): import("@aventara/core").AvApplicationProjectingCall<
199
220
  "Post", { readonly family: "find"; readonly variant: "many" }>;
200
221
  ```
201
222
 
202
- The names, one set per surface (`application`, `client`, `appTx`, `clientTx`):
223
+ The names, one set per surface (`application`, `client`, `serverClient`, `appTx`, `clientTx`, `serverClientTx`):
203
224
 
204
225
  | Node | `framework.application` | the others |
205
226
  | --- | --- | --- |
206
- | the whole surface | `AvApplicationFacade` | `AvClientFacade`, `AvAppTxFacade`, `AvClientTxFacade` |
227
+ | the whole surface | `AvApplicationFacade` | `AvClientFacade`, `AvServerClientFacade`, `AvAppTxFacade`, `AvClientTxFacade`, `AvServerClientTxFacade` |
207
228
  | one Resource | `AvApplicationResourceFacade` | `AvClientResourceFacade`, … |
208
229
  | one family | `AvApplicationFamilyFacade` | `AvClientFamilyFacade`, … |
209
- | a variant that takes a `select` or `include` | `AvApplicationProjectingCall` | `AvClientProjectingCall`, `AvAppTxProjectingCall`, `AvClientTxProjectingCall` |
210
- | a variant that does not (the `count` variants: `find.count`, `update.count`, …) | `AvApplicationPlainCall` | `AvClientPlainCall`, `AvAppTxPlainCall`, `AvClientTxPlainCall` |
230
+ | a variant that takes a `select` or `include` | `AvApplicationProjectingCall` | `AvClientProjectingCall`, `AvServerClientProjectingCall`, `AvAppTxProjectingCall`, `AvClientTxProjectingCall`, `AvServerClientTxProjectingCall` |
231
+ | a variant that does not (the `count` variants: `find.count`, `update.count`, …) | `AvApplicationPlainCall` | `AvClientPlainCall`, `AvServerClientPlainCall`, `AvAppTxPlainCall`, `AvClientTxPlainCall`, `AvServerClientTxPlainCall` |
211
232
 
212
233
  Each takes the view, `AvContractView`, as its first argument. The view's arguments are your model by its name, the root's
213
234
  and the overlay's exposure as written, and the `fields` entries your configuration's `resources` blocks hold. A
@@ -230,8 +251,8 @@ never with the model the inline stages were typed from.
230
251
 
231
252
  ## Server-side calls copy their input
232
253
 
233
- `framework.application`, `framework.client`, `framework.execute`, `framework.executeTransaction`, and the `appTx` /
234
- `clientTx` builders read what you pass them once, when you call them, and copy it into plain data the framework owns.
254
+ `framework.application`, `framework.client`, `framework.serverClient`, `framework.execute`,
255
+ `framework.executeTransaction`, and the `appTx` / `clientTx` / `serverClientTx` builders read what you pass them once, when you call them, and copy it into plain data the framework owns.
235
256
  Validation, your guards and pipes, computed values and the adapter all see only the copy, and your stages see it
236
257
  frozen: only a pipe changes the arguments, by returning new ones, so what the validators checked is what the adapter
237
258
  writes. In practice:
@@ -374,6 +395,54 @@ caller may change or delete, as `update.many` does. After the call, every row th
374
395
  (the after check, the move-out re-read), and every row they created is read back as stored and asked `can`; one refusal
375
396
  rolls the whole write back. Where `transactionsStyle` is `"none"`, a nested write under a policy is refused.
376
397
 
398
+ ## An access provider
399
+
400
+ `access` takes the policy engine and the request state from one object, an `AvAccessProvider`. Aventara Access
401
+ provides one, and you can write your own. It has two members:
402
+
403
+ - `policy`: the engine, as `policy.engine` takes it.
404
+ - `state({ headers, request })`: seeds each request's `ctx.$state`. Its `me` is the principal the engine is asked
405
+ about (`null` for an anonymous caller).
406
+
407
+ The host calls `state` once per request: `@aventara/nest` with the platform request's headers and the request itself,
408
+ `@aventara/next` with the request's headers (and the route's `Request` on a route call). With `access`, leave out
409
+ `policy` and the host's own `state` option; either one beside it is refused at startup, and as a type error where the
410
+ configuration's type shows both. Server code that calls the Framework directly passes the state itself,
411
+ `{ state: await access.state({ headers }) }`.
412
+
413
+ A point-of-sale till with its own PIN sign-in, written by hand:
414
+
415
+ ```ts
416
+ import type { AvAccessProvider } from "@aventara/core";
417
+
418
+ type Cashier = { id: string; role: "cashier" | "manager" };
419
+
420
+ declare module "@aventara/core" {
421
+ interface AvState {
422
+ me?: Cashier | null;
423
+ }
424
+ }
425
+
426
+ export const tillAccess = {
427
+ // The till sends the PIN the cashier typed as `x-pos-pin`.
428
+ state: async ({ headers }) => ({ me: await cashierByPin(headers.get("x-pos-pin")) }),
429
+ policy: {
430
+ can: ({ principal, resource, action }) => {
431
+ const me = principal as Cashier | null;
432
+ if (me === null) return false; // no PIN, no till
433
+ if (resource === "Refund" && action !== "read") return me.role === "manager";
434
+ return true;
435
+ },
436
+ scopeOf: () => true,
437
+ fieldsOf: () => ({ always: "all", perRow: {} }),
438
+ },
439
+ } satisfies AvAccessProvider;
440
+
441
+ const framework = await createFramework({ api: "/api", adapter, access: tillAccess });
442
+ ```
443
+
444
+ Every stage reads the cashier as `ctx.$state.me`, typed by the `AvState` declaration above.
445
+
377
446
  ## License
378
447
 
379
448
  PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
@@ -66,8 +66,8 @@ export const CONTRACT_COMPILER_DIAGNOSTIC_GUIDANCE = {
66
66
  },
67
67
  COMPILER_RESERVED_RESOURCE_NAME: {
68
68
  message: "Reserved Resource name.",
69
- cause: "A model name starts with $, which is reserved for the framework's own names (ctx.$sql, $ref).",
70
- fix: "Rename the model; the table it maps to can keep its name.",
69
+ cause: "A model name starts with $, which is reserved for the framework's own names (ctx.$sql, $ref), or, on a host that adds members beside the Resources, is one of those members' names (on Next.js: tx and transaction on avClient, appTx and transaction on avApp).",
70
+ fix: "Rename the model; the table it maps to can keep its name. A host member's name is also left free when the layer it belongs to hides the Resource.",
71
71
  },
72
72
  COMPILER_MISSING_IDENTIFIER: {
73
73
  message: "A unique operation has no identifier.",
@@ -11,6 +11,8 @@ import type { AvConfigFieldDefinition } from "./framework-field-definition.js";
11
11
  import { type ResolvedPipelineTree } from "./pipeline-tree.resolver.js";
12
12
  /** The root's `policy`, its engine's passive lists read once. */
13
13
  export type ResolvedPolicyConfig = {
14
+ /** Where the engine was configured, for the startup diagnostics' paths. */
15
+ readonly at: "policy.engine" | "access.policy";
14
16
  readonly engine: AvPolicyEngine;
15
17
  readonly principal: NonNullable<AvConfig["policy"]>["principal"];
16
18
  /** The governed fields and relations, by Resource. */
@@ -158,11 +158,16 @@ function frozenPathLists(value) {
158
158
  Object.freeze(paths.map((path) => Object.freeze([...path]))),
159
159
  ])));
160
160
  }
161
- function resolvePolicy(policy) {
161
+ const accessPrincipal = ({ $state, }) => $state.me ?? null;
162
+ function resolvePolicy(configured, access) {
163
+ const policy = access === undefined
164
+ ? configured
165
+ : { engine: access.policy, principal: accessPrincipal };
162
166
  if (policy === undefined) {
163
167
  return undefined;
164
168
  }
165
169
  return Object.freeze({
170
+ at: access === undefined ? "policy.engine" : "access.policy",
166
171
  engine: policy.engine,
167
172
  principal: policy.principal,
168
173
  governed: frozenLists(policy.engine.governed),
@@ -188,7 +193,7 @@ export function resolveFrameworkConfig(config) {
188
193
  adapter: config.adapter,
189
194
  diagnostics: config.diagnostics,
190
195
  misuse: config.misuse,
191
- policy: resolvePolicy(config.policy),
196
+ policy: resolvePolicy(config.policy, config.access),
192
197
  definitions: definitionsOf(root.resources),
193
198
  trees: {
194
199
  root: root.resources,
@@ -7,6 +7,7 @@ const ROOT_ONLY_PROPERTIES = new Set([
7
7
  "adapter",
8
8
  "prebuilt",
9
9
  "policy",
10
+ "access",
10
11
  "diagnostics",
11
12
  "misuse",
12
13
  "application",
@@ -628,6 +629,12 @@ export function validateFrameworkConfig(value) {
628
629
  if (Object.hasOwn(value, "policy") && value.policy !== undefined) {
629
630
  validatePolicy(value.policy, diagnostics);
630
631
  }
632
+ if (Object.hasOwn(value, "access") && value.access !== undefined) {
633
+ validateAccess(value.access, diagnostics);
634
+ if (Object.hasOwn(value, "policy") && value.policy !== undefined) {
635
+ diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", "policy", "policy cannot be configured with access: access.policy is the policy engine, and ctx.$state.me the principal; remove policy"));
636
+ }
637
+ }
631
638
  if (Object.hasOwn(value, "diagnostics") &&
632
639
  value.diagnostics !== undefined &&
633
640
  typeof value.diagnostics !== "function") {
@@ -670,26 +677,43 @@ function validatePolicy(value, diagnostics) {
670
677
  diagnostics.push(diagnostic("CONFIG_REQUIRED", "policy.engine", "policy.engine is required"));
671
678
  return;
672
679
  }
673
- const engine = value.engine;
680
+ validatePolicyEngine(value.engine, "policy.engine", diagnostics);
681
+ }
682
+ function validateAccess(value, diagnostics) {
683
+ if (value === null || typeof value !== "object") {
684
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "access", "access must be an access provider, { policy, state }"));
685
+ return;
686
+ }
687
+ const provider = value;
688
+ if (typeof provider.state !== "function") {
689
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "access.state", "access.state must be a function"));
690
+ }
691
+ if (provider.policy === undefined) {
692
+ diagnostics.push(diagnostic("CONFIG_REQUIRED", "access.policy", "access.policy is required"));
693
+ return;
694
+ }
695
+ validatePolicyEngine(provider.policy, "access.policy", diagnostics);
696
+ }
697
+ function validatePolicyEngine(engine, at, diagnostics) {
674
698
  if (engine === null || typeof engine !== "object") {
675
- diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "policy.engine", "policy.engine must be an object"));
699
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", at, `${at} must be an object`));
676
700
  return;
677
701
  }
678
702
  const members = engine;
679
703
  for (const question of ["can", "scopeOf", "fieldsOf"]) {
680
704
  if (typeof members[question] !== "function") {
681
- diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `policy.engine.${question}`, `policy.engine.${question} must be a function`));
705
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `${at}.${question}`, `${at}.${question} must be a function`));
682
706
  }
683
707
  }
684
- validatePolicyLists(members.governed, "governed", { all: "string lists", one: "a list of strings" }, isStringList, diagnostics);
685
- validatePolicyLists(members.paths, "paths", {
708
+ validatePolicyLists(members.governed, at, "governed", { all: "string lists", one: "a list of strings" }, isStringList, diagnostics);
709
+ validatePolicyLists(members.paths, at, "paths", {
686
710
  all: "path lists",
687
711
  one: "a list of paths, each a list of strings",
688
712
  }, (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);
713
+ validatePolicyLists(members.afterChecks, at, "afterChecks", { all: "action lists", one: "a list of actions" }, isStringList, diagnostics);
714
+ validateAfterCheckActions(members.afterChecks, at, diagnostics);
691
715
  }
692
- function validateAfterCheckActions(lists, diagnostics) {
716
+ function validateAfterCheckActions(lists, at, diagnostics) {
693
717
  if (!isPlainRecord(lists)) {
694
718
  return;
695
719
  }
@@ -699,7 +723,7 @@ function validateAfterCheckActions(lists, diagnostics) {
699
723
  }
700
724
  for (const action of actions) {
701
725
  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`));
726
+ diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", `${at}.afterChecks.${resource}`, `${at}.afterChecks.${resource}: "${action}" is no action an after check runs on; only "update" is`));
703
727
  }
704
728
  }
705
729
  }
@@ -707,17 +731,17 @@ function validateAfterCheckActions(lists, diagnostics) {
707
731
  function isStringList(value) {
708
732
  return (Array.isArray(value) && value.every((name) => typeof name === "string"));
709
733
  }
710
- function validatePolicyLists(lists, list, entry, isEntry, diagnostics) {
734
+ function validatePolicyLists(lists, at, list, entry, isEntry, diagnostics) {
711
735
  if (lists === undefined) {
712
736
  return;
713
737
  }
714
738
  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`));
739
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `${at}.${list}`, `${at}.${list} must be an object of ${entry.all} when provided`));
716
740
  return;
717
741
  }
718
742
  for (const [key, value] of Object.entries(lists)) {
719
743
  if (!isEntry(value)) {
720
- diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `policy.engine.${list}.${key}`, `policy.engine.${list}.${key} must be ${entry.one}`));
744
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `${at}.${list}.${key}`, `${at}.${list}.${key} must be ${entry.one}`));
721
745
  }
722
746
  }
723
747
  }
@@ -243,6 +243,7 @@ type RootSchema<M extends AvAdapterModel, Cfg> = LayerKeysSchema & {
243
243
  readonly engine: Leaf;
244
244
  readonly principal: Leaf;
245
245
  };
246
+ readonly access: Leaf;
246
247
  readonly diagnostics: Leaf;
247
248
  readonly misuse: Leaf;
248
249
  readonly resources: RootResourcesSchema<M>;
@@ -264,7 +265,16 @@ type Exact<S, V> = [S] extends [Leaf] ? unknown : V extends readonly unknown[] ?
264
265
  * rc.0 key, a `definition` in an overlay, a stage a relation field does not take,
265
266
  * or a name the model and the root do not have.
266
267
  */
267
- export type AvConfigExact<M extends AvAdapterModel, Cfg> = Exact<RootSchema<M, Cfg>, Cfg> & ExposedWithExemptions<Cfg, "application"> & ExposedWithExemptions<Cfg, "client"> & ExposedWithExemptions<Cfg, "agent"> & ForceNeedsTransactions<M, Cfg>;
268
+ export type AvConfigExact<M extends AvAdapterModel, Cfg> = Exact<RootSchema<M, Cfg>, Cfg> & ExposedWithExemptions<Cfg, "application"> & ExposedWithExemptions<Cfg, "client"> & ExposedWithExemptions<Cfg, "agent"> & ForceNeedsTransactions<M, Cfg> & AccessWithoutPolicy<Cfg>;
269
+ /** `policy` refused when the configuration also states `access`. */
270
+ type AccessWithoutPolicy<Cfg> = Cfg extends {
271
+ readonly access: object;
272
+ readonly policy: object;
273
+ } ? {
274
+ readonly policy: readonly [
275
+ "Error: `access` brings the policy engine; remove `policy`"
276
+ ];
277
+ } : unknown;
268
278
  /** A layer's own literal `transactionsStyle`, `undefined` when it states none. */
269
279
  type StyleOf<Layer> = Layer extends {
270
280
  readonly transactionsStyle: infer Style;
@@ -2,6 +2,7 @@ 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 { AvAccessProvider } from "../policy/access-provider.interface.js";
5
6
  import type { AvPolicyEngine } from "../policy/policy-engine.js";
6
7
  import type { AvPrebuiltContracts } from "../prebuilt/prebuilt-contracts.js";
7
8
  import type { AvDiagnosticsSink } from "../runtime/diagnostics-reporter.js";
@@ -100,6 +101,14 @@ export type AvConfig<M extends AvAdapterModel = AvAdapterModel, Cfg = AvConfigAn
100
101
  readonly $origin: AvRequestOrigin;
101
102
  }) => unknown;
102
103
  };
104
+ /**
105
+ * The access provider (Aventara Access, or one written by hand): its
106
+ * `policy` engine checks every client and agent request, with
107
+ * `ctx.$state.me` as the principal, and the host seeds each request's
108
+ * `ctx.$state` with its `state`. Not together with `policy`, nor with a
109
+ * host's own `state` option.
110
+ */
111
+ readonly access?: AvAccessProvider;
103
112
  readonly diagnostics?: AvDiagnosticsSink;
104
113
  /** Told about every caller value refused as unreadable or not plain data. */
105
114
  readonly misuse?: AvMisuseSink;
@@ -1,3 +1,4 @@
1
+ import { brandCoreClass } from "../runtime/core-class.brand.js";
1
2
  import { FILTER_MODES, PROJECTION_SCOPES, WRITE_SCOPES } from "./capability.js";
2
3
  import { LIFECYCLE_TRAITS } from "./lifecycle-trait.js";
3
4
  import { projectionScopeOf, writeScopeOf } from "./resource-operations.js";
@@ -5,6 +6,7 @@ const PAGINATION_ORDER = ["limit", "offset", "cursor"];
5
6
  const REDUCER_ORDER = ["$count"];
6
7
  export class AvCanonicalJsonError extends TypeError {
7
8
  }
9
+ brandCoreClass(AvCanonicalJsonError, "AvCanonicalJsonError");
8
10
  function compareCodeUnits(left, right) {
9
11
  if (left === right) {
10
12
  return 0;
@@ -104,8 +104,16 @@ type PolicyEngineOf<Cfg> = (NonNullable<(Cfg & {
104
104
  })["policy"]> & {
105
105
  readonly engine?: unknown;
106
106
  })["engine"];
107
+ /** The configuration's `access.policy`, read by indexed access. */
108
+ type AccessEngineOf<Cfg> = ((Cfg & {
109
+ readonly access?: {
110
+ readonly policy?: unknown;
111
+ };
112
+ })["access"] & {
113
+ readonly policy?: unknown;
114
+ })["policy"];
107
115
  /** The configuration's policy engine's `governed` set, when its type states one. */
108
- type GovernedBlock<Cfg> = LiteralRecord<(NonNullable<PolicyEngineOf<Cfg>> & {
116
+ type GovernedBlock<Cfg> = LiteralRecord<(NonNullable<PolicyEngineOf<Cfg>> & AccessEngineOf<Cfg> & {
109
117
  readonly governed?: unknown;
110
118
  })["governed"]>;
111
119
  /** The names `Governed` lists for Resource `R`, or `never`. */
@@ -1,3 +1,4 @@
1
+ import { brandCoreClass } from "../runtime/core-class.brand.js";
1
2
  import { errorDocsUrl } from "../runtime/error.catalog.js";
2
3
  const FRAMEWORK_CONSTRUCTION_MESSAGES = {
3
4
  configuration: "Aventara framework configuration validation failed",
@@ -27,3 +28,4 @@ export class AvConstructionError extends Error {
27
28
  }));
28
29
  }
29
30
  }
31
+ brandCoreClass(AvConstructionError, "AvConstructionError");
@@ -7,7 +7,7 @@ import type { AvOperationResponse } from "../runtime/operation-response.js";
7
7
  import type { AvTransactionRunner } from "../transactions/deferred-operation.js";
8
8
  import type { AvTransactionRequest } from "../transactions/transaction-request.js";
9
9
  import type { AvTransactionResponse } from "../transactions/transaction-response.js";
10
- import type { ApplicationOperations, ApplicationTransactionBuilders, ClientOperations, ClientTransactionBuilders } from "./operation-facade.js";
10
+ import type { ApplicationOperations, ApplicationTransactionBuilders, ClientOperations, ClientTransactionBuilders, ServerClientOperations, ServerClientTransactionBuilders } from "./operation-facade.js";
11
11
  /**
12
12
  * One Aventara instance, as returned by {@link createFramework}: its compiled
13
13
  * Contracts, typed in-process operations, transaction builders and the
@@ -50,6 +50,19 @@ export type AvFramework<M extends AvAdapterModel = AvAdapterModel, Cfg = AvConfi
50
50
  * failure thrown.
51
51
  */
52
52
  readonly client: ClientOperations<M, Cfg>;
53
+ /**
54
+ * The client layer for server code, taking application values:
55
+ * `framework.serverClient.<resource>.<family>.<variant>(args, options?)`.
56
+ *
57
+ * Typed and run exactly as {@link client} (the Client Contract's view, the
58
+ * client pipelines, validation, exposure and policy), but its arguments are
59
+ * the application forms: a `datetime` is a `Date`, a `bigint` a `bigint`, a
60
+ * `decimal` an `AvDecimal`, `bytes` a `Uint8Array`. Each call copies its
61
+ * arguments, encodes them into the wire forms `client` reads, and hands the
62
+ * same client-origin request to {@link execute}. Results are the same as
63
+ * `client`'s; the optional second argument is {@link AvExecutionOptions}.
64
+ */
65
+ readonly serverClient: ServerClientOperations<M, Cfg>;
53
66
  /**
54
67
  * Transaction builders under application rules:
55
68
  * `framework.appTx.<resource>.<family>.<variant>(args)` defers one operation
@@ -72,6 +85,14 @@ export type AvFramework<M extends AvAdapterModel = AvAdapterModel, Cfg = AvConfi
72
85
  * share one (`A2004`/`V1020` at the first step of the other origin).
73
86
  */
74
87
  readonly clientTx: ClientTransactionBuilders<M, Cfg>;
88
+ /**
89
+ * The {@link clientTx} builders taking application values, as
90
+ * {@link serverClient} takes them: each handle's arguments are copied and
91
+ * encoded into the wire forms when it is built. Its handles are client-origin,
92
+ * so one {@link transaction} may hold them beside `clientTx` handles, never
93
+ * beside `appTx` ones.
94
+ */
95
+ readonly serverClientTx: ServerClientTransactionBuilders<M, Cfg>;
75
96
  execute(request: AvOperationRequest): Promise<AvOperationResponse>;
76
97
  /**
77
98
  * Validates a whole transaction plan, then runs its operations one after
@@ -4,10 +4,11 @@ import { resolveFrameworkConfig, } from "../config/config.resolver.js";
4
4
  import { validateFrameworkConfig } from "../config/config.validator.js";
5
5
  import { validateForcedOperations } from "../config/forced-operation.validator.js";
6
6
  import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
7
+ import { recordAccessProvider } from "../policy/access-provider.registry.js";
7
8
  import { callerOf } from "../policy/policy-request.js";
8
9
  import { policyStartupDiagnostics } from "../policy/policy-startup.validator.js";
9
10
  import { loadPrebuiltContracts, warnStalePrebuilt, } from "../prebuilt/prebuilt-contracts.loader.js";
10
- import { copyEnvelopeAtDoor, refuseCallerData, } from "../runtime/caller-data.door.js";
11
+ import { copyAtDoor, copyEnvelopeAtDoor, refuseCallerData, } from "../runtime/caller-data.door.js";
11
12
  import { createDiagnosticsReporter, createTransactionDiagnosticsChannel, recordDiagnosticsSink, } from "../runtime/diagnostics-reporter.js";
12
13
  import { createMisuseReporter, } from "../runtime/misuse-reporter.js";
13
14
  import { internalFailure, operationFailure, } from "../runtime/operation-response.js";
@@ -16,6 +17,7 @@ import { recordOwnedExecution } from "../runtime/owned-execution.registry.js";
16
17
  import { validateOperationEnvelope, validateTransactionEnvelope, } from "../runtime/request-envelope.validator.js";
17
18
  import { requestIdOf } from "../runtime/request-id.js";
18
19
  import { stateOptionOf } from "../runtime/request-state.resolver.js";
20
+ import { encodeOperationArgumentsForWire } from "../transactions/operation-arguments-encoder.js";
19
21
  import { createTransactionPlanAssembler } from "../transactions/transaction-plan-assembler.js";
20
22
  import { createTransactionRuntime } from "../transactions/transaction-runtime.js";
21
23
  import { AvConstructionError } from "./framework-construction.error.js";
@@ -68,11 +70,21 @@ export async function createFramework(config) {
68
70
  const executeTransaction = createTransactionRuntime(runtimeDependencies, pipelineDependencies);
69
71
  const misuse = createMisuseReporter(resolvedConfig.misuse);
70
72
  const executeOperation = (request, caller) => executeTotally(execute, request, resolvedConfig.diagnostics, "caller", caller);
73
+ const executeApplicationValues = (request, caller) => executeTotally(encodingApplicationValues(execute, compilation.contracts.client), request, resolvedConfig.diagnostics, "caller", caller);
71
74
  const protocol = {
72
75
  version: AVENTARA_PROTOCOL_VERSION,
73
76
  hash: compilation.contracts.client.protocol.hash,
74
77
  };
75
78
  const assembler = createTransactionPlanAssembler(compilation.contracts, protocol, misuse);
79
+ const clientRequest = (resource, identity, args, requestId, state) => ({
80
+ ...identity,
81
+ origin: "client",
82
+ protocol,
83
+ requestId,
84
+ ...(state === undefined ? {} : { state }),
85
+ resource,
86
+ arguments: args,
87
+ });
76
88
  const framework = {
77
89
  api: resolvedConfig.api,
78
90
  mcp: resolvedConfig.mcp,
@@ -85,25 +97,20 @@ export async function createFramework(config) {
85
97
  resource,
86
98
  arguments: args,
87
99
  }), executeOperation),
88
- client: createOperationFacade(model, (resource, identity, args, requestId, state) => ({
89
- ...identity,
90
- origin: "client",
91
- protocol,
92
- requestId,
93
- ...(state === undefined ? {} : { state }),
94
- resource,
95
- arguments: args,
96
- }), executeOperation),
100
+ client: createOperationFacade(model, clientRequest, executeOperation),
101
+ serverClient: createOperationFacade(model, clientRequest, executeApplicationValues),
97
102
  appTx: Object.freeze({
98
103
  ...createOperationTree(model, (resource, identity) => (args) => assembler.defer("application", resource, identity, args === undefined ? {} : args)),
99
104
  [APPLICATION_SQL_BUILDER]: (strings, ...values) => assembler.deferSql(strings, values),
100
105
  }),
101
106
  clientTx: createOperationTree(model, (resource, identity) => (args) => assembler.defer("client", resource, identity, args === undefined ? {} : args)),
107
+ serverClientTx: createOperationTree(model, (resource, identity) => (args) => assembler.defer("client", resource, identity, args === undefined ? {} : args, "application")),
102
108
  execute: (request) => executeRequestTotally(execute, request, resolvedConfig.diagnostics, misuse),
103
109
  executeTransaction: (request) => executeTransactionRequestTotally(executeTransaction, request, resolvedConfig.diagnostics, misuse),
104
110
  transaction: (async (operations, options) => settleOperation(await runTransactionTotally(executeTransaction, assembler, operations, requestIdOf(options), stateOptionOf(options), resolvedConfig.diagnostics, callerOf(options)))),
105
111
  };
106
112
  recordDiagnosticsSink(framework, resolvedConfig.diagnostics);
113
+ recordAccessProvider(framework, config.access);
107
114
  recordOwnedExecution(framework, {
108
115
  execute: (request, refused) => executeTotally(execute, request, resolvedConfig.diagnostics, refused === undefined ? "owned" : { refused }),
109
116
  executeTransaction: (request, refused) => executeTransactionTotally(executeTransaction, request, resolvedConfig.diagnostics, undefined, refused === undefined
@@ -129,6 +136,18 @@ async function prebuiltCompilation(prebuilt, model, config, resolved) {
129
136
  }
130
137
  return { ok: true, contracts: loaded.contracts };
131
138
  }
139
+ function encodingApplicationValues(execute, contract) {
140
+ return (request, report, _source, caller) => {
141
+ const copy = copyAtDoor(request.arguments);
142
+ if (!copy.ok) {
143
+ return execute(request, report, { refused: copy.error }, caller);
144
+ }
145
+ return execute({
146
+ ...request,
147
+ arguments: encodeOperationArgumentsForWire(contract, request, copy.value),
148
+ }, report, "owned", caller);
149
+ };
150
+ }
132
151
  async function executeTotally(execute, request, diagnostics, source, caller) {
133
152
  const report = createDiagnosticsReporter(diagnostics, request);
134
153
  try {