@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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { type AvAdapter, type AvAdapterModel, type AvConfig, type AvConfigAny, type AvConfigExact, type AvFramework, type AvState, createFramework } from "@aventara/core";
|
|
2
|
+
import { type AvNextHandlers, type AvNextStateContext } from "./route-handlers.factory.js";
|
|
3
|
+
import { type AvNextAppTransaction, type AvNextClientTransaction } from "./server-operations.factory.js";
|
|
4
|
+
/** `Aventara()`'s options: a configuration whose adapter may be built on first use, and `state`. */
|
|
5
|
+
type AventaraOptions<M extends AvAdapterModel, Cfg> = Omit<AvConfig<M, Cfg>, "adapter"> & HostOptions<M>;
|
|
6
|
+
/** The two host keys: an adapter that may be built on first use, and `state`. */
|
|
7
|
+
type HostOptions<M extends AvAdapterModel> = {
|
|
8
|
+
/**
|
|
9
|
+
* The adapter, or a function that builds it (it may return a promise),
|
|
10
|
+
* called once, on the first request.
|
|
11
|
+
*/
|
|
12
|
+
readonly adapter: AvAdapter<M> | (() => AvAdapter<M> | PromiseLike<AvAdapter<M>>);
|
|
13
|
+
/**
|
|
14
|
+
* Seeds a request's `ctx.$state`, once per request, from its headers and,
|
|
15
|
+
* on a route call, the route handler's request. A throw is answered like
|
|
16
|
+
* any developer failure: a thrown `AvError` as its own outcome, anything
|
|
17
|
+
* else as an internal failure. Omitted, the state is `{}`, or what the
|
|
18
|
+
* `access` provider's `state` seeds; not together with `access`.
|
|
19
|
+
*/
|
|
20
|
+
state?(context: AvNextStateContext): AvState | Promise<AvState>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* On Next.js, `mcp` needs `authorization`: Next adds `x-forwarded-for` to
|
|
24
|
+
* every request, so an endpoint without it would refuse every caller. The
|
|
25
|
+
* expected type of an `mcp` written without it; `undefined` otherwise.
|
|
26
|
+
*/
|
|
27
|
+
type McpOnNext<Cfg> = Cfg extends {
|
|
28
|
+
readonly mcp: infer Endpoint;
|
|
29
|
+
} ? [Endpoint] extends [undefined] ? undefined : string extends Endpoint ? undefined : Endpoint extends string ? {
|
|
30
|
+
readonly "On Next.js, `mcp` needs `authorization` ({ path, authorization: { servers, verify } }): Next.js adds x-forwarded-for to every request, so an MCP endpoint without it would refuse every caller.": true;
|
|
31
|
+
} : "authorization" extends keyof Endpoint ? undefined : {
|
|
32
|
+
readonly "On Next.js, `mcp` needs `authorization` ({ path, authorization: { servers, verify } }): Next.js adds x-forwarded-for to every request, so an MCP endpoint without it would refuse every caller.": true;
|
|
33
|
+
readonly path?: unknown;
|
|
34
|
+
} : undefined;
|
|
35
|
+
/** `state` refused beside `access`, by the sentence that says why. */
|
|
36
|
+
type StateBesideAccess<Cfg> = Cfg extends {
|
|
37
|
+
readonly access: object;
|
|
38
|
+
} ? {
|
|
39
|
+
readonly state?: readonly [
|
|
40
|
+
"Error: `access` seeds each request's state; remove `state`"
|
|
41
|
+
];
|
|
42
|
+
} : unknown;
|
|
43
|
+
/** The options as `createFramework` reads them: `state` taken out. */
|
|
44
|
+
type ConfigOf<Cfg> = Cfg extends {
|
|
45
|
+
readonly state: (...context: never) => unknown;
|
|
46
|
+
} ? Omit<Cfg, "state"> : Cfg;
|
|
47
|
+
/** The Framework `Aventara()` builds from its options. */
|
|
48
|
+
type HostedFramework<M extends AvAdapterModel, Cfg> = Awaited<ReturnType<typeof createFramework<M, ConfigOf<Cfg>>>>;
|
|
49
|
+
/**
|
|
50
|
+
* `Aventara({ framework, state })`'s options: a Framework `createFramework`
|
|
51
|
+
* built, and `state`. A configuration key beside `framework` is refused.
|
|
52
|
+
*/
|
|
53
|
+
type FrameworkOptions = {
|
|
54
|
+
/** A Framework built elsewhere: what `createFramework` resolved to. */
|
|
55
|
+
readonly framework: AvFramework;
|
|
56
|
+
/** A configuration's: refused beside `framework`. */
|
|
57
|
+
readonly adapter?: never;
|
|
58
|
+
state?(context: AvNextStateContext): AvState | Promise<AvState>;
|
|
59
|
+
};
|
|
60
|
+
/** The Framework the options carry under `framework`, else the one they configure. */
|
|
61
|
+
type GivenFramework<M extends AvAdapterModel, Cfg> = Cfg extends {
|
|
62
|
+
readonly framework: infer Given extends HostedMembers;
|
|
63
|
+
} ? Given : HostedFramework<M, Cfg>;
|
|
64
|
+
/** The four members of a Framework the host's members are typed from. */
|
|
65
|
+
type HostedMembers = {
|
|
66
|
+
readonly serverClient: unknown;
|
|
67
|
+
readonly serverClientTx: unknown;
|
|
68
|
+
readonly application: unknown;
|
|
69
|
+
readonly appTx: unknown;
|
|
70
|
+
};
|
|
71
|
+
/** What `Aventara()` returns: the members over its Framework. */
|
|
72
|
+
type AventaraHostOf<F extends HostedMembers> = {
|
|
73
|
+
/**
|
|
74
|
+
* `{ GET, POST, PUT, PATCH, DELETE }` for the catch-all route file under the
|
|
75
|
+
* configured `api` path: `export const { GET, POST, PUT, PATCH, DELETE } = handlers;`
|
|
76
|
+
*/
|
|
77
|
+
readonly handlers: AvNextHandlers;
|
|
78
|
+
/**
|
|
79
|
+
* The client layer for Server Components and Server Actions:
|
|
80
|
+
* `avClient.<resource>.<family>.<variant>(args, options?)`, the Framework's
|
|
81
|
+
* `serverClient`. Arguments and results are application values (a `Date`,
|
|
82
|
+
* a `bigint`, an `AvDecimal`); what the client layer hides is absent; the
|
|
83
|
+
* policy checks the principal `state` gives, the `state` run once per
|
|
84
|
+
* request with the headers of `next/headers`. A direct call: no HTTP.
|
|
85
|
+
*
|
|
86
|
+
* `avClient.tx` builds client-layer transaction handles and
|
|
87
|
+
* `avClient.transaction([...])` runs them with the same state.
|
|
88
|
+
*/
|
|
89
|
+
readonly avClient: F["serverClient"] & {
|
|
90
|
+
/**
|
|
91
|
+
* Transaction handles under the client layer's rules, taking application
|
|
92
|
+
* values: `avClient.tx.<resource>.<family>.<variant>(args)`. Built on the
|
|
93
|
+
* built Framework: after `await ready()`.
|
|
94
|
+
*/
|
|
95
|
+
readonly tx: F["serverClientTx"];
|
|
96
|
+
/**
|
|
97
|
+
* Runs `avClient.tx` handles as one transaction with this request's
|
|
98
|
+
* state: `avClient.transaction([h1, h2, …], options?)`.
|
|
99
|
+
*/
|
|
100
|
+
readonly transaction: AvNextClientTransaction;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* The application layer, for trusted server code:
|
|
104
|
+
* `avApp.<resource>.<family>.<variant>(args, options?)`, the Framework's
|
|
105
|
+
* `application`. The policy checks a call only when it names a principal
|
|
106
|
+
* (`{ as }`).
|
|
107
|
+
*
|
|
108
|
+
* `avApp.appTx` builds application transaction handles and
|
|
109
|
+
* `avApp.transaction([...])` runs them.
|
|
110
|
+
*/
|
|
111
|
+
readonly avApp: F["application"] & {
|
|
112
|
+
/**
|
|
113
|
+
* Transaction handles under the application layer's rules:
|
|
114
|
+
* `avApp.appTx.<resource>.<family>.<variant>(args)`, and `avApp.appTx.$sql`.
|
|
115
|
+
* Built on the built Framework: after `await ready()`.
|
|
116
|
+
*/
|
|
117
|
+
readonly appTx: F["appTx"];
|
|
118
|
+
/**
|
|
119
|
+
* Runs `avApp.appTx` handles as one transaction:
|
|
120
|
+
* `avApp.transaction([h1, h2, …], options?)`.
|
|
121
|
+
*/
|
|
122
|
+
readonly transaction: AvNextAppTransaction;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Builds the Framework and its routes, once; rejects with the construction
|
|
126
|
+
* error. `src/instrumentation.ts` awaits it at startup, so a bad
|
|
127
|
+
* configuration fails `next start`. Reads no request: it runs outside one.
|
|
128
|
+
* Once built, it registers the in-process path `@/aventara/universal`'s
|
|
129
|
+
* `avClient` calls on the server, with each request's state.
|
|
130
|
+
*/
|
|
131
|
+
readonly ready: () => Promise<void>;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Hosts an Aventara Framework in a Next.js App Router application. Returns at
|
|
135
|
+
* once and builds nothing: the Framework is created on first use, or by
|
|
136
|
+
* `ready()`, once.
|
|
137
|
+
*
|
|
138
|
+
* ```ts
|
|
139
|
+
* export const { handlers, avClient, avApp, ready } = Aventara({
|
|
140
|
+
* ...aventaraConfig,
|
|
141
|
+
* adapter: () => createPrismaAdapter({ ...adapter, client: prisma }),
|
|
142
|
+
* state: async ({ headers }) => ({ user: await userOf(headers) }),
|
|
143
|
+
* });
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* Or a Framework built elsewhere, as `createFramework` resolved it (with
|
|
147
|
+
* `state` beside it, and no configuration key):
|
|
148
|
+
* `Aventara({ framework, state })`.
|
|
149
|
+
*/
|
|
150
|
+
export declare function Aventara<M extends AvAdapterModel, const Cfg = AvConfigAny>(options: Cfg extends {
|
|
151
|
+
readonly framework: object;
|
|
152
|
+
} ? FrameworkOptions : McpOnNext<Cfg> extends object ? Omit<Cfg, "mcp"> & HostOptions<M> & {
|
|
153
|
+
readonly mcp: McpOnNext<Cfg>;
|
|
154
|
+
} & StateBesideAccess<Cfg> : Cfg & AvConfigExact<M, ConfigOf<Cfg>> & AventaraOptions<M, ConfigOf<Cfg>> & StateBesideAccess<Cfg>): AventaraHostOf<GivenFramework<M, Cfg>>;
|
|
155
|
+
export {};
|
|
@@ -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), optionsFingerprint(options, "source"))
|
|
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, checked.access);
|
|
50
|
+
return {
|
|
51
|
+
handlers: createRouteHandlers(protocolOf, checked.state),
|
|
52
|
+
avClient,
|
|
53
|
+
avApp,
|
|
54
|
+
ready: () => {
|
|
55
|
+
readied ??= protocolOf().then(() => {
|
|
56
|
+
registerUniversalPath(universalPath(protocolOf, universalState), avClient.$access);
|
|
57
|
+
});
|
|
58
|
+
return readied;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AvAccessProvider, 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
|
+
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
|
+
* The Framework's access provider's `client`, known before the Framework
|
|
21
|
+
* is built (`null` for a provider without one), or `undefined` with no
|
|
22
|
+
* `access`: which verbs `avClient.$access` holds.
|
|
23
|
+
*/
|
|
24
|
+
readonly access: AccessClient | undefined;
|
|
25
|
+
};
|
|
26
|
+
/** What `avClient.$access` reads of an access provider: its `client`, or `null`. */
|
|
27
|
+
export type AccessClient = {
|
|
28
|
+
readonly client: AvAccessProvider["client"] | null;
|
|
29
|
+
};
|
|
30
|
+
/** `{ framework }`: a Framework `createFramework` built; or a configuration for it. */
|
|
31
|
+
type FrameworkSourceOption = {
|
|
32
|
+
readonly kind: "framework";
|
|
33
|
+
readonly framework: AvFramework;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: "config";
|
|
36
|
+
/**
|
|
37
|
+
* The configuration without `state`, and with `adapter` resolved: built
|
|
38
|
+
* on this call when it was given as a function.
|
|
39
|
+
*/
|
|
40
|
+
readonly config: () => Promise<AvConfigAny>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The options as given, split: `state` taken out, and either the given
|
|
44
|
+
* Framework, or the configuration with a function `adapter` deferred to first
|
|
45
|
+
* use, since core refuses a root key it does not know and a function in place
|
|
46
|
+
* of an adapter.
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkedOptions(options: unknown): CheckedOptions;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { accessProviderOf } 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
|
+
const provider = accessProviderOf(framework);
|
|
38
|
+
return {
|
|
39
|
+
state: oneStateSeed(boundState, provider?.state),
|
|
40
|
+
source: { kind: "framework", framework },
|
|
41
|
+
access: provider === undefined
|
|
42
|
+
? undefined
|
|
43
|
+
: { client: provider.client ?? null },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const access = config.access;
|
|
47
|
+
const adapter = config.adapter;
|
|
48
|
+
return {
|
|
49
|
+
state: oneStateSeed(boundState, access === undefined ? undefined : (context) => access.state(context)),
|
|
50
|
+
source: {
|
|
51
|
+
kind: "config",
|
|
52
|
+
config: async () => typeof adapter === "function"
|
|
53
|
+
? { ...config, adapter: await adapter() }
|
|
54
|
+
: config,
|
|
55
|
+
},
|
|
56
|
+
access: access === undefined ? undefined : { client: access.client ?? null },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function oneStateSeed(state, access) {
|
|
60
|
+
if (state !== undefined && access !== undefined) {
|
|
61
|
+
throw new Error(STATE_BESIDE_ACCESS);
|
|
62
|
+
}
|
|
63
|
+
return state ?? access;
|
|
64
|
+
}
|
|
65
|
+
function isFramework(value) {
|
|
66
|
+
if (typeof value !== "object" || value === null) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const candidate = value;
|
|
70
|
+
return (typeof candidate.api === "string" &&
|
|
71
|
+
typeof candidate.contracts?.client === "object" &&
|
|
72
|
+
candidate.contracts.client !== null);
|
|
73
|
+
}
|
|
@@ -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,21 @@
|
|
|
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
|
+
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
|
+
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
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
can: (binding) => (request, state) => binding.handleCan(request, state === undefined ? {} : { state }),
|
|
6
|
+
};
|
|
7
|
+
export const MCP_ROUTES = {
|
|
8
|
+
endpoint: (binding) => (request, state) => binding.handleEndpoint(request, state === undefined ? {} : { state }),
|
|
9
|
+
metadata: (binding) => (request) => binding.encodeMetadata(request),
|
|
10
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
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
|
+
}>;
|
|
35
|
+
export {};
|
|
@@ -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>;
|