@aventara/core 1.0.0-rc.3 → 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 (73) hide show
  1. package/CHANGELOG.md +130 -1
  2. package/ERRORS.md +6 -6
  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 +5 -0
  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/operation-arguments.d.ts +1 -1
  25. package/dist/operations/operation-call.d.ts +9 -5
  26. package/dist/operations/operation-result.d.ts +5 -3
  27. package/dist/operations/operation-validator-core.js +6 -0
  28. package/dist/policy/policy-admission.d.ts +22 -0
  29. package/dist/policy/policy-admission.js +400 -0
  30. package/dist/policy/policy-after-update.runtime.d.ts +29 -0
  31. package/dist/policy/policy-after-update.runtime.js +131 -0
  32. package/dist/policy/policy-condition.d.ts +15 -0
  33. package/dist/policy/policy-condition.js +36 -0
  34. package/dist/policy/policy-engine.d.ts +111 -0
  35. package/dist/policy/policy-engine.js +1 -0
  36. package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
  37. package/dist/policy/policy-nested-write.runtime.js +449 -0
  38. package/dist/policy/policy-read.runtime.d.ts +83 -0
  39. package/dist/policy/policy-read.runtime.js +494 -0
  40. package/dist/policy/policy-request.d.ts +37 -0
  41. package/dist/policy/policy-request.js +52 -0
  42. package/dist/policy/policy-session.d.ts +83 -0
  43. package/dist/policy/policy-session.js +234 -0
  44. package/dist/policy/policy-startup.validator.d.ts +8 -0
  45. package/dist/policy/policy-startup.validator.js +106 -0
  46. package/dist/policy/policy-write.runtime.d.ts +30 -0
  47. package/dist/policy/policy-write.runtime.js +394 -0
  48. package/dist/prebuilt/core-version.generated.d.ts +2 -0
  49. package/dist/prebuilt/core-version.generated.js +1 -0
  50. package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
  51. package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
  52. package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
  53. package/dist/prebuilt/prebuilt-contracts.js +1 -0
  54. package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
  55. package/dist/prebuilt/prebuilt-contracts.loader.js +122 -0
  56. package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
  57. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
  58. package/dist/runtime/computed-read-runtime.d.ts +6 -1
  59. package/dist/runtime/computed-read-runtime.js +224 -45
  60. package/dist/runtime/context-executor.runtime.js +4 -2
  61. package/dist/runtime/execution-options.d.ts +8 -0
  62. package/dist/runtime/framework-warning.emitter.d.ts +6 -0
  63. package/dist/runtime/framework-warning.emitter.js +8 -0
  64. package/dist/runtime/level-lifecycle.runtime.js +3 -2
  65. package/dist/runtime/operation-code.guidance.js +3 -3
  66. package/dist/runtime/operation-request.d.ts +7 -0
  67. package/dist/runtime/operation-runtime.d.ts +9 -1
  68. package/dist/runtime/operation-runtime.js +114 -10
  69. package/dist/runtime/readonly-value.d.ts +5 -1
  70. package/dist/runtime/readonly-value.js +67 -28
  71. package/dist/transactions/transaction-runtime.d.ts +4 -1
  72. package/dist/transactions/transaction-runtime.js +27 -5
  73. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,134 @@
1
1
  # @aventara/core
2
2
 
