@aventara/next 0.0.0-stage → 1.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +322 -2
- package/dist/aventara.factory.d.ts +155 -0
- package/dist/aventara.factory.js +61 -0
- package/dist/host-bootstrap.validator.d.ts +49 -0
- package/dist/host-bootstrap.validator.js +73 -0
- package/dist/hydration-tag.format.d.ts +11 -0
- package/dist/hydration-tag.format.js +6 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/mcp-binding.loader.d.ts +33 -0
- package/dist/mcp-binding.loader.js +39 -0
- package/dist/protocol-route.table.d.ts +21 -0
- package/dist/protocol-route.table.js +10 -0
- package/dist/request-body.reader.d.ts +35 -0
- package/dist/request-body.reader.js +130 -0
- package/dist/request-state.cache.d.ts +9 -0
- package/dist/request-state.cache.js +8 -0
- package/dist/route-handlers.factory.d.ts +60 -0
- package/dist/route-handlers.factory.js +69 -0
- package/dist/server-operations.factory.d.ts +40 -0
- package/dist/server-operations.factory.js +182 -0
- package/dist/shared-framework.registry.d.ts +25 -0
- package/dist/shared-framework.registry.js +75 -0
- package/dist/universal-path.registry.d.ts +28 -0
- package/dist/universal-path.registry.js +36 -0
- package/dist/value.dehydrator.d.ts +14 -0
- package/dist/value.dehydrator.js +72 -0
- package/dist/web-exchange.translator.d.ts +15 -0
- package/dist/web-exchange.translator.js +37 -0
- package/package.json +73 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @aventara/next
|
|
2
|
+
|
|
3
|
+
## 1.0.0-rc.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 04bb39a: `engines.node` is now `^22.23.4 || >=24.22.1`, the Node security releases of 2026-10-20 (CVE-2026-105481,
|
|
8
|
+
CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-106072, CVE-2026-106073, CVE-2026-106074,
|
|
9
|
+
CVE-2026-111497). The bins refuse an older Node in one sentence naming that range.
|
|
10
|
+
- 587dd01: The route handlers answer `POST {api}/_access/can` when the Framework is built with `access`, through
|
|
11
|
+
`AvProtocol.bind(framework).handleCan`, with the request's state seeded as on every other protocol route.
|
|
12
|
+
- d7e7369: `avClient.$access`: `framework.serverClient.$access` with the request filled in. `can` is answered under the
|
|
13
|
+
request's state; `me()` and the other Aventara Access verbs run in process with the headers of `next/headers` (a
|
|
14
|
+
Server Action's sign-in sets its cookie through Access itself). `ready()` registers the verbs for
|
|
15
|
+
`@/aventara/universal` on the server.
|
|
16
|
+
- 7ee547b: `avClient.$access` carries Aventara Access's `kiosk` verbs (`switchUser`, `lock`, `setPin`, `enrollDevice`,
|
|
17
|
+
`revokeDevice`) when the provider states them, run in process with the headers of `next/headers`, so a Server Action's
|
|
18
|
+
switch sets the rotated device cookie through Access itself; `ready()` registers them for `@/aventara/universal`.
|
|
19
|
+
- 88263a1: Documentation: `$access.can([])` answers `[]`, not a refusal; the MCP README states the query and fragment rule for
|
|
20
|
+
every authorization server.
|
|
21
|
+
- 15f3402: `avClient.$access` holds only the verbs the access provider offers, as `serverClient.$access` does. Calling another one
|
|
22
|
+
rejects with `AvProtocolError` `A2002`, naming why (no `access`, or a provider that does not offer that verb), instead
|
|
23
|
+
of a `TypeError`.
|
|
24
|
+
|
|
25
|
+
In `next dev`, an edit that changes only a function (a guard's or a pipe's body, the adapter factory) builds the
|
|
26
|
+
Framework again; it no longer needs a restart.
|
|
27
|
+
|
|
28
|
+
- Updated dependencies [587dd01]
|
|
29
|
+
- Updated dependencies [d7e7369]
|
|
30
|
+
- Updated dependencies [7ee547b]
|
|
31
|
+
- Updated dependencies [f52c499]
|
|
32
|
+
- Updated dependencies [2907484]
|
|
33
|
+
- Updated dependencies [a6bbdf0]
|
|
34
|
+
- Updated dependencies [3e26549]
|
|
35
|
+
- Updated dependencies [a6bbdf0]
|
|
36
|
+
- Updated dependencies [6f3f6f6]
|
|
37
|
+
- Updated dependencies [04bb39a]
|
|
38
|
+
- Updated dependencies [d7e7369]
|
|
39
|
+
- Updated dependencies [3fb3333]
|
|
40
|
+
- Updated dependencies [827ccb6]
|
|
41
|
+
- Updated dependencies [7f5abc3]
|
|
42
|
+
- Updated dependencies [88263a1]
|
|
43
|
+
- Updated dependencies [10341d5]
|
|
44
|
+
- Updated dependencies [8129844]
|
|
45
|
+
- Updated dependencies [88263a1]
|
|
46
|
+
- Updated dependencies [3020b5c]
|
|
47
|
+
- Updated dependencies [88263a1]
|
|
48
|
+
- Updated dependencies [88263a1]
|
|
49
|
+
- @aventara/core@1.0.0-rc.10
|
|
50
|
+
- @aventara/mcp@1.0.0-rc.10
|
|
51
|
+
|
|
52
|
+
## 1.0.0-rc.9
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- 09c5703: New package `@aventara/next`: hosts an Aventara Framework in a Next.js App Router application (Next `>=16.4 <17`,
|
|
57
|
+
Node runtime). It is in the fixed version group, released at the same version as every other package. `react` (`^19`)
|
|
58
|
+
is a peer, and `@aventara/mcp` an optional one. The entry is server-only.
|
|
59
|
+
|
|
60
|
+
- `Aventara({ ...aventaraConfig, adapter, state })` takes the configuration `createFramework` takes, flat. `adapter`
|
|
61
|
+
may be a function that builds the adapter (it may return a promise), and `state` (`({ headers, request }) => state`)
|
|
62
|
+
seeds each request's `ctx.$state`. It returns at once and builds nothing: the Framework is created on first use,
|
|
63
|
+
once.
|
|
64
|
+
- It returns `{ handlers, avClient, avApp, ready }`. `handlers` is `{ GET, POST, PUT, PATCH, DELETE }` for one
|
|
65
|
+
catch-all route file (`src/app/api/[...aventara]/route.ts`), serving the protocol's operation routes,
|
|
66
|
+
`GET /_contract` and `POST /_transactions`. `avClient` is the client layer for Server Components and Server Actions
|
|
67
|
+
(`framework.serverClient`, with this request's `state`). `avApp` is the application layer for trusted server code
|
|
68
|
+
(`framework.application`). `ready()` builds the Framework once and rejects with the construction error, so
|
|
69
|
+
`src/instrumentation.ts` can await it and a bad configuration fails `next start`.
|
|
70
|
+
- `Aventara({ framework, state })` hosts a Framework you built with `createFramework`, as
|
|
71
|
+
`AventaraModule.forRoot({ framework })` does in NestJS. A Framework and a configuration together are a type error,
|
|
72
|
+
and are refused when `Aventara()` is called.
|
|
73
|
+
- `access`: an access provider (from `@aventara/core`). Its `policy` checks each request and its `state` seeds
|
|
74
|
+
`ctx.$state`, called with the same `{ headers, request }`. It does not go with `state` or `policy`.
|
|
75
|
+
- `aventara-build --client` writes `@/aventara/client` (the browser client) and `@/aventara/universal`, whose `avClient`
|
|
76
|
+
calls `/api` in the browser and answers the same request in process on the server. `avDehydrate` (this package) and
|
|
77
|
+
`avHydrate` (`@/aventara/client`) carry `Date`, `bigint`, `Uint8Array` and decimal values from a Server Component to
|
|
78
|
+
a Client Component, through TanStack Query's `serializeData`/`deserializeData` or an SWR `fallback`.
|
|
79
|
+
- With `mcp` configured, the same `handlers` serve `@aventara/mcp`'s endpoint and its OAuth protected-resource
|
|
80
|
+
metadata, each from a one-line route file. On Next.js `mcp` needs `authorization`: without it the configuration is
|
|
81
|
+
a type error on the `Aventara({...})` call, or refused when the Framework is built. The endpoint is named at the
|
|
82
|
+
origin the agent called (`X-Forwarded-Host` and `X-Forwarded-Proto` when present, else `Host`).
|
|
83
|
+
- Every copy of `src/aventara/server.ts` that Next.js evaluates in one server process shares one Framework per `api`
|
|
84
|
+
path, so `ready()` in `instrumentation.ts` builds the one the routes use. In production, a different configuration
|
|
85
|
+
on a path already held is refused when `Aventara()` is called; in development it replaces the held one.
|
|
86
|
+
- `serverExternalPackages: ["@aventara/core", "@aventara/prisma7-adapter"]` in `next.config.ts` is required: Node.js
|
|
87
|
+
then loads core and the adapter once per server process instead of a copy per layer, so every layer gets values
|
|
88
|
+
and errors of the same classes and one Prisma client. `aventara new --host next` and `aventara init` add it, or
|
|
89
|
+
print it when they cannot edit the config safely.
|
|
90
|
+
- A call reads the database, so it runs at request time: every `avClient`, `avApp` and `@/aventara/universal` call
|
|
91
|
+
first awaits Next's `io()` (`next/cache`). Under Cache Components (`cacheComponents: true`, create-next-app's
|
|
92
|
+
default) `next build` never runs one while it prerenders a page, and a page that calls one needs what any uncached
|
|
93
|
+
database read needs: the calling component inside `<Suspense>` (or a `loading.tsx`), `"use cache"` on a function
|
|
94
|
+
that makes the call, or `export const instant = false`. Without one, `next build` fails with Next's own message
|
|
95
|
+
naming the three. In a request, inside `"use cache"`, in a Server Action, in an `after()` callback and without
|
|
96
|
+
Cache Components the call runs at once.
|
|
97
|
+
- `AvNextHandlers` (the type of `handlers`), `AvNextClientTransaction` (`avClient.transaction`) and
|
|
98
|
+
`AvNextAppTransaction` (`avApp.transaction`) are exported, so a project that emits declarations
|
|
99
|
+
(`declaration: true`) can write `export const { handlers, avClient, avApp, ready } = Aventara(...)` with no
|
|
100
|
+
annotation. `AvNextStateContext`, what the `state` option is handed (`{ headers, request? }`), is exported too,
|
|
101
|
+
for a `state` function written in a file of its own.
|
|
102
|
+
|
|
103
|
+
- Updated dependencies [205a379]
|
|
104
|
+
- Updated dependencies [a0bb05e]
|
|
105
|
+
- Updated dependencies [86ab319]
|
|
106
|
+
- Updated dependencies [fdf52e6]
|
|
107
|
+
- @aventara/core@1.0.0-rc.9
|
|
108
|
+
- @aventara/mcp@1.0.0-rc.9
|
package/LICENSE
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Required Notice: Copyright 2026 Mohamed Ragheb
|
|
2
|
+
|
|
3
|
+
# PolyForm Shield License 1.0.0
|
|
4
|
+
|
|
5
|
+
<https://polyformproject.org/licenses/shield/1.0.0>
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncompete
|
|
34
|
+
|
|
35
|
+
Any purpose is a permitted purpose, except for providing any product that competes with the software or any product the licensor or any of its affiliates provides using the software.
|
|
36
|
+
|
|
37
|
+
## Competition
|
|
38
|
+
|
|
39
|
+
Goods and services compete even when they provide functionality through different kinds of interfaces or for different technical platforms. Applications can compete with services, libraries with plugins, frameworks with development tools, and so on, even if they're written in different programming languages or for different computer architectures. Goods and services compete even when provided free of charge. If you market a product as a practical substitute for the software or another product, it definitely competes.
|
|
40
|
+
|
|
41
|
+
## New Products
|
|
42
|
+
|
|
43
|
+
If you are using the software to provide a product that does not compete, but the licensor or any of its affiliates brings your product into competition by providing a new version of the software or another product using the software, you may continue using versions of the software available under these terms beforehand to provide your competing product, but not any later versions.
|
|
44
|
+
|
|
45
|
+
## Discontinued Products
|
|
46
|
+
|
|
47
|
+
You may begin using the software to compete with a product or service that the licensor or any of its affiliates has stopped providing, unless the licensor includes a plain-text line beginning with `Licensor Line of Business:` with the software that mentions that line of business. For example:
|
|
48
|
+
|
|
49
|
+
> Licensor Line of Business: YoyodyneCMS Content Management System (http://example.com/cms)
|
|
50
|
+
|
|
51
|
+
## Sales of Business
|
|
52
|
+
|
|
53
|
+
If the licensor or any of its affiliates sells a line of business developing the software or using the software to provide a product, the buyer can also enforce [Noncompete](#noncompete) for that product.
|
|
54
|
+
|
|
55
|
+
## Fair Use
|
|
56
|
+
|
|
57
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
58
|
+
|
|
59
|
+
## No Other Rights
|
|
60
|
+
|
|
61
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
62
|
+
|
|
63
|
+
## Patent Defense
|
|
64
|
+
|
|
65
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
66
|
+
|
|
67
|
+
## Violations
|
|
68
|
+
|
|
69
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
70
|
+
|
|
71
|
+
## No Liability
|
|
72
|
+
|
|
73
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
74
|
+
|
|
75
|
+
## Definitions
|
|
76
|
+
|
|
77
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
78
|
+
|
|
79
|
+
A **product** can be a good or service, or a combination of them.
|
|
80
|
+
|
|
81
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
82
|
+
|
|
83
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all its affiliates.
|
|
84
|
+
|
|
85
|
+
**Affiliates** means the other organizations than an organization has control over, is under the control of, or is under common control with.
|
|
86
|
+
|
|
87
|
+
**Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
88
|
+
|
|
89
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
90
|
+
|
|
91
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Additional Permission — Aventara
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Mohamed Ragheb
|
|
4
|
+
|
|
5
|
+
In addition to the permissions of the PolyForm Shield License 1.0.0 in `LICENSE`, the licensor grants the following additional permission:
|
|
6
|
+
|
|
7
|
+
The Noncompete section applies only to providing a product that competes with the software itself — the Aventara framework, its packages and its tooling. Providing a product that competes with any other product the licensor or its affiliates provide using the software is a permitted purpose.
|
|
8
|
+
|
|
9
|
+
This additional permission only adds to your permissions; it does not restrict or replace any term of `LICENSE`.
|
package/README.md
CHANGED
|
@@ -1,3 +1,323 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@aventara/next`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Hosts an Aventara Framework in a Next.js App Router application (Next 16.4 or later 16.x, Node runtime). It serves the
|
|
4
|
+
framework's HTTP protocol (`AvProtocol.bind(framework)` from `@aventara/core/protocol`) from one catch-all route file.
|
|
5
|
+
Every rule of the protocol (routes, decoding, statuses, envelopes, headers, absence) is core's; this package only turns
|
|
6
|
+
a Web `Request` into the protocol's request and writes the protocol's answer back as a Web `Response`.
|
|
7
|
+
|
|
8
|
+
`@aventara/core` is its dependency; `next` (`>=16.4 <17`) and `react` (`^19`) are peers, and `@aventara/mcp` an
|
|
9
|
+
optional one (see MCP). The entry is server-only: a Client Component
|
|
10
|
+
importing it fails the Next.js build.
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// src/aventara/server.ts
|
|
16
|
+
import "server-only";
|
|
17
|
+
import { Aventara } from "@aventara/next";
|
|
18
|
+
import { createPrismaAdapter } from "@aventara/prisma7-adapter";
|
|
19
|
+
import { prisma } from "../prisma";
|
|
20
|
+
import adapter from "./adapter";
|
|
21
|
+
import { aventaraConfig } from "./config";
|
|
22
|
+
|
|
23
|
+
export const { handlers, avClient, avApp, ready } = Aventara({
|
|
24
|
+
...aventaraConfig, // api: "/api", resources, policy, ...
|
|
25
|
+
adapter: () => createPrismaAdapter({ ...adapter, client: prisma }),
|
|
26
|
+
state: async ({ headers }) => ({ user: await userFrom(headers) }), // your auth library
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// src/app/api/[...aventara]/route.ts (the folder matches api: "/api")
|
|
32
|
+
import { handlers } from "@/aventara/server";
|
|
33
|
+
|
|
34
|
+
export const { GET, POST, PUT, PATCH, DELETE } = handlers;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Option | What it is |
|
|
38
|
+
|---|---|
|
|
39
|
+
| everything `createFramework` takes | `api`, `resources`, `policy`, `limits`, `prebuilt`, ... checked exactly as `createFramework` checks them |
|
|
40
|
+
| `adapter` | the adapter, or a function that builds it (it may return a promise), called on the first request |
|
|
41
|
+
| `state` | `({ headers, request }) => state`: seeds each request's `ctx.$state` (where `policy.principal` reads the user) |
|
|
42
|
+
| `access` | an access provider (see `@aventara/core`): its `policy` checks each request and its `state` seeds `ctx.$state`, handed the same `{ headers, request }`. Not together with `state` or `policy` |
|
|
43
|
+
|
|
44
|
+
Or hand it a Framework you built, as `createFramework` resolved it (as `AventaraModule.forRoot({ framework })` takes
|
|
45
|
+
one in NestJS), with `state` beside it (or none, when it was built with `access`):
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const framework = await createFramework({ ...aventaraConfig, adapter: createPrismaAdapter({ ...adapter, client }) });
|
|
49
|
+
export const { handlers, avClient, avApp, ready } = Aventara({ framework, state });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every member works on that Framework, which is never built again: `avClient.tx` and `avApp.appTx` build handles at
|
|
53
|
+
once, and `ready()` checks it (a Resource named like a member, `mcp` without `authorization`) and registers the
|
|
54
|
+
universal path. A Framework and a configuration are two sources of one Framework, so the options carry one or the
|
|
55
|
+
other: a configuration key beside `framework` is a type error, and `Aventara()` refuses it when called, naming the
|
|
56
|
+
keys. So is a `framework` that is not one.
|
|
57
|
+
|
|
58
|
+
`Aventara()` returns at once and builds nothing: `next build` imports the route module, so the Framework (and the
|
|
59
|
+
adapter, when it is a function) is created on first use, or by `ready()`, once. A configuration `createFramework`
|
|
60
|
+
refuses rejects that use with its `AvConstructionError`.
|
|
61
|
+
|
|
62
|
+
| Member | What it is |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `handlers` | `{ GET, POST, PUT, PATCH, DELETE }` for the catch-all route file |
|
|
65
|
+
| `avClient` | the client layer for Server Components and Server Actions: `framework.serverClient`, with this request's `state`; `avClient.tx` and `avClient.transaction([...])` |
|
|
66
|
+
| `avApp` | the application layer for trusted server code: `framework.application`; `avApp.appTx` and `avApp.transaction([...])` |
|
|
67
|
+
| `ready` | `() => Promise<void>`: builds the Framework once; rejects with the construction error; registers the in-process path `@/aventara/universal` calls on the server |
|
|
68
|
+
|
|
69
|
+
`handlers` is typed `AvNextHandlers`, and `avClient.transaction` and `avApp.transaction` `AvNextClientTransaction` and
|
|
70
|
+
`AvNextAppTransaction`, all three exported, so a project that emits declarations (`declaration: true`) writes
|
|
71
|
+
`export const { handlers, avClient, avApp, ready } = Aventara(...)` with no annotation. `AvNextStateContext` is what
|
|
72
|
+
`state` is handed (`{ headers, request? }`), for a `state` function written in a file of its own:
|
|
73
|
+
`export const state = async ({ headers }: AvNextStateContext) => ({ user: await userOf(headers) });`.
|
|
74
|
+
|
|
75
|
+
## Server Components and Server Actions
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// src/app/campaigns/page.tsx
|
|
79
|
+
import { Suspense } from "react";
|
|
80
|
+
import { avClient } from "@/aventara/server";
|
|
81
|
+
|
|
82
|
+
async function Campaigns() {
|
|
83
|
+
const campaigns = await avClient.Campaign.find.many({
|
|
84
|
+
where: { startsAt: { gt: new Date() } },
|
|
85
|
+
select: ["id", "name", "startsAt"],
|
|
86
|
+
});
|
|
87
|
+
return <ul>{campaigns.map((c) => <li key={c.id}>{c.name}</li>)}</ul>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function Page() {
|
|
91
|
+
return (
|
|
92
|
+
<Suspense fallback={<p>Loading…</p>}>
|
|
93
|
+
<Campaigns />
|
|
94
|
+
</Suspense>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`avClient.<Resource>.<family>.<variant>(args, options?)` is `framework.serverClient`: typed from the Client
|
|
100
|
+
Contract's view (what the client layer hides is absent), taking and answering application values (`Date`, `bigint`,
|
|
101
|
+
`AvDecimal`, `Uint8Array`). A call runs in process, with no HTTP: the same client pipelines, validation, exposure and
|
|
102
|
+
policy as a request to `handlers`, with the principal `policy.principal` reads from `state`. A refusal is thrown as
|
|
103
|
+
its `AvError` class (`AvAuthError` for the policy).
|
|
104
|
+
|
|
105
|
+
`state` is called with `{ headers }` from `next/headers` (there is no `request` outside a route call), once per
|
|
106
|
+
server render however many calls the render makes. Outside a render, in a Server Action's body for example, it runs
|
|
107
|
+
once per call. A call that names its own `state` in its options uses it as given. Without a `state` option, calls
|
|
108
|
+
start from `{}` and read no headers.
|
|
109
|
+
|
|
110
|
+
### Request-time data
|
|
111
|
+
|
|
112
|
+
A call reads the database, so Next.js renders it at request time, never into the page `next build` prerenders. Every
|
|
113
|
+
`avClient`, `avApp` and `@/aventara/universal` call first awaits Next's `io()` (`next/cache`). Under Cache Components
|
|
114
|
+
(`cacheComponents: true`, which create-next-app sets), a page that calls one needs what any uncached database read
|
|
115
|
+
needs, one of:
|
|
116
|
+
|
|
117
|
+
- `<Suspense>` around the component that calls it (or a `loading.tsx` beside the page): the fallback is prerendered and
|
|
118
|
+
the data streams in at request time, as above;
|
|
119
|
+
- `"use cache"` on a function that makes the call: the result is cached, and prerendered with the page;
|
|
120
|
+
- `export const instant = false` in the page: it renders whole, at request time.
|
|
121
|
+
|
|
122
|
+
With none of them, `next build` fails with Next's own message, which names the three. In a request, inside
|
|
123
|
+
`"use cache"`, in a Server Action, in an `after()` callback and without Cache Components the call runs at once.
|
|
124
|
+
|
|
125
|
+
With `access`, `avClient.$access` is `framework.serverClient.$access` with this request filled in: `can([...])` is
|
|
126
|
+
answered under the request's state, and `me()`, `signIn(...)` and the other verbs the provider offers run Aventara
|
|
127
|
+
Access in process with the headers of `next/headers`. In a Server Action, Access sets a sign-in's cookie itself
|
|
128
|
+
(Better Auth's `nextCookies`). `@/aventara/universal`'s `$access` does the same on the server, through what `ready()`
|
|
129
|
+
registers, and calls Access's own routes in the browser. `$access` holds the verbs the provider offers, read from the
|
|
130
|
+
options before the Framework is built, as `serverClient.$access` holds them. Calling another one rejects with
|
|
131
|
+
`AvProtocolError` `A2002`, naming why: no `access`, or a provider whose `client` does not offer that verb.
|
|
132
|
+
|
|
133
|
+
On Access's `kiosk` profile, call `switchUser`, `lock` and `enrollDevice` from a Server Action (or from the browser),
|
|
134
|
+
never while rendering a Server Component: each rotates the device's credential cookie, which Next lets Access set only
|
|
135
|
+
in a Server Action or a Route Handler, so a rotation during a render is lost and the device's next switch is refused. Never call a verb that
|
|
136
|
+
changes anything (`signIn`, `signUp`, `signOut`, `resetPassword`, and the kiosk's five) inside `"use cache"`: a cached
|
|
137
|
+
result would replay one caller's answer to another and skip the call. Each verb's answer is projected onto its result
|
|
138
|
+
schema on the server too, so no session token or device secret reaches your code.
|
|
139
|
+
|
|
140
|
+
`avApp` is `framework.application`: trusted server code. It reads no request, and the policy checks a call only when
|
|
141
|
+
it names a principal (`{ as: principal }`).
|
|
142
|
+
|
|
143
|
+
Transactions keep their layer: `avClient.tx` builds client-layer handles (`framework.serverClientTx`) and
|
|
144
|
+
`avClient.transaction([...])` runs them with this request's state; `avApp.appTx` builds application handles (and
|
|
145
|
+
`avApp.appTx.$sql`), and `avApp.transaction([...])` runs them. A list that mixes the two is a type error, and refused
|
|
146
|
+
at runtime (`A2004`). Handles are built on the built Framework: `await ready()` before building them.
|
|
147
|
+
|
|
148
|
+
## Startup
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
// src/instrumentation.ts
|
|
152
|
+
export async function register() {
|
|
153
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
154
|
+
const { ready } = await import("@/aventara/server");
|
|
155
|
+
await ready();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`ready()` builds the Framework and its routes, once: a second call returns the same promise. It rejects with the
|
|
161
|
+
construction error, so a bad configuration fails `next start`. It reads no request, so it runs where there is none
|
|
162
|
+
(`instrumentation.ts`, a script).
|
|
163
|
+
|
|
164
|
+
### `serverExternalPackages` (required)
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// next.config.ts
|
|
168
|
+
const nextConfig: NextConfig = {
|
|
169
|
+
serverExternalPackages: ["@aventara/core", "@aventara/prisma7-adapter"],
|
|
170
|
+
};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Next.js bundles a package into each layer of the server (instrumentation, route handlers, Server Components) as a
|
|
174
|
+
copy of its own. Listed here, `@aventara/core` and the adapter are loaded by Node.js once per server process instead:
|
|
175
|
+
one core, so the Framework every layer shares (below) answers each layer with values and errors of the same classes
|
|
176
|
+
(`AvDecimal`, `AvValidationError`, ...), and one Prisma client. `aventara new --host next` and `aventara init` add the
|
|
177
|
+
line (an existing config they cannot edit safely gets it printed instead). `@aventara/next` itself stays bundled: it
|
|
178
|
+
relies on Next.js's server aliases (`next/navigation`, `next/headers`, `server-only`).
|
|
179
|
+
|
|
180
|
+
Next.js evaluates `src/aventara/server.ts` more than once in one server process (once for `instrumentation.ts`, again
|
|
181
|
+
for the routes and pages). Every copy that calls `Aventara()` with the same options shares one Framework, held per
|
|
182
|
+
`api` path for the process, so `ready()` in `instrumentation.ts` builds the one the routes use, with one adapter, and
|
|
183
|
+
`avClient.tx` works from the first request. Two instances on two `api` paths hold two. In production, a different
|
|
184
|
+
configuration on a path already held is refused when `Aventara()` is called; in development it replaces the held one,
|
|
185
|
+
as `next dev` evaluates the file again after an edit. Copies are told apart by the options' data (Next.js rewrites a
|
|
186
|
+
function's source in each layer). An edit that changes only a function, a guard's or a pipe's body or the adapter
|
|
187
|
+
factory, is told by each layer from the text that layer saw last: in development the first layer to see it builds the
|
|
188
|
+
Framework again, and the other layers join that build. A Framework given as `{ framework }` is that call's own.
|
|
189
|
+
|
|
190
|
+
Once the Framework is built, `ready()` also registers the path `@/aventara/universal`'s `avClient` takes on the server
|
|
191
|
+
(below). Without `instrumentation.ts`, that `avClient` refuses its calls on the server in one sentence.
|
|
192
|
+
|
|
193
|
+
## Prefetch and hydrate: `@/aventara/universal`, `avDehydrate`, `avHydrate`
|
|
194
|
+
|
|
195
|
+
`aventara-build --client` writes `src/aventara/universal.ts`. Its `avClient` has the browser client's types and runs on
|
|
196
|
+
both sides: in the browser it calls `/api`; on the server it answers the same request in process, through the bound
|
|
197
|
+
protocol and with this request's `state`, so a query prefetched in a Server Component and refetched in the browser
|
|
198
|
+
gets the same values (`Date`, `bigint`, the client's own `AvDecimal`).
|
|
199
|
+
|
|
200
|
+
A decimal is a class instance, which React refuses to pass from a Server Component to a Client Component. `avDehydrate`
|
|
201
|
+
(this package, server side) turns each `Date`, `bigint`, `Uint8Array` and decimal into a tagged plain object, and
|
|
202
|
+
`avHydrate` (`@/aventara/client`, browser side) turns them back.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// src/app/notes/notes.queries.ts (server and browser)
|
|
206
|
+
import { queryOptions } from "@tanstack/react-query";
|
|
207
|
+
import { avClient } from "@/aventara/universal";
|
|
208
|
+
|
|
209
|
+
export const notesQuery = queryOptions({
|
|
210
|
+
queryKey: ["notes"],
|
|
211
|
+
queryFn: () => avClient.Note.find.many({ select: ["id", "title", "createdAt"] }),
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
// src/app/notes/page.tsx (Server Component)
|
|
217
|
+
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
|
|
218
|
+
import { Suspense } from "react";
|
|
219
|
+
import { avDehydrate } from "@aventara/next";
|
|
220
|
+
import { notesQuery } from "./notes.queries";
|
|
221
|
+
import { NoteList } from "./note-list";
|
|
222
|
+
|
|
223
|
+
async function Notes() {
|
|
224
|
+
const queryClient = new QueryClient();
|
|
225
|
+
await queryClient.prefetchQuery(notesQuery);
|
|
226
|
+
return (
|
|
227
|
+
<HydrationBoundary state={dehydrate(queryClient, { serializeData: avDehydrate })}>
|
|
228
|
+
<NoteList />
|
|
229
|
+
</HydrationBoundary>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default function Page() {
|
|
234
|
+
return (
|
|
235
|
+
<Suspense fallback={<p>Loading…</p>}>
|
|
236
|
+
<Notes />
|
|
237
|
+
</Suspense>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
// src/app/providers.tsx
|
|
244
|
+
"use client";
|
|
245
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
246
|
+
import { avHydrate } from "@/aventara/client";
|
|
247
|
+
|
|
248
|
+
const queryClient = new QueryClient({ defaultOptions: { hydrate: { deserializeData: avHydrate } } });
|
|
249
|
+
export const Providers = ({ children }: { children: React.ReactNode }) => (
|
|
250
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
251
|
+
);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
With SWR, pass `avDehydrate({ [key]: data })` from the Server Component and `avHydrate(fallback)` to `SWRConfig`'s
|
|
255
|
+
`fallback` in the Client Component.
|
|
256
|
+
|
|
257
|
+
Importing `@/aventara/client` instead of `@/aventara/universal` in shared code opts out of running on the server: its
|
|
258
|
+
calls are refused there.
|
|
259
|
+
|
|
260
|
+
## Routes
|
|
261
|
+
|
|
262
|
+
`handlers` answers the protocol's routes under `api`: each operation route (`POST /api/_resources/...`),
|
|
263
|
+
`GET /api/_contract` (with its `ETag`, and `304` on `If-None-Match`) and `POST /api/_transactions` when the
|
|
264
|
+
ClientContract's transactions are interactive. Every method is exported so that a wrong method on an operation route
|
|
265
|
+
gets the protocol's `405` (`A2012`) with `Allow`. A path the protocol does not own goes to Next's `notFound()`.
|
|
266
|
+
|
|
267
|
+
A request body is read up to `limits.maxRequestBytes` decoded bytes; `gzip`, `deflate` and `br` bodies are inflated,
|
|
268
|
+
and any other content coding is the protocol's `415` (`A2011`).
|
|
269
|
+
|
|
270
|
+
## MCP
|
|
271
|
+
|
|
272
|
+
With `mcp` in the configuration, the same `handlers` also serve `@aventara/mcp`'s endpoint and its OAuth
|
|
273
|
+
protected-resource metadata. Add one route file for each path:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
// src/aventara/server.ts
|
|
277
|
+
export const { handlers } = Aventara({
|
|
278
|
+
...aventaraConfig,
|
|
279
|
+
mcp: {
|
|
280
|
+
path: "/mcp",
|
|
281
|
+
authorization: {
|
|
282
|
+
servers: ["https://auth.example.com"],
|
|
283
|
+
verify: async (token) => (await verifyAccessToken(token)) ?? null, // your token check
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
adapter: () => createPrismaAdapter({ ...adapter, client: prisma }),
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
// src/app/mcp/route.ts
|
|
292
|
+
import { handlers } from "@/aventara/server";
|
|
293
|
+
|
|
294
|
+
export const { GET, POST, DELETE } = handlers;
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
// src/app/.well-known/oauth-protected-resource/mcp/route.ts
|
|
299
|
+
import { handlers } from "@/aventara/server";
|
|
300
|
+
|
|
301
|
+
export const { GET } = handlers;
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Install `@aventara/mcp` at the same version as `@aventara/core` (`npm install --save-exact @aventara/mcp@<version>`).
|
|
305
|
+
It is an optional peer, loaded only when `mcp` is configured; when it is missing, the first request fails with a
|
|
306
|
+
sentence that names the install command.
|
|
307
|
+
|
|
308
|
+
On Next.js, `mcp` needs `authorization`. Without it, `@aventara/mcp` answers only callers on the same machine that
|
|
309
|
+
send no `x-forwarded-for`, and Next.js adds `x-forwarded-for` to every request, so every caller would be refused. An
|
|
310
|
+
`mcp` without `authorization` is a type error on the `Aventara({...})` call, and, for a configuration typed wider than its literal, refused when the Framework is built (the first request), with a sentence that says so.
|
|
311
|
+
|
|
312
|
+
Everything the endpoint answers is `@aventara/mcp`'s: the bearer check and its `401` with `WWW-Authenticate`, the
|
|
313
|
+
tools, the tool calls (each one an agent-origin protocol request, with `state` and the token's claims in
|
|
314
|
+
`ctx.$state.mcp.claims`), the `405` on `GET` and `DELETE` (the endpoint is stateless) and the metadata document. The
|
|
315
|
+
request body is read up to the Agent Contract's `maxRequestBytes` and handed on as sent.
|
|
316
|
+
|
|
317
|
+
## Status
|
|
318
|
+
|
|
319
|
+
A prerelease, installed with its tag:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
npm install @aventara/next@rc
|
|
323
|
+
```
|