@aventara/next 0.0.0-stage → 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.
@@ -0,0 +1,61 @@
1
+ import { createFramework, } from "@aventara/core";
2
+ import { checkedOptions } from "./host-bootstrap.validator.js";
3
+ import { loadMcpBinding } from "./mcp-binding.loader.js";
4
+ import { createRequestState } from "./request-state.cache.js";
5
+ import { bindProtocolHost, createRouteHandlers, } from "./route-handlers.factory.js";
6
+ import { createServerOperations, refuseMemberClashes, } from "./server-operations.factory.js";
7
+ import { frameworkSlot, optionsFingerprint, } from "./shared-framework.registry.js";
8
+ import { registerUniversalPath, universalPath, } from "./universal-path.registry.js";
9
+ export function Aventara(options) {
10
+ const checked = checkedOptions(options);
11
+ const { source } = checked;
12
+ const slot = source.kind === "config"
13
+ ? frameworkSlot(String(options.api), optionsFingerprint(options))
14
+ : { fingerprint: "", framework: undefined, built: undefined };
15
+ const held = (created) => {
16
+ refuseMemberClashes(created);
17
+ slot.built = created;
18
+ return created;
19
+ };
20
+ if (source.kind === "framework") {
21
+ try {
22
+ slot.framework = Promise.resolve(held(source.framework));
23
+ }
24
+ catch (error) {
25
+ slot.framework = Promise.reject(error);
26
+ slot.framework.catch(() => undefined);
27
+ }
28
+ }
29
+ const frameworkOf = () => {
30
+ slot.framework ??=
31
+ source.kind === "config"
32
+ ? source
33
+ .config()
34
+ .then((config) => createFramework(config))
35
+ .then(held)
36
+ : Promise.resolve(held(source.framework));
37
+ return slot.framework;
38
+ };
39
+ let protocol;
40
+ const protocolOf = () => {
41
+ protocol ??= frameworkOf().then(async (built) => bindProtocolHost(built, await loadMcpBinding(built)));
42
+ return protocol;
43
+ };
44
+ let readied;
45
+ const requestState = createRequestState(checked.state);
46
+ const universalState = checked.state === undefined
47
+ ? undefined
48
+ : async () => (await requestState()) ?? {};
49
+ const { avClient, avApp } = createServerOperations({ built: frameworkOf, current: () => slot.built }, requestState);
50
+ return {
51
+ handlers: createRouteHandlers(protocolOf, checked.state),
52
+ avClient,
53
+ avApp,
54
+ ready: () => {
55
+ readied ??= protocolOf().then(() => {
56
+ registerUniversalPath(universalPath(protocolOf, universalState));
57
+ });
58
+ return readied;
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,38 @@
1
+ import type { AvConfigAny, AvFramework } from "@aventara/core";
2
+ import type { HostState } from "./route-handlers.factory.js";
3
+ /**
4
+ * Refuses an MCP endpoint without `authorization`: on Next.js it could answer
5
+ * no caller. Checked when the Framework is built, on its resolved `mcp`.
6
+ */
7
+ export declare function refuseOpenMcp(endpoint: {
8
+ readonly authorization?: unknown;
9
+ }): void;
10
+ /** The options, split into what the host keeps and where its Framework comes from. */
11
+ export type CheckedOptions = {
12
+ /**
13
+ * What seeds each request's state: the developer's `state`, or the
14
+ * Framework's access provider's (`access`). Core never sees it.
15
+ */
16
+ readonly state: HostState | undefined;
17
+ /** The Framework's source: one built elsewhere, or a configuration this host builds. */
18
+ readonly source: FrameworkSourceOption;
19
+ };
20
+ /** `{ framework }`: a Framework `createFramework` built; or a configuration for it. */
21
+ export type FrameworkSourceOption = {
22
+ readonly kind: "framework";
23
+ readonly framework: AvFramework;
24
+ } | {
25
+ readonly kind: "config";
26
+ /**
27
+ * The configuration without `state`, and with `adapter` resolved: built
28
+ * on this call when it was given as a function.
29
+ */
30
+ readonly config: () => Promise<AvConfigAny>;
31
+ };
32
+ /**
33
+ * The options as given, split: `state` taken out, and either the given
34
+ * Framework, or the configuration with a function `adapter` deferred to first
35
+ * use, since core refuses a root key it does not know and a function in place
36
+ * of an adapter.
37
+ */
38
+ export declare function checkedOptions(options: unknown): CheckedOptions;
@@ -0,0 +1,68 @@
1
+ import { accessStateOf } from "@aventara/core/internal";
2
+ const NOT_AN_OBJECT = "Aventara: the options are not an object; pass `{ ...aventaraConfig, adapter, state }`, or `{ framework, state }`.";
3
+ const NOT_A_FRAMEWORK = "Aventara: `framework` is not an Aventara Framework (it has no string `api` and no `contracts.client`); pass what `createFramework` resolved to.";
4
+ function bothForms(keys) {
5
+ return `Aventara: the options carry both \`framework\` and configuration keys (${keys.map((key) => `\`${key}\``).join(", ")}); pass \`{ framework, state }\` or \`{ ...aventaraConfig, adapter, state }\`, not both.`;
6
+ }
7
+ const STATE_BESIDE_ACCESS = "Aventara: the options carry both `access` and `state`; `access` seeds each request's state, so remove `state`.";
8
+ const STATE_NOT_A_FUNCTION = "Aventara: `state` is not a function; pass `({ headers, request }) => ({ … })`, called once per request.";
9
+ const MCP_WITHOUT_AUTHORIZATION = "Aventara: `mcp` on Next.js needs `authorization` (`mcp: { path, authorization: { servers, verify } }`). Without it the MCP endpoint answers only loopback callers that send no `x-forwarded-for`, and Next.js adds `x-forwarded-for` to every request, so every caller would be refused.";
10
+ export function refuseOpenMcp(endpoint) {
11
+ if (endpoint.authorization === undefined) {
12
+ throw new Error(MCP_WITHOUT_AUTHORIZATION);
13
+ }
14
+ }
15
+ export function checkedOptions(options) {
16
+ if (typeof options !== "object" || options === null) {
17
+ throw new Error(NOT_AN_OBJECT);
18
+ }
19
+ const { state, ...config } = options;
20
+ if (state !== undefined && typeof state !== "function") {
21
+ throw new Error(STATE_NOT_A_FUNCTION);
22
+ }
23
+ const boundState = state === undefined
24
+ ? undefined
25
+ : state.bind(options);
26
+ const { framework, ...rest } = config;
27
+ if (framework !== undefined) {
28
+ const configured = Object.entries(rest)
29
+ .filter(([, value]) => value !== undefined)
30
+ .map(([key]) => key);
31
+ if (configured.length > 0) {
32
+ throw new Error(bothForms(configured));
33
+ }
34
+ if (!isFramework(framework)) {
35
+ throw new Error(NOT_A_FRAMEWORK);
36
+ }
37
+ return {
38
+ state: oneStateSeed(boundState, accessStateOf(framework)),
39
+ source: { kind: "framework", framework },
40
+ };
41
+ }
42
+ const access = config.access;
43
+ const adapter = config.adapter;
44
+ return {
45
+ state: oneStateSeed(boundState, access === undefined ? undefined : (context) => access.state(context)),
46
+ source: {
47
+ kind: "config",
48
+ config: async () => typeof adapter === "function"
49
+ ? { ...config, adapter: await adapter() }
50
+ : config,
51
+ },
52
+ };
53
+ }
54
+ function oneStateSeed(state, access) {
55
+ if (state !== undefined && access !== undefined) {
56
+ throw new Error(STATE_BESIDE_ACCESS);
57
+ }
58
+ return state ?? access;
59
+ }
60
+ function isFramework(value) {
61
+ if (typeof value !== "object" || value === null) {
62
+ return false;
63
+ }
64
+ const candidate = value;
65
+ return (typeof candidate.api === "string" &&
66
+ typeof candidate.contracts?.client === "object" &&
67
+ candidate.contracts.client !== null);
68
+ }
@@ -0,0 +1,11 @@
1
+ /** The tagged form a dehydrated value takes: `{ [tagKey]: <tag>, [valueKey]: <wire form> }`. */
2
+ export declare const HYDRATION_FORMAT: {
3
+ /** The key a tag is under. */
4
+ readonly tagKey: "$av";
5
+ /** The key the tagged value's wire form is under. */
6
+ readonly valueKey: "v";
7
+ /** The tag of a plain object that holds `tagKey` as data. */
8
+ readonly objectTag: "object";
9
+ /** The scalars a value is tagged as: those whose runtime values JSON does not carry. */
10
+ readonly scalarTags: readonly ["bigint", "bytes", "datetime", "decimal"];
11
+ };
@@ -0,0 +1,6 @@
1
+ export const HYDRATION_FORMAT = {
2
+ tagKey: "$av",
3
+ valueKey: "v",
4
+ objectTag: "object",
5
+ scalarTags: ["bigint", "bytes", "datetime", "decimal"],
6
+ };
@@ -0,0 +1,5 @@
1
+ import "server-only";
2
+ export { Aventara } from "./aventara.factory.js";
3
+ export type { AvNextHandlers, AvNextStateContext, } from "./route-handlers.factory.js";
4
+ export type { AvNextAppTransaction, AvNextClientTransaction, } from "./server-operations.factory.js";
5
+ export { avDehydrate } from "./value.dehydrator.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import "server-only";
2
+ export { Aventara } from "./aventara.factory.js";
3
+ export { avDehydrate } from "./value.dehydrator.js";
@@ -0,0 +1,33 @@
1
+ import type { AvFramework, AvState } from "@aventara/core";
2
+ import type { AvProtocolResponse } from "@aventara/core/protocol";
3
+ /** One request to the MCP endpoint or its metadata, as `@aventara/mcp` reads it. */
4
+ export type McpRequest = {
5
+ readonly method: string;
6
+ /** The request's absolute URL: the OAuth metadata names the endpoint by its origin. */
7
+ readonly url: string;
8
+ readonly headers: Readonly<Record<string, string>>;
9
+ readonly body: Uint8Array;
10
+ /** A route handler never sees the socket. */
11
+ readonly remoteAddress: undefined;
12
+ };
13
+ /** One route the MCP binding lists. */
14
+ export type McpRoute = {
15
+ readonly kind: "endpoint" | "metadata";
16
+ readonly path: string;
17
+ readonly methods: readonly ("GET" | "POST" | "DELETE")[];
18
+ };
19
+ export type McpBinding = {
20
+ readonly surface: () => readonly McpRoute[];
21
+ readonly maxRequestBytes: number;
22
+ readonly handleEndpoint: (request: McpRequest, options?: {
23
+ readonly state?: () => AvState | Promise<AvState>;
24
+ }) => Promise<AvProtocolResponse>;
25
+ readonly encodeMetadata: (request: McpRequest) => AvProtocolResponse;
26
+ };
27
+ /**
28
+ * The MCP binding for `framework`, or `undefined` when its configuration has no
29
+ * `mcp`. Called once, when the Framework is built: an `mcp` without
30
+ * `authorization`, or a missing `@aventara/mcp`, rejects that build with a
31
+ * fixed sentence.
32
+ */
33
+ export declare function loadMcpBinding(framework: AvFramework, specifier?: string): Promise<McpBinding | undefined>;
@@ -0,0 +1,39 @@
1
+ import { refuseOpenMcp } from "./host-bootstrap.validator.js";
2
+ const MCP_PACKAGE = "@aventara/mcp";
3
+ const missing = (specifier, coreVersion) => `Aventara: the configuration has \`mcp\`, and the package that serves it, ${specifier}, is not installed. Install it at @aventara/core's exact version (npm install --save-exact ${specifier}@${coreVersion ?? "<your @aventara/core version>"}), or remove \`mcp\` from the configuration.`;
4
+ async function installedCoreVersion() {
5
+ try {
6
+ const manifest = new URL("../package.json", import.meta.resolve("@aventara/core"));
7
+ const { default: read } = (await import(manifest.href, { with: { type: "json" } }));
8
+ return read.name === "@aventara/core" && typeof read.version === "string"
9
+ ? read.version
10
+ : undefined;
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ function isMissing(error, specifier) {
17
+ const { code, message } = (error ?? {});
18
+ return ((code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") &&
19
+ typeof message === "string" &&
20
+ message.includes(`'${specifier}'`));
21
+ }
22
+ export async function loadMcpBinding(framework, specifier = MCP_PACKAGE) {
23
+ if (framework.mcp === undefined) {
24
+ return undefined;
25
+ }
26
+ refuseOpenMcp(framework.mcp);
27
+ let loaded;
28
+ try {
29
+ loaded = await import(specifier);
30
+ }
31
+ catch (error) {
32
+ if (isMissing(error, specifier)) {
33
+ throw new Error(missing(specifier, await installedCoreVersion()));
34
+ }
35
+ throw error;
36
+ }
37
+ const { AvMcp } = loaded;
38
+ return AvMcp.bind(framework);
39
+ }
@@ -0,0 +1,20 @@
1
+ import type { AvState } from "@aventara/core";
2
+ import type { AvProtocolBinding, AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
3
+ import type { McpBinding, McpRequest, McpRoute } from "./mcp-binding.loader.js";
4
+ /** One route `protocol.surface()` lists. */
5
+ export type SurfaceRoute = ReturnType<AvProtocolBinding["surface"]>[number];
6
+ /** The developer's `state` option over one route request: seeds its `ctx.$state`. */
7
+ export type StateSeed = () => AvState | Promise<AvState>;
8
+ /** What a route answers its request with: a bound protocol member. */
9
+ export type RouteAnswer = (request: AvProtocolRequest, state: StateSeed | undefined) => AvProtocolResponse | Promise<AvProtocolResponse>;
10
+ /** The bound member each kind of surface route is answered by. */
11
+ export type ProtocolRouteTable = {
12
+ readonly [K in SurfaceRoute["kind"]]: (binding: AvProtocolBinding) => RouteAnswer;
13
+ };
14
+ export declare const PROTOCOL_ROUTES: ProtocolRouteTable;
15
+ /** What an MCP route answers its request with: a bound `@aventara/mcp` member. */
16
+ export type McpRouteAnswer = (request: McpRequest, state: StateSeed | undefined) => AvProtocolResponse | Promise<AvProtocolResponse>;
17
+ /** The bound `@aventara/mcp` member each kind of MCP route is answered by. */
18
+ export declare const MCP_ROUTES: {
19
+ readonly [K in McpRoute["kind"]]: (binding: McpBinding) => McpRouteAnswer;
20
+ };
@@ -0,0 +1,9 @@
1
+ export const PROTOCOL_ROUTES = {
2
+ operation: (binding) => (request, state) => binding.handleOperation(request, state === undefined ? {} : { state }),
3
+ contract: (binding) => (request) => binding.encodeContract(request),
4
+ transactions: (binding) => (request, state) => binding.handleTransaction(request, state === undefined ? {} : { state }),
5
+ };
6
+ export const MCP_ROUTES = {
7
+ endpoint: (binding) => (request, state) => binding.handleEndpoint(request, state === undefined ? {} : { state }),
8
+ metadata: (binding) => (request) => binding.encodeMetadata(request),
9
+ };
@@ -0,0 +1,34 @@
1
+ import type { AvProtocolBinding, AvProtocolResponse } from "@aventara/core/protocol";
2
+ /**
3
+ * What the body reader hands on: the stored bytes, an unsupported coding, or a
4
+ * body that does not decode in the coding it names.
5
+ */
6
+ export type RequestBodyRead = {
7
+ readonly kind: "read";
8
+ readonly content: Uint8Array;
9
+ } | {
10
+ readonly kind: "unsupported-coding";
11
+ } | {
12
+ readonly kind: "undecodable";
13
+ };
14
+ /**
15
+ * The answer to a body the reader could not hand on, through the bound
16
+ * `failure` member: an unsupported coding is the protocol's `A2011`; a body
17
+ * that does not decode in its coding is malformed, `A2000`.
18
+ */
19
+ export declare function unreadBodyAnswer(binding: AvProtocolBinding, read: Exclude<RequestBodyRead, {
20
+ readonly kind: "read";
21
+ }>): AvProtocolResponse;
22
+ /**
23
+ * Reads `request`'s body to its end. Its `Content-Encoding` absent, empty or
24
+ * `identity` is no coding; one of `gzip`, `deflate`, `br` (any case) is
25
+ * inflated; anything else, a stacked coding included, is unsupported. A body
26
+ * that does not decode in the coding it names is undecodable. Either way the
27
+ * stream is still read to its end.
28
+ */
29
+ export declare function readRequestBody(request: Request, cap: number): Promise<RequestBodyRead>;
30
+ /** Reads `request`'s body to its end, storing its bytes as sent, up to `cap + 1`. */
31
+ export declare function readRawBody(request: Request, cap: number): Promise<{
32
+ readonly kind: "read";
33
+ readonly content: Uint8Array;
34
+ }>;
@@ -0,0 +1,130 @@
1
+ export function unreadBodyAnswer(binding, read) {
2
+ return read.kind === "unsupported-coding"
3
+ ? binding.failure("A2011")
4
+ : binding.failure("A2000");
5
+ }
6
+ const INFLATERS = {
7
+ gzip: (zlib) => zlib.createGunzip(),
8
+ deflate: (zlib) => zlib.createInflate(),
9
+ br: (zlib) => zlib.createBrotliDecompress(),
10
+ };
11
+ let zlibModule;
12
+ const zlib = () => {
13
+ const specifier = "node:zlib";
14
+ zlibModule ??= import(specifier);
15
+ return zlibModule;
16
+ };
17
+ async function* chunksOf(body) {
18
+ if (body === null) {
19
+ return;
20
+ }
21
+ const reader = body.getReader();
22
+ try {
23
+ for (;;) {
24
+ const { done, value } = await reader.read();
25
+ if (done) {
26
+ return;
27
+ }
28
+ yield value;
29
+ }
30
+ }
31
+ finally {
32
+ reader.releaseLock();
33
+ }
34
+ }
35
+ export async function readRequestBody(request, cap) {
36
+ const source = chunksOf(request.body);
37
+ const coding = codingOf(request.headers.get("content-encoding"));
38
+ if (coding === undefined) {
39
+ for await (const _ of source) {
40
+ }
41
+ return { kind: "unsupported-coding" };
42
+ }
43
+ if (coding === "identity") {
44
+ return storedUpTo(source, cap);
45
+ }
46
+ const stored = boundedStore(cap + 1);
47
+ const inflater = INFLATERS[coding](await zlib());
48
+ const inflated = (async () => {
49
+ for await (const chunk of inflater) {
50
+ if (!stored.add(chunk)) {
51
+ inflater.destroy();
52
+ return;
53
+ }
54
+ }
55
+ })();
56
+ const stopped = inflated.then(() => undefined, () => undefined);
57
+ for await (const chunk of source) {
58
+ if (!inflater.destroyed) {
59
+ await Promise.race([
60
+ new Promise((written) => {
61
+ inflater.write(chunk, () => written());
62
+ }),
63
+ stopped,
64
+ ]);
65
+ }
66
+ }
67
+ if (!inflater.destroyed) {
68
+ inflater.end();
69
+ }
70
+ try {
71
+ await inflated;
72
+ }
73
+ catch {
74
+ return { kind: "undecodable" };
75
+ }
76
+ return { kind: "read", content: stored.content() };
77
+ }
78
+ export function readRawBody(request, cap) {
79
+ return storedUpTo(chunksOf(request.body), cap);
80
+ }
81
+ async function storedUpTo(source, cap) {
82
+ const stored = boundedStore(cap + 1);
83
+ for await (const chunk of source) {
84
+ stored.add(chunk);
85
+ }
86
+ return { kind: "read", content: stored.content() };
87
+ }
88
+ function codingOf(contentEncoding) {
89
+ const named = (contentEncoding ?? "")
90
+ .split(",")
91
+ .map((token) => token.trim().toLowerCase())
92
+ .filter((token) => token !== "");
93
+ if (named.length === 0) {
94
+ return "identity";
95
+ }
96
+ if (named.length > 1) {
97
+ return undefined;
98
+ }
99
+ const [only] = named;
100
+ return only === "identity" ||
101
+ only === "gzip" ||
102
+ only === "deflate" ||
103
+ only === "br"
104
+ ? only
105
+ : undefined;
106
+ }
107
+ function boundedStore(limit) {
108
+ const chunks = [];
109
+ let size = 0;
110
+ return {
111
+ add(chunk) {
112
+ const room = limit - size;
113
+ if (room > 0) {
114
+ const kept = chunk.byteLength <= room ? chunk : chunk.subarray(0, room);
115
+ chunks.push(kept);
116
+ size += kept.byteLength;
117
+ }
118
+ return size < limit;
119
+ },
120
+ content() {
121
+ const bytes = new Uint8Array(size);
122
+ let offset = 0;
123
+ for (const chunk of chunks) {
124
+ bytes.set(chunk, offset);
125
+ offset += chunk.byteLength;
126
+ }
127
+ return bytes;
128
+ },
129
+ };
130
+ }
@@ -0,0 +1,9 @@
1
+ import type { AvState } from "@aventara/core";
2
+ import type { HostState } from "./route-handlers.factory.js";
3
+ /**
4
+ * The request's state for `avClient`: the developer's `state`, handed the
5
+ * headers `next/headers` reads, run once per request. `undefined` without a
6
+ * `state` option, so the call starts from `{}` as on a route, and reads no
7
+ * headers.
8
+ */
9
+ export declare function createRequestState(state: HostState | undefined): () => Promise<AvState | undefined>;
@@ -0,0 +1,8 @@
1
+ import { headers } from "next/headers.js";
2
+ import { cache } from "react";
3
+ export function createRequestState(state) {
4
+ if (state === undefined) {
5
+ return async () => undefined;
6
+ }
7
+ return cache(async () => state({ headers: await headers() }));
8
+ }
@@ -0,0 +1,60 @@
1
+ import type { AvFramework, AvState } from "@aventara/core";
2
+ import { type AvProtocolBinding } from "@aventara/core/protocol";
3
+ import type { McpBinding } from "./mcp-binding.loader.js";
4
+ import { type McpRouteAnswer, type RouteAnswer } from "./protocol-route.table.js";
5
+ /** What `Aventara()`'s `state` option is handed for one request: `{ headers, request? }`. */
6
+ export type AvNextStateContext = {
7
+ /** The request's headers. */
8
+ readonly headers: Headers;
9
+ /** The route handler's request: present on every call through `handlers`. */
10
+ readonly request?: Request;
11
+ };
12
+ /** The developer's `state` option: seeds a request's `ctx.$state`. */
13
+ export type HostState = (context: AvNextStateContext) => AvState | Promise<AvState>;
14
+ /** A Next.js App Router route handler. */
15
+ export type RouteHandler = (request: Request) => Promise<Response>;
16
+ /**
17
+ * `handlers`, from `Aventara()`: the route handlers the catch-all route file
18
+ * under the configured `api` path exports,
19
+ * `export const { GET, POST, PUT, PATCH, DELETE } = handlers;`.
20
+ */
21
+ export type AvNextHandlers = {
22
+ readonly GET: RouteHandler;
23
+ readonly POST: RouteHandler;
24
+ readonly PUT: RouteHandler;
25
+ readonly PATCH: RouteHandler;
26
+ readonly DELETE: RouteHandler;
27
+ };
28
+ /** The protocol bound to one Framework, with its routes keyed for dispatch. */
29
+ export type ProtocolHost = {
30
+ readonly binding: AvProtocolBinding;
31
+ /** The Framework's normalized mount path (`""` or `/seg(/seg)*`). */
32
+ readonly api: string;
33
+ readonly maxRequestBytes: number;
34
+ /** Each `surface()` route's answer, keyed by `METHOD path`. */
35
+ readonly routes: ReadonlyMap<string, RouteAnswer>;
36
+ /** The MCP binding's routes when the configuration has `mcp`. */
37
+ readonly mcp: McpHost | undefined;
38
+ };
39
+ /** The MCP binding's routes, keyed for dispatch. */
40
+ type McpHost = {
41
+ readonly maxRequestBytes: number;
42
+ /** Each MCP route's answer, keyed by its absolute path. */
43
+ readonly routes: ReadonlyMap<string, McpRouteAnswer>;
44
+ };
45
+ /**
46
+ * Binds `framework`'s protocol once: every route `surface()` lists, keyed by
47
+ * method and path, and the MCP binding's routes, when there is one, by path.
48
+ */
49
+ export declare function bindProtocolHost(framework: AvFramework, mcp: McpBinding | undefined): ProtocolHost;
50
+ /** A route's dispatch key: its method and its path under the mount path. */
51
+ export declare const routeKey: (method: string, path: string) => string;
52
+ /**
53
+ * The route handlers over the protocol `host` resolves to. A request for one of
54
+ * the MCP binding's paths is answered by that binding; a request whose method
55
+ * and path are a route of `surface()` by its bound member; any other is the
56
+ * protocol's absence answer, or, for a path outside the protocol's namespace,
57
+ * Next's `notFound()`. Nothing is built until the first request.
58
+ */
59
+ export declare function createRouteHandlers(host: () => Promise<ProtocolHost>, state: HostState | undefined): AvNextHandlers;
60
+ export {};
@@ -0,0 +1,69 @@
1
+ import { AvProtocol } from "@aventara/core/protocol";
2
+ import { notFound } from "next/navigation";
3
+ import { MCP_ROUTES, PROTOCOL_ROUTES, } from "./protocol-route.table.js";
4
+ import { readRawBody, readRequestBody, unreadBodyAnswer, } from "./request-body.reader.js";
5
+ import { mcpRequest, NO_BYTES, pathnameOf, protocolRequest, webResponse, } from "./web-exchange.translator.js";
6
+ export function bindProtocolHost(framework, mcp) {
7
+ const binding = AvProtocol.bind(framework);
8
+ return {
9
+ mcp: mcp === undefined
10
+ ? undefined
11
+ : {
12
+ maxRequestBytes: mcp.maxRequestBytes,
13
+ routes: new Map(mcp
14
+ .surface()
15
+ .map((route) => [route.path, MCP_ROUTES[route.kind](mcp)])),
16
+ },
17
+ binding,
18
+ api: framework.api,
19
+ maxRequestBytes: framework.contracts.client.limits.maxRequestBytes,
20
+ routes: new Map(binding
21
+ .surface()
22
+ .map((route) => [
23
+ routeKey(route.method, route.path),
24
+ PROTOCOL_ROUTES[route.kind](binding),
25
+ ])),
26
+ };
27
+ }
28
+ export const routeKey = (method, path) => `${method} ${path}`;
29
+ function outsideApi(pathname, api) {
30
+ return `Aventara: a request for ${pathname} reached handlers, which serve the configured \`api\` path ${api === "" ? "/" : api}; put the route file at app${api}/[...aventara]/route.ts.`;
31
+ }
32
+ export function createRouteHandlers(host, state) {
33
+ const handle = async (request) => {
34
+ const pathname = pathnameOf(request);
35
+ const { binding, api, maxRequestBytes, routes, mcp } = await host();
36
+ const seed = state === undefined
37
+ ? undefined
38
+ : () => state({ headers: request.headers, request });
39
+ const mcpAnswer = mcp?.routes.get(pathname);
40
+ if (mcp !== undefined && mcpAnswer !== undefined) {
41
+ const body = await readRawBody(request, mcp.maxRequestBytes);
42
+ return webResponse(await mcpAnswer(mcpRequest(request, body.content), seed));
43
+ }
44
+ if (pathname !== api && !pathname.startsWith(`${api}/`)) {
45
+ throw new Error(outsideApi(pathname, api));
46
+ }
47
+ const path = pathname.slice(api.length);
48
+ const answer = routes.get(routeKey(request.method, path));
49
+ if (answer === undefined) {
50
+ const absent = binding.answerAbsent(protocolRequest(request, path, NO_BYTES));
51
+ if (absent === undefined) {
52
+ notFound();
53
+ }
54
+ return webResponse(absent);
55
+ }
56
+ const body = await readRequestBody(request, maxRequestBytes);
57
+ if (body.kind !== "read") {
58
+ return webResponse(unreadBodyAnswer(binding, body));
59
+ }
60
+ return webResponse(await answer(protocolRequest(request, path, body.content), seed));
61
+ };
62
+ return Object.freeze({
63
+ GET: handle,
64
+ POST: handle,
65
+ PUT: handle,
66
+ PATCH: handle,
67
+ DELETE: handle,
68
+ });
69
+ }
@@ -0,0 +1,38 @@
1
+ import { type AvExecutionOptions, type AvFramework, type AvOperation, type AvState, type AvTransactionResults } from "@aventara/core";
2
+ /**
3
+ * `avClient.transaction`: runs `avClient.tx` handles as one transaction, with
4
+ * this request's state. Client-layer handles only: an `avApp.appTx` handle in
5
+ * the list is a type error, and refused at runtime (`A2004`/`V1020`).
6
+ */
7
+ export type AvNextClientTransaction = <const Ops extends readonly AvOperation<unknown, "client">[]>(operations: Ops, options?: AvExecutionOptions) => Promise<AvTransactionResults<Ops>>;
8
+ /**
9
+ * `avApp.transaction`: runs `avApp.appTx` handles as one transaction.
10
+ * Application handles only: an `avClient.tx` handle in the list is a type
11
+ * error, and refused at runtime (`A2004`/`V1020`).
12
+ */
13
+ export type AvNextAppTransaction = <const Ops extends readonly AvOperation<unknown, "application">[]>(operations: Ops, options?: AvExecutionOptions) => Promise<AvTransactionResults<Ops>>;
14
+ /** Where the members find their Framework: built on first use, once. */
15
+ export type FrameworkSource = {
16
+ /** The Framework, built on the first call; a construction error is kept. */
17
+ readonly built: () => Promise<AvFramework>;
18
+ /** The Framework when it is already built, else `undefined`. */
19
+ readonly current: () => AvFramework | undefined;
20
+ };
21
+ /**
22
+ * Refuses a Framework one of whose Resources, in the Contract of the layer
23
+ * `avClient` or `avApp` serves (client, application), is named like that
24
+ * tree's own member (`tx`, `transaction`, `appTx`).
25
+ *
26
+ * @throws {@link AvConstructionError} naming each clash.
27
+ */
28
+ export declare function refuseMemberClashes(framework: AvFramework): void;
29
+ /**
30
+ * `avClient` and `avApp` over the Framework `source` builds. Each call awaits
31
+ * the Framework, then runs the Framework's own member: `avClient`'s with the
32
+ * request's state from `requestState` (a `state` the call names wins),
33
+ * `avApp`'s as given.
34
+ */
35
+ export declare function createServerOperations(source: FrameworkSource, requestState: () => Promise<AvState | undefined>): {
36
+ readonly avClient: object;
37
+ readonly avApp: object;
38
+ };