3
+ ## 1.0.0-rc.4
4
+
5
+ ### Patch Changes
6
+
7
+ - e3d03a7: The error catalog says more about policies. `A2003` now names the cases a policy adds: a unique read of a row outside the
8
+ caller's read scope, and a single-row update or delete of a row the caller cannot read, are misses, while a row the
9
+ caller can read but not change answers `A4002`. `CONFIG_REQUIRED`, `CONFIG_UNKNOWN_PROPERTY` and `CONFIG_INVALID_VALUE`
10
+ name the `policy` key's rules: `policy.engine` and `policy.principal` are required, `policy` and `prebuilt` are root-only,
11
+ and the engine's `governed`, `paths` and `afterChecks` lists must name what the Core Contract has. Wording only; no
12
+ behaviour changes.
13
+ - d10639a: Faster reads. The read-only copies stages are told cost about half what they did: a read with a computed field
14
+ copies each row in one pass, a row with several computed fields shares its nested objects between the stages'
15
+ copies, and after hooks share one frozen copy of the result. A row whose computed field returns a value at once no
16
+ longer waits a turn per row. The MCP endpoint builds its JSON Schema validator once instead of on every request.
17
+ What stages are told, the order they run in and what a request answers are unchanged.
18
+ - f67e68c: A service method can return any façade node or variant without a return type. Under `declaration: true` (a Nest
19
+ build) the declaration file now writes a short reference, under half a kilobyte per method at any schema size, where it
20
+ wrote the whole Contract view (about 440 KB per method over 50 Resources). A method returning a variant
21
+ (`this.framework.application.Post.find.many`) no longer needs an annotation to compile, and one returning a transaction
22
+ variant (`this.framework.appTx.Post.create.one`) no longer runs the compiler out of memory.
23
+
24
+ New type names, exported from `@aventara/core` so a declaration can write them (you never need to write them yourself):
25
+
26
+ - `AvContractView`, the Contract view the façades are typed over;
27
+ - one call type per surface and shape: `AvApplicationProjectingCall`, `AvApplicationPlainCall`,
28
+ `AvClientProjectingCall`, `AvClientPlainCall`, `AvAppTxProjectingCall`, `AvAppTxPlainCall`,
29
+ `AvClientTxProjectingCall`, `AvClientTxPlainCall`.
30
+
31
+ A configuration with `fields` entries still writes those entries after each reference; annotate the method if that
32
+ matters (see the README's "Returning a façade from a method").
33
+
34
+ - 708556c: Policy engines check nested writes. A create, update or upsert whose data writes a relation through a directive
35
+ (`$create`, `$connect`, `$connectOrCreate`, `$update`, `$upsert`, `$delete`, `$disconnect`, `$set`) now runs under a
36
+ policy, where before it was refused. It runs as one adapter call inside the write's interactive transaction, and one
37
+ failed check rolls the whole write back with `403 A4002`.
38
+
39
+ - Every row the nested write creates is read back as stored (its parent's reference filled, database defaults
40
+ present) and asked `can({ action: "create", row })`, its `exists` lookups seeing the rows the same call wrote. A
41
+ create whose own data holds a nested write is asked about its row the same way, after the insert.
42
+ - A row a directive links or unlinks is held to the scope of the row that changes: a linked row that holds the
43
+ reference (a to-many relation's rows, the back end of a one-to-one, a many-to-many relation's rows) must be in its
44
+ update scope, one whose parent holds the reference (a to-one relation's target) in its read scope. One `exists`
45
+ per Resource answers it, inside the transaction. A linked row that changes is an update: after the call it gets
46
+ the after check (`can({ action: "update", row })`) and the move-out re-read. Linking a new row to a one-to-one's
47
+ back end unlinks the old one, which is held the same way.
48
+ - A to-many nested `$update` or `$delete` acts only on the rows within the update (or delete) scope, as
49
+ `update.many` and `delete.many` do: core reads those rows' identifiers inside the transaction and hands the
50
+ adapter `where: { id: { in: [...] } }`. A to-one one refuses the write when its row is outside the scope. A nested
51
+ `$update`'s changed rows get the after check and the move-out re-read after the call.
52
+ - The fields a nested record sends are held to its Resource's `fieldsOf({ access: "write" })`, and a relation written
53
+ through a directive to the parent's grant, its reference fields with it.
54
+ - Where the operation's `transactionsStyle` is `"none"`, a nested write under a policy is refused before it runs.
55
+
56
+ - 3a0dfca: A write the policy checks costs fewer database round trips. What each check decides and refuses is unchanged.
57
+
58
+ - A single-row update or delete carries the caller's scopes inside its own `where` and runs as one statement, with no
59
+ read of its row before it. One that matches nothing still answers `A4002` when the caller can read the row and
60
+ `A2003` when it cannot, with one lookup on that miss only. The Prisma adapter sends it as
61
+ `update({ where: { id, AND: [scope] } })`.
62
+ - A create asks `can({ action: "create", row })` and its `exists` lookups before the insert, without a transaction. A
63
+ transaction around them never closed the race between the lookup and the insert at the database's default
64
+ isolation, and cost a BEGIN and a COMMIT.
65
+ - A transaction opens only when a check after the write may roll it back: an update's after check or move-out, a
66
+ created row held to a write grant, a nested write, or returned rows checked by `exists`. A plain checked create or
67
+ delete is one statement, its interceptors and hooks outside any transaction.
68
+ - The after check asks `can` about the row the update returned, so it reads nothing again. Only a move-out reads the
69
+ changed rows again. An `update.count` runs as the returning `update.many` when the Contract offers it.
70
+
71
+ Round trips per request on PostgreSQL: `create.one` 3 → 1, `delete.unique` 4 → 1, `update.unique` 5 → 3,
72
+ `update.many` 4 → 3, `update.count` 5 → 3. Reads, and a Framework without a policy, are unchanged.
73
+
74
+ - 99169d2: Policy engines, reads first. `createFramework` takes an optional root `policy: { engine, principal }`: an engine that
75
+ answers three questions (`can`, `scopeOf`, `fieldsOf`, each now or as a promise), and a function that derives a
76
+ request's principal from its `ctx.$state`. Core never imports an engine; one satisfies `AvPolicyEngine` by its shape,
77
+ and a scope or grant is a fragment of Aventara's own `where` (`AvPolicyWhere`).
78
+
79
+ - Client and agent requests are checked (an MCP tool call is an agent request). An application call is checked only
80
+ when it names a principal: `framework.application.Note.find.many(args, { as: user })`. A call a stage makes through
81
+ `ctx` never is.
82
+ - Before any guard, `can` and the fields the request names or receives (`fieldsOf`); a refusal is `403 A4002`. At the
83
+ adapter call, after every developer stage, the read scope is ANDed into the root `where`, every to-many include and
84
+ its `$count`, and every relation filter, at every level. A to-one include of a row out of scope refuses the request;
85
+ it is never nulled.
86
+ - Governed fields (the engine's `governed` list) are left out of every default projection and carry `governed: true`
87
+ in the Contracts. Name one to get it: `select: ["$defaults", "email"]`, where `$defaults` is the level's default
88
+ projection. It is refused where a returned row does not grant it, and a filter or sort by it is checked before the
89
+ query runs, so it cannot reveal values the caller may not read.
90
+ - The engine's `governed` and `paths` lists are checked against the Core Contract at startup. A rule's paths are lists
91
+ of segments, as other paths in core are: `["Note", "campaign", "members", "some", "userId", "equals"]`.
92
+ - Writes are checked as well; see the policy writes entry.
93
+
94
+ Without `policy`, nothing changes: the Contracts and their hashes are the same bytes.
95
+
96
+ - 0fd2608: A write the policy checks costs less CPU after the round-trip changes. The operation core hands its adapter no longer
97
+ carries the request's policy session, so an adapter that keeps its operations no longer keeps each request's session
98
+ with them. The fields a write returns only for the after check are dropped as soon as the check has read them, so a
99
+ read compute stage is told the row the request asked for, not every scalar field. What each check decides, the
100
+ fields `can` is asked about, and the database round trips are unchanged.
101
+ - 154e440: Policy engines check writes. Every create, update, delete and upsert a client or agent makes (or an application call
102
+ made `{ as }` a principal) is now checked by the configuration's `policy`, where before it was refused.
103
+
104
+ - The fields a write sends are held to `fieldsOf({ access: "write" })` before any guard; a field outside it answers
105
+ `403 A4002`. A field writable on some rows only is checked against every row an update reaches, and against the
106
+ row a create inserts, after the insert and inside its transaction.
107
+ - A create asks `can({ action: "create", row })` about each new row on its final data. Its rules can look rows up
108
+ through `exists`, which runs inside the write's transaction, so a plan that creates a campaign and then its first
109
+ note sees the campaign.
110
+ - An update or delete acts only on rows the caller may both change (`scopeOf` for the action) and read. A single-row
111
+ one that matches nothing answers `A4002` when the caller can read the row, and `A2003` when it cannot, so the
112
+ answer does not reveal that the row exists. A `many` or `count` one answers for the caller's rows only.
113
+ - An update that moves a row out of what the caller reads is rolled back with `A4002`. So is one that fails an `after`
114
+ condition: inside the transaction core asks `can({ action: "update", row })` about each changed row of every
115
+ checked update. The engine's new `afterChecks` member (`{ Note: ["update"] }`) names the updates whose grants carry
116
+ such a condition, which are the ones refused where there is no interactive transaction.
117
+ - Where the operation's `transactionsStyle` is `"interactive"`, a checked write runs in a transaction of its own. Where
118
+ it is `"none"` (Cloudflare D1), a write that can only be checked after it runs is refused before it runs; every other
119
+ write runs, a create looking up before it inserts, and an unlisted update without the after check.
120
+ - Nested writes (a relation directive in a write's data) are refused under a policy for now.
121
+
122
+ `afterChecks` is checked against the Core Contract at startup and changes no Contract and no prebuilt fingerprint.
123
+
124
+ - 2fcffdb: Prebuilt Contracts. `createFramework` takes an optional `prebuilt` key: the four Contracts compiled ahead of time (type
125
+ `AvPrebuiltContracts`), so a serverless or edge cold start loads them instead of compiling them. A file is used only
126
+ when this core compiled it, for this protocol, from the same configuration and adapter model; its Contracts, hashes
127
+ and `/_contract` bytes are then the ones compiling would give. Any other file is ignored whole: the framework compiles
128
+ at startup exactly as without one and warns once (`AVENTARA_PREBUILT_STALE`), naming the Resources whose configuration
129
+ or model changed, or the version that differs. Without `prebuilt`, nothing changes. The build step that writes the
130
+ file arrives with `@aventara/build`.
131
+
3
132
  ## 1.0.0-rc.3
4
133
 
5
134
  ### Patch Changes
@@ -27,7 +156,7 @@
27
156
  on an `AvComputedValueError`, with nothing of the hidden field in the answer. Before, a shaped Json field hidden from
28
157
  the layer was not judged at all, and a hidden field's own validators answered the caller `A2004` with an issue at the
29
158
  hidden field's path. A layer that shows the field still answers `A2004` at it.
30
- - b38034d: On PostgreSQL, a `@unique` value too long for its index (an index row past the B-tree's 8191 bytes, SQLSTATE
159
+ - b38034d: On PostgreSQL, a `@unique` value too long for its index (an index row past the B-tree's limit of about 2704 bytes, SQLSTATE
31
160
  `54000`) is answered `422 A2004`, not `500 A3001`: one `V1004` issue at `["arguments"]`, as a value too long for its
32
161
  column already was. The adapter reads the SQLSTATE, never the message. The issue's message for a value too long now
33
162
  reads `The database refused this value: it is longer than the database can hold for its column.`
package/ERRORS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Aventara error codes
2
2
 
3
- Every code `@aventara/core` 1.0.0-rc.3 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.4 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
 
@@ -30,7 +30,7 @@ The caller can fix these. The generated client and server-side calls throw the c
30
30
  | [`A2000`](https://aventara-framework.vercel.app/docs/error-codes#a2000) | Invalid request. | The HTTP request cannot be interpreted: malformed JSON, a body that is not a JSON object, an unknown `/_*` path, a body that does not decode in its Content-Encoding, or a missing Aventara-Protocol-Version or Aventara-Contract-Hash header (the message names it). | Send a JSON object with both identity headers. The generated client does this for you. |
31
31
  | [`A2001`](https://aventara-framework.vercel.app/docs/error-codes#a2001) | Unknown Resource. | The Resource is not in the caller's contract: misspelt, hidden by exposure, or added to the schema after aventara:prepare last ran. cause.message is "The requested Resource is not available." and the issue is V1005 at `["resource"]`. | Use the model's exact name. After a schema change run the adapter's generate step (`npm run aventara:prepare` in a scaffolded project), restart, and regenerate the client. A Resource hidden on purpose is reached from server code (framework.application). |
32
32
  | [`A2002`](https://aventara-framework.vercel.app/docs/error-codes#a2002) | Unknown operation. | The operation is not available on that Resource: a typo, removed by exposure, an operation the adapter does not offer (such as delete.many), or a ctx.$sql, appTx.$sql or ctx.$transaction the adapter or the layer cannot run. The issue is V1006 at `["family"]` or `["variant"]`. | Call an operation the contract lists (`GET <api>/_contract`), regenerate a stale client, and check the layer's exemptions and transactionsStyle. |
33
- | [`A2003`](https://aventara-framework.vercel.app/docs/error-codes#a2003) | Not found. | 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. | Catch AvNotFoundError, or use a first-style operation, which answers A1001 with null. Check the $connect identifier. |
33
+ | [`A2003`](https://aventara-framework.vercel.app/docs/error-codes#a2003) | Not found. | 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. | Catch AvNotFoundError, or use a first-style operation, which answers A1001 with null. Check the $connect identifier. |
34
34
  | [`A2004`](https://aventara-framework.vercel.app/docs/error-codes#a2004) | Validation failed. | 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. | 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. |
35
35
  | [`A2005`](https://aventara-framework.vercel.app/docs/error-codes#a2005) | Contract mismatch. | The caller's Aventara-Contract-Hash is not the server's current one: the schema, the configuration, the limits or the exposure changed, or the client targets another deployment. | Regenerate the client against the running server: `npm run avclient:generate`. |
36
36
  | [`A2006`](https://aventara-framework.vercel.app/docs/error-codes#a2006) | Protocol mismatch. | Aventara-Protocol-Version is not 1. | Send `Aventara-Protocol-Version: 1`, or upgrade the client. |
@@ -63,7 +63,7 @@ Produced by your guards; the framework never authenticates by itself.
63
63
  |---|---|---|---|
64
64
  | [`A4000`](https://aventara-framework.vercel.app/docs/error-codes#a4000) | Unauthenticated. | A guard threw new AvError("A4000", ...): authentication is required or failed. The framework never authenticates by itself. | Send the credentials your guards expect, such as the Authorization header. |
65
65
  | [`A4001`](https://aventara-framework.vercel.app/docs/error-codes#a4001) | Forbidden. | A guard returned false ("Operation denied by a framework guard."), a field write guard or a nested write's target guard included, or a stage threw A4001. | Call as a caller the guard admits, or stop sending the guarded field. To answer another code, throw an AvError from the guard. |
66
- | [`A4002`](https://aventara-framework.vercel.app/docs/error-codes#a4002) | Policy denied. | A guard or hook threw new AvError("A4002", ...) with a policy reason of your own. | Read cause.message for the policy's reason. |
66
+ | [`A4002`](https://aventara-framework.vercel.app/docs/error-codes#a4002) | Policy denied. | 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", ...). | 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. |
67
67
 
68
68
  ## Validation issues (V1xxx)
69
69
 
@@ -100,10 +100,10 @@ Each entry of `cause.issues[]` under `A2001`, `A2002`, `A2004`, `A2007` or `A200
100
100
 
101
101
  | Code | Meaning | Cause | Fix |
102
102
  |---|---|---|---|
103
- | [`CONFIG_REQUIRED`](https://aventara-framework.vercel.app/docs/error-codes#config_required) | A required configuration key is missing. | A key that must be present is absent: api, adapter, or an overlay's exposed when the overlay sets exemptions. | Add the key at the diagnostic's path. |
104
- | [`CONFIG_UNKNOWN_PROPERTY`](https://aventara-framework.vercel.app/docs/error-codes#config_unknown_property) | Unknown configuration key. | A key the configuration does not define at that place: a typo, or a key written at the wrong level of the tree. | Remove it, or move it where it belongs (the configuration reference's "Where a key can go"). |
103
+ | [`CONFIG_REQUIRED`](https://aventara-framework.vercel.app/docs/error-codes#config_required) | A required configuration key is missing. | 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. | Add the key at the diagnostic's path. |
104
+ | [`CONFIG_UNKNOWN_PROPERTY`](https://aventara-framework.vercel.app/docs/error-codes#config_unknown_property) | Unknown configuration key. | 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. | Remove it, or move it where it belongs (the configuration reference's "Where a key can go"). |
105
105
  | [`CONFIG_INVALID_TYPE`](https://aventara-framework.vercel.app/docs/error-codes#config_invalid_type) | A configuration key has the wrong type. | A value of the wrong type, or a key set to an explicit undefined. | Give the value the type the reference states; leave a key out rather than setting it to undefined. |
106
- | [`CONFIG_INVALID_VALUE`](https://aventara-framework.vercel.app/docs/error-codes#config_invalid_value) | A configuration value is not allowed. | A malformed api or mcp path, a negative limit, an unknown transactionsStyle, a format that is not a named zod format, or forceTransaction under an effective transactionsStyle "none". | Use a value the message allows. |
106
+ | [`CONFIG_INVALID_VALUE`](https://aventara-framework.vercel.app/docs/error-codes#config_invalid_value) | A configuration value is not allowed. | 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). | Use a value the message allows. |
107
107
  | [`CONFIG_MOVED_PROPERTY`](https://aventara-framework.vercel.app/docs/error-codes#config_moved_property) | A configuration key moved in rc.1. | Configuration written for 1.0.0-rc.0: restrictions, entrypoint, fields, behaviors, transactions or pipelines.before/after. | Move it where the message says: restrictions to exposed + exemptions, entrypoint to api, fields to `resources.<R>.fields.<f>.definition`, transactions to transactionsStyle, before/after to pipelines.hooks. |
108
108
  | [`CONFIG_FORBIDDEN_IN_OVERLAY`](https://aventara-framework.vercel.app/docs/error-codes#config_forbidden_in_overlay) | A root-only key is written in an overlay. | A field definition in application, client or agent. | Define the field once at the root (`resources.<R>.fields.<f>.definition`); an overlay only chooses its visibility. |
109
109
 
package/README.md CHANGED
@@ -170,10 +170,8 @@ after compute and see every required field present.
170
170
  ## Returning a façade from a method
171
171
 
172
172
  A Nest project emits declarations (`declaration: true`), so a method without a return type has its type written out.
173
- Every node of the typed façades has a name for that: a whole surface (`AvApplicationFacade`), one Resource
174
- (`AvApplicationResourceFacade`), one family (`AvApplicationFamilyFacade`), and the same three for `client`, `appTx` and
175
- `clientTx` (`AvClientFacade`, `AvAppTxResourceFacade`, …). The declaration writes the name, with the Contract view as
176
- its argument, so this compiles:
173
+ Every node of the typed façades has a name for that, and so does the Contract view they are typed over, so a method can
174
+ return any of them without an annotation:
177
175
 
178
176
  ```ts
179
177
  @Injectable()
@@ -183,21 +181,43 @@ export class PostsService {
183
181
  posts() {
184
182
  return this.framework.application.Post;
185
183
  }
184
+
185
+ findMany() {
186
+ return this.framework.application.Post.find.many;
187
+ }
186
188
  }
187
189
  ```
188
190
 
189
- The view is written out in full, about 400 KB of declaration per such method over 50 Resources. Give the method a
190
- return type to keep it small: `AppFramework["application"]["Post"]` is written as that reference, about 0.2 KB.
191
+ The declaration file writes a short reference, under half a kilobyte per method at any schema size:
191
192
 
192
193
  ```ts
193
- posts(): AppFramework["application"]["Post"] {
194
- return this.framework.application.Post;
195
- }
194
+ posts(): import("@aventara/core").AvApplicationResourceFacade<
195
+ import("@aventara/core").AvContractView<import("…/discovery.artifact.js").AvModel, never, "omit", never, "omit", never, never>,
196
+ "Post">;
197
+ findMany(): import("@aventara/core").AvApplicationProjectingCall<
198
+ import("@aventara/core").AvContractView<import("…/discovery.artifact.js").AvModel, never, "omit", never, "omit", never, never>,
199
+ "Post", { readonly family: "find"; readonly variant: "many" }>;
196
200
  ```
197
201
 
198
- A variant (`this.framework.application.Post.find.many`) is a call type the declaration cannot name: give that method a
199
- return type, or return the call's result, whose row type is small. An application that publishes no declarations can
200
- set `"declaration": false` and skip the question.
202
+ The names, one set per surface (`application`, `client`, `appTx`, `clientTx`):
203
+
204
+ | Node | `framework.application` | the others |
205
+ | --- | --- | --- |
206
+ | the whole surface | `AvApplicationFacade` | `AvClientFacade`, `AvAppTxFacade`, `AvClientTxFacade` |
207
+ | one Resource | `AvApplicationResourceFacade` | `AvClientResourceFacade`, … |
208
+ | 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` |
211
+
212
+ Each takes the view, `AvContractView`, as its first argument. The view's arguments are your model by its name, the root's
213
+ and the overlay's exposure as written, and the `fields` entries your configuration's `resources` blocks hold. A
214
+ configuration with no `fields` entries writes `never` there. One that has them writes those entries, stages erased, after
215
+ every reference: measured at about 23 KB per method over 50 Resources that each configure a field. Annotate the method
216
+ (`posts(): AppFramework["application"]["Post"]`) if that matters to you; the annotation is written as that reference.
217
+
218
+ You never need to write these names yourself. They exist so a declaration file can name what a method returns, and a
219
+ project that reads only your declarations types the method exactly as your Framework does. An application that publishes
220
+ no declarations can set `"declaration": false` and skip the question.
201
221
 
202
222
  The configuration itself costs almost nothing to declare. The Prisma adapter's discovery artifact names the model's
203
223
  type (`AvModel`), so the scaffold's `aventaraConfig()`, a `buildFramework` and its `AppFramework` alias write
@@ -310,6 +330,46 @@ It compiles to `framework.contracts.agent`, identified by its own hash, and `fra
310
330
  endpoint. A host runs agent calls through `AvProtocol.bind(framework, { origin: "agent" })`, which validates them
311
331
  exactly as client calls are validated, against the Agent Contract.
312
332
 
333
+ ## A policy engine
334
+
335
+ `policy` hands authorization to an engine Aventara never imports. The engine answers three questions per
336
+ Resource — may the principal run this action (`can`), which rows (`scopeOf`, a `where` fragment), which fields
337
+ (`fieldsOf`) — and core enforces the answers on every client and agent request:
338
+
339
+ ```ts
340
+ const framework = await createFramework({
341
+ api: "/api",
342
+ adapter,
343
+ policy: {
344
+ engine, // satisfies AvPolicyEngine
345
+ principal: ({ $state }) => $state.user ?? null,
346
+ },
347
+ });
348
+
349
+ await framework.application.Note.find.many({}, { as: user }); // checked as `user`
350
+ await framework.application.Note.find.many({}); // server code: not checked
351
+ ```
352
+
353
+ The read scope is applied at the adapter call, after every developer stage, to the root `where`, every to-many include
354
+ and its `$count`, and every relation filter. A field the engine governs (its `governed` list) is never in a default
355
+ projection; name it to get it, `select: ["$defaults", "email"]`. A refusal answers `403 A4002`.
356
+
357
+ Writes are checked too. A create asks `can` about each new row, whose rules may look rows up through `exists`. An
358
+ update or delete acts only on rows the caller may change and read; a single-row one that misses answers `A4002` when
359
+ the caller can read the row and `A2003` when it cannot. A field the write sends must be in
360
+ `fieldsOf({ access: "write" })`. An update that moves a row out of what the caller reads, or fails an `after` condition
361
+ (`can` asked about the row the update returned), is rolled back. Where `transactionsStyle` is `"interactive"`, a
362
+ checked write with a check after it (an update, a nested write) runs in a transaction of its own, and a create or
363
+ delete checked before it runs is one statement; where it is `"none"`, a write that could only be checked after it runs
364
+ is refused (for after conditions, the updates the engine lists in `afterChecks`).
365
+
366
+ Nested writes (`$create`, `$connect` and the other relation directives) run as one call inside that transaction. Before
367
+ it, every row they link is held to the right scope: a linked row that holds the reference must be one the caller may
368
+ change, one whose parent holds it one the caller may read. A to-many `$update` or `$delete` acts only on the rows the
369
+ caller may change or delete, as `update.many` does. After the call, every row they changed is checked as an update is
370
+ (the after check, the move-out re-read), and every row they created is read back as stored and asked `can`; one refusal
371
+ rolls the whole write back. Where `transactionsStyle` is `"none"`, a nested write under a policy is refused.
372
+
313
373
  ## License
314
374
 
315
375
  PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
@@ -5,6 +5,13 @@ import type { AvAdapterModel } from "./adapter-model.js";
5
5
  * Provider execution request after framework semantic validation.
6
6
  * Operation identity and arguments remain provider-neutral; adapters receive
7
7
  * only validated semantic argument objects, never raw transport input.
8
+ *
9
+ * One shape exists here only: the flat unique selector of an `update.unique` or
10
+ * a `delete.unique` may carry `AND`, a list of conditions in the general
11
+ * `where` grammar the selected row must also meet. A row the selector names
12
+ * that misses them is a miss (the same `null` as a row that is not there). An
13
+ * adapter must honour it or refuse the operation, never drop it: dropping it
14
+ * would write a row the conditions exclude.
8
15
  */
9
16
  export type AvAdapterOperation<M extends AvAdapterModel = AvAdapterModel> = AvOperationIdentity & {
10
17
  readonly resource: Extract<keyof M["resources"], string>;
@@ -30,3 +30,9 @@ export type ContractCompilationResult = {
30
30
  * Agent Contract from the view of the layer it extends.
31
31
  */
32
32
  export declare function compileContracts<M extends AvAdapterModel>(model: M, config: ResolvedFrameworkConfig<M>): Promise<ContractCompilationResult>;
33
+ /**
34
+ * Binds Contracts loaded from a prebuilt file to the runtime pieces
35
+ * {@link compileContracts} binds to the ones it compiles, from the live
36
+ * `config` and `model` they were compiled from.
37
+ */
38
+ export declare function bindPrebuiltContracts<M extends AvAdapterModel>(model: M, config: ResolvedFrameworkConfig<M>, contracts: AvContracts): void;
@@ -1002,6 +1002,7 @@ function projectCore(model, config, diagnostics) {
1002
1002
  baseline.transactions = resolveTransactions(model, "root", scope, diagnostics);
1003
1003
  checkResourceTree(model, baseline, "root", config.trees.root, config.definitions, diagnostics);
1004
1004
  applyFieldDefinitions(model, baseline, config.definitions, diagnostics);
1005
+ markGovernedFields(baseline, config.policy?.governed);
1005
1006
  applyCompute(baseline, scope.compute);
1006
1007
  checkLayerValidators(baseline.resources, "root", config.trees.root, diagnostics);
1007
1008
  const validators = compileValidatorTable(baseline.resources, scope.pipelines);
@@ -1014,6 +1015,27 @@ function projectCore(model, config, diagnostics) {
1014
1015
  validateProjection(projection, "root", diagnostics);
1015
1016
  return { baseline, projection, validators };
1016
1017
  }
1018
+ function markGovernedFields(baseline, governed) {
1019
+ if (governed === undefined) {
1020
+ return;
1021
+ }
1022
+ for (const [resourceName, names] of Object.entries(governed)) {
1023
+ const resource = Object.hasOwn(baseline.resources, resourceName)
1024
+ ? baseline.resources[resourceName]
1025
+ : undefined;
1026
+ if (resource === undefined) {
1027
+ continue;
1028
+ }
1029
+ for (const name of names) {
1030
+ const field = Object.hasOwn(resource.fields, name)
1031
+ ? resource.fields[name]
1032
+ : undefined;
1033
+ if (field !== undefined) {
1034
+ field.governed = true;
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1017
1039
  function projectDerivedScope(model, scopeName, core, parentView, config, diagnostics) {
1018
1040
  const scope = config[scopeName];
1019
1041
  const baseline = cloneValue(core.baseline);
@@ -1095,18 +1117,57 @@ export async function compileContracts(model, config) {
1095
1117
  };
1096
1118
  }
1097
1119
  }
1098
- bindShapedFields(core, compilerConfig.definitions);
1099
- bindShapedFields(application, compilerConfig.definitions);
1100
- bindShapedFields(client, compilerConfig.definitions);
1101
- bindShapedFields(agent, compilerConfig.definitions);
1102
- bindFieldValidators(core, coreCompilation.validators);
1103
- bindFieldValidators(application, applicationProjection.validators);
1104
- bindFieldValidators(client, clientProjection.validators);
1105
- bindFieldValidators(agent, agentProjection.validators);
1106
- return {
1107
- ok: true,
1108
- contracts: { core, application, client, agent },
1109
- };
1120
+ const contracts = { core, application, client, agent };
1121
+ bindRuntimePieces(contracts, compilerConfig.definitions, {
1122
+ core: coreCompilation.validators,
1123
+ application: applicationProjection.validators,
1124
+ client: clientProjection.validators,
1125
+ agent: agentProjection.validators,
1126
+ });
1127
+ return { ok: true, contracts };
1128
+ }
1129
+ function bindRuntimePieces(contracts, definitions, validators) {
1130
+ for (const origin of ["core", "application", "client", "agent"]) {
1131
+ bindShapedFields(contracts[origin], definitions);
1132
+ bindFieldValidators(contracts[origin], validators[origin]);
1133
+ }
1134
+ }
1135
+ export function bindPrebuiltContracts(model, config, contracts) {
1136
+ const compilerConfig = config;
1137
+ const fields = validatedFieldsView(model, compilerConfig.definitions);
1138
+ bindRuntimePieces(contracts, compilerConfig.definitions, {
1139
+ core: compileValidatorTable(fields, compilerConfig.root.pipelines),
1140
+ application: compileValidatorTable(fields, compilerConfig.application.pipelines),
1141
+ client: compileValidatorTable(fields, compilerConfig.client.pipelines),
1142
+ agent: compileValidatorTable(fields, compilerConfig.agent.pipelines),
1143
+ });
1144
+ }
1145
+ function validatedFieldsView(model, definitions) {
1146
+ const resources = { ...model.resources };
1147
+ for (const [resourceName, resourceDefinitions] of Object.entries(definitions)) {
1148
+ const resource = Object.hasOwn(model.resources, resourceName)
1149
+ ? model.resources[resourceName]
1150
+ : undefined;
1151
+ if (resource === undefined) {
1152
+ continue;
1153
+ }
1154
+ let added;
1155
+ for (const [fieldName, definition] of Object.entries(resourceDefinitions)) {
1156
+ if (Object.hasOwn(resource.fields, fieldName)) {
1157
+ continue;
1158
+ }
1159
+ added ??= {};
1160
+ added[fieldName] = {
1161
+ kind: "scalar",
1162
+ type: definition.type,
1163
+ list: definition.list,
1164
+ };
1165
+ }
1166
+ if (added !== undefined) {
1167
+ resources[resourceName] = { fields: { ...resource.fields, ...added } };
1168
+ }
1169
+ }
1170
+ return resources;
1110
1171
  }
1111
1172
  async function identifiedContract(projection) {
1112
1173
  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
- const emitWarning = globalThis.process?.emitWarning;
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, or forceTransaction under an effective transactionsStyle "none".',
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,