@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.
- package/CHANGELOG.md +59 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +304 -2
- package/dist/aventara.factory.d.ts +155 -0
- package/dist/aventara.factory.js +61 -0
- package/dist/host-bootstrap.validator.d.ts +38 -0
- package/dist/host-bootstrap.validator.js +68 -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 +20 -0
- package/dist/protocol-route.table.js +9 -0
- package/dist/request-body.reader.d.ts +34 -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 +38 -0
- package/dist/server-operations.factory.js +123 -0
- package/dist/shared-framework.registry.d.ts +25 -0
- package/dist/shared-framework.registry.js +65 -0
- package/dist/universal-path.registry.d.ts +25 -0
- package/dist/universal-path.registry.js +31 -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
|
@@ -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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -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,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,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
|
+
};
|