@aotter/mantle 0.2.0-alpha.10 → 0.2.0-alpha.11
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/dist/auth/adapter.js +1 -28
- package/dist/auth/admin.js +2 -5
- package/dist/auth/caller.js +0 -27
- package/dist/auth/consent.js +100 -0
- package/dist/auth/hardening.js +0 -7
- package/dist/auth/identity.js +0 -11
- package/dist/auth/oauth.js +0 -32
- package/dist/auth/resource-seeds.js +0 -2
- package/dist/auth/route-http.js +33 -0
- package/dist/auth/routes.js +5 -151
- package/dist/build/compiler.d.ts +12 -611
- package/dist/build/compiler.js +3 -7029
- package/dist/bun/assets.js +0 -2
- package/dist/domain/adminOrder.js +0 -2
- package/dist/domain/columns.js +0 -15
- package/dist/domain/describe/chunk-SGMRVDGE.js +1 -0
- package/dist/domain/describe/index.js +1 -115
- package/dist/domain/describe/target-hash.js +1 -12
- package/dist/domain/entry.js +0 -14
- package/dist/domain/generated-tools.js +0 -1
- package/dist/domain/input.js +0 -2
- package/dist/domain/paging.js +0 -17
- package/dist/domain/recipes.js +0 -2
- package/dist/domain/userReferences.js +0 -1
- package/dist/domain/wireScalars.js +0 -6
- package/dist/infrastructure/adminEntries.js +9 -6
- package/dist/infrastructure/adminPaging.js +0 -2
- package/dist/infrastructure/artifacts.js +0 -4
- package/dist/infrastructure/authSchema.js +0 -9
- package/dist/infrastructure/bundle.js +0 -29
- package/dist/infrastructure/cli.js +0 -7
- package/dist/infrastructure/generate.js +0 -14
- package/dist/infrastructure/generateAuth.js +1 -9
- package/dist/infrastructure/help.js +0 -5
- package/dist/infrastructure/init.js +0 -4
- package/dist/infrastructure/inputs.js +0 -9
- package/dist/infrastructure/mounts.js +0 -3
- package/dist/infrastructure/wrangler.js +0 -6
- package/dist/kernel/authorization.js +0 -1
- package/dist/kernel/binds.js +1 -11
- package/dist/kernel/caller-identity.d.ts +0 -3
- package/dist/kernel/caller-identity.js +0 -15
- package/dist/kernel/caller-mode.d.ts +4 -0
- package/dist/kernel/caller-mode.js +14 -0
- package/dist/kernel/contracts.d.ts +1 -9
- package/dist/kernel/contracts.js +1 -42
- package/dist/kernel/cron.js +0 -4
- package/dist/kernel/dialect/common.js +0 -3
- package/dist/kernel/dialect/postgres.js +0 -2
- package/dist/kernel/dialect/sqlite.js +1 -6
- package/dist/kernel/drain.js +2 -8
- package/dist/kernel/driver/bun-sqlite.js +0 -4
- package/dist/kernel/driver/d1.js +0 -1
- package/dist/kernel/driver/postgres.js +0 -15
- package/dist/kernel/driver/transaction-context.js +0 -1
- package/dist/kernel/email.js +0 -4
- package/dist/kernel/host-access.js +2 -3
- package/dist/kernel/host-caller.d.ts +1 -1
- package/dist/kernel/host-caller.js +3 -6
- package/dist/kernel/json-body.js +0 -2
- package/dist/kernel/mcp-apps.js +0 -5
- package/dist/kernel/mcp-call.js +0 -1
- package/dist/kernel/mcp-handler.js +2 -26
- package/dist/kernel/mcp.js +0 -5
- package/dist/kernel/media.js +0 -18
- package/dist/kernel/paging.js +1 -9
- package/dist/kernel/publishing.js +1 -2
- package/dist/kernel/r2-media.js +0 -7
- package/dist/kernel/rest-dispatch.js +0 -1
- package/dist/kernel/rest-router.js +2 -3
- package/dist/kernel/rest.js +0 -3
- package/dist/kernel/scalars.js +1 -9
- package/dist/kernel/search.js +0 -1
- package/dist/kernel/site.js +0 -2
- package/dist/kernel/store.js +1 -26
- package/dist/kernel/surface-failure.d.ts +10 -0
- package/dist/kernel/surface-failure.js +29 -0
- package/dist/kernel/surface.js +0 -3
- package/dist/kernel/sweep.js +0 -1
- package/dist/kernel/translations.js +0 -1
- package/dist/kernel/user-caller.js +0 -6
- package/dist/kernel/user-fields.d.ts +1 -1
- package/dist/kernel/user-fields.js +1 -11
- package/dist/kernel/validation.js +0 -8
- package/dist/kernel/web-gateway.js +0 -2
- package/dist/kernel/web-head.js +0 -1
- package/dist/kernel/web-index.js +0 -1
- package/dist/kernel/web-response.js +0 -1
- package/dist/kernel/web-serve.js +97 -0
- package/dist/kernel/web.d.ts +1 -1
- package/dist/kernel/web.js +10 -91
- package/dist/kernel/where.js +0 -6
- package/dist/usecase/adminBindings.js +28 -21
- package/dist/usecase/adminClient.js +0 -21
- package/dist/usecase/adminDescriptors.js +2 -9
- package/dist/usecase/generate.js +0 -6
- package/dist/usecase/handlers.js +0 -3
- package/dist/usecase/mcp-media.js +0 -1
- package/dist/usecase/mcp.js +0 -4
- package/dist/usecase/paging.js +0 -2
- package/dist/usecase/publishing.js +0 -4
- package/dist/usecase/rest.js +0 -1
- package/dist/usecase/site.js +0 -1
- package/dist/usecase/stores.js +1 -13
- package/dist/usecase/tool-version.js +0 -3
- package/dist/usecase/translations.js +0 -2
- package/dist/usecase/validation.js +0 -20
- package/docs/examples/README.md +1 -1
- package/docs/examples/app-shell/package.json +3 -3
- package/docs/examples/app-shell/src/routes.ts +3 -1
- package/docs/examples/app-shell/src/service.ts +2 -1
- package/docs/handbook/cloudflare/service-entry.md +3 -0
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/agent-setup.md +4 -4
- package/docs/handbook/guides/public-pages.md +6 -0
- package/docs/handbook/reference/admin-descriptors.md +4 -1
- package/docs/handbook/reference/schema.md +1 -1
- package/docs/handbook/releases/index.md +3 -3
- package/package.json +2 -2
- package/skills/README.md +1 -1
- package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
package/dist/kernel/contracts.js
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
/** Boundary primitives only. No plan, SQL interpreter or resource ownership. */
|
|
2
1
|
import { ContractError } from "./contract-error.js";
|
|
3
2
|
export { ContractError } from "./contract-error.js";
|
|
4
3
|
import { hasRole, isAdminCaller as isAdmin } from "./caller-identity.js";
|
|
5
|
-
/** Gateway mode or the one execution prop carrying forwarded authority. */
|
|
6
4
|
export const forwarded = (ctx, gateway) => !!(gateway || ctx?.props && Object.hasOwn(ctx.props, "mantle"));
|
|
7
|
-
/** Privilege never follows a member role name. Only the original verified identity carries the admin layer. */
|
|
8
5
|
export { isAdmin };
|
|
9
|
-
/** Both authenticated layers pass ctx.user; the host layer still needs its original brand. */
|
|
10
6
|
export const isUser = (caller) => caller.kind === "user" || isAdmin(caller);
|
|
11
7
|
export const hasAdminRole = (caller, name) => isAdmin(caller) && hasRole(caller, name);
|
|
12
|
-
/** Only the core transport failures; other codes carry their own status, and unknown codes remain server errors. */
|
|
13
8
|
export const statusOf = (code) => ({ __proto__: null,
|
|
14
9
|
INPUT_VALIDATION_FAILED: 400, UNAUTHENTICATED: 401, AUTH_DENIED: 403, NOT_FOUND: 404,
|
|
15
10
|
METHOD_NOT_ALLOWED: 405, CONFLICT: 409, OUTCOME_UNKNOWN: 503,
|
|
16
11
|
}[code] ?? 500);
|
|
17
|
-
/** Redact native errors at the selected boundary; the bare query preserves their identity. */
|
|
18
12
|
export function wire(error) {
|
|
19
13
|
const status = error instanceof ContractError ? error.status ?? statusOf(error.code) : 500;
|
|
20
14
|
if (status !== 500) {
|
|
@@ -46,52 +40,17 @@ export function wireRows(rows, fields = []) {
|
|
|
46
40
|
return result;
|
|
47
41
|
});
|
|
48
42
|
}
|
|
49
|
-
/** Deny unless `allowed`: a user is AUTH_DENIED, anyone else UNAUTHENTICATED. */
|
|
50
43
|
export function deny(caller, allowed, path) {
|
|
51
44
|
if (!allowed)
|
|
52
45
|
throw new ContractError("subject" in caller ? "AUTH_DENIED" : "UNAUTHENTICATED", "Authorization predicate not satisfied.", path);
|
|
53
46
|
}
|
|
54
|
-
/** A row statement that changed no row cannot tell why without leaking whether the row exists, so it says how to recover. */
|
|
55
47
|
export const ROW_CONFLICT = "No row matched this change: it does not exist or is not visible to you, it changed since it was read, or its state no longer allows this change. Read it again before deciding whether to retry.";
|
|
56
|
-
/** CONFLICT for a write the database refused: a unique-index clash (`fields.kind` "unique"), or a row statement that did not change exactly one row ("row"). */
|
|
57
48
|
export const conflict = (path, unique, cause) => new ContractError("CONFLICT", unique ? "A unique constraint of the Schema was violated." : ROW_CONFLICT, path, { cause, fields: { kind: unique ? "unique" : "row" } });
|
|
58
|
-
/** A Schema `checks` violation (Mantle's SQLite check trigger, or PostgreSQL SQLSTATE 23514) is a CONFLICT naming the check. */
|
|
59
49
|
export const checkConflict = (path, error) => {
|
|
60
50
|
const message = error?.message ?? "", marker = /MANTLE_CHECK ([^:\n]+): (.+)/.exec(message);
|
|
61
51
|
if (!/MANTLE_CHECK /.test(message))
|
|
62
52
|
return undefined;
|
|
63
|
-
// D1 appends its own `: SQLITE_CONSTRAINT (extended: …)`; it stays in `cause`, not in what a client reads.
|
|
64
53
|
return schemaCheckFailed(path, error, marker?.[1], marker?.[2]?.replace(/: SQLITE_CONSTRAINT.*$/, ""));
|
|
65
54
|
};
|
|
66
|
-
/** One message on every target, built from the parsed fields; the driver's own text stays in `cause`. */
|
|
67
55
|
export const schemaCheckFailed = (path, cause, schema, check) => new ContractError("CONFLICT", schema && check ? `A check of the Schema failed: ${schema}: ${check}` : "A check of the Schema failed.", path, { cause, fields: { kind: "check", schema, check } });
|
|
68
|
-
|
|
69
|
-
export function checkOrigin(request, caller) {
|
|
70
|
-
if (!("credential" in caller) || caller.credential !== "session" || ["GET", "HEAD", "OPTIONS"].includes(request.method))
|
|
71
|
-
return;
|
|
72
|
-
const site = request.headers.get("sec-fetch-site"), origin = request.headers.get("origin");
|
|
73
|
-
if ((!site && !origin) || (site && site !== "same-origin" && site !== "none") || (origin && origin !== new URL(request.url).origin))
|
|
74
|
-
throw new ContractError("AUTH_DENIED", "Cross-origin session mutation rejected.", "request:origin");
|
|
75
|
-
}
|
|
76
|
-
/** A resolver's ContractError (e.g. UNAUTHENTICATED for an expired token) keeps its status; anything else is an application failure. */
|
|
77
|
-
export async function surfaceCaller(request, env, resolve, readOnly = false) {
|
|
78
|
-
try {
|
|
79
|
-
return await resolve(request, env, { readOnly });
|
|
80
|
-
}
|
|
81
|
-
catch (cause) {
|
|
82
|
-
if (cause instanceof ContractError)
|
|
83
|
-
throw cause;
|
|
84
|
-
throw new Error("Caller resolution failed.", { cause });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
/** The client sees the redacted wire form; the log gets the surface (a route or tool, never headers) and the original message, stack and cause. */
|
|
88
|
-
export function surfaceError(error, where = "surface") {
|
|
89
|
-
const result = wire(error);
|
|
90
|
-
if (result.status === 500)
|
|
91
|
-
console.error(`[mantle ${where}] unhandled failure`, error);
|
|
92
|
-
return result;
|
|
93
|
-
}
|
|
94
|
-
export function surfaceFailure(error, challenge, where) {
|
|
95
|
-
const result = surfaceError(error, where);
|
|
96
|
-
return Response.json({ error: result.error }, { status: result.status, headers: { "cache-control": "private, no-store", ...(challenge && [401, 403].includes(result.status) && result.error.path !== "request:origin" ? { "www-authenticate": challenge } : {}) } });
|
|
97
|
-
}
|
|
56
|
+
export { checkOrigin, surfaceCaller, surfaceError, surfaceFailure } from "./surface-failure.js";
|
package/dist/kernel/cron.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
/** SDK-owned POSIX to Cloudflare cron conversion, copied verbatim from Core's CloudflareCron so Workers don't pull Core's D1 driver; Core is retired with 0.2.0, so this copy becomes the only one. */
|
|
2
1
|
const PART = /^(\*|\d+|[A-Za-z]{3})(?:-(\d+|[A-Za-z]{3}))?(?:\/(\d+))?$/;
|
|
3
2
|
const MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
|
|
4
3
|
const DAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
|
5
|
-
/** label, lowest, highest, names (by value from `lowest`) */
|
|
6
4
|
const FIELDS = [["minute", 0, 59, null], ["hour", 0, 23, null], ["day of month", 1, 31, null], ["month", 1, 12, MONTHS], ["weekday", 0, 6, DAYS]];
|
|
7
5
|
export function toCloudflareCron(cron) {
|
|
8
6
|
const refuse = (why) => {
|
|
@@ -14,7 +12,6 @@ export function toCloudflareCron(cron) {
|
|
|
14
12
|
const weekday = [];
|
|
15
13
|
fields.forEach((field, i) => {
|
|
16
14
|
const [label, min, max, names] = FIELDS[i];
|
|
17
|
-
// a value as a number: a name counts as its number, and a number must be written plainly (`01` and `1` are one value)
|
|
18
15
|
const value = (token) => {
|
|
19
16
|
if (/^\d+$/.test(token)) {
|
|
20
17
|
if (token.length > 1 && token.startsWith("0"))
|
|
@@ -51,7 +48,6 @@ export function toCloudflareCron(cron) {
|
|
|
51
48
|
if (i === 4)
|
|
52
49
|
weekday.push(...out);
|
|
53
50
|
});
|
|
54
|
-
// POSIX runs on either day when both are restricted; Cloudflare does not document that rule, so only one may be
|
|
55
51
|
if (fields[2] !== "*" && fields[4] !== "*")
|
|
56
52
|
refuse("restrict the day of month or the weekday, not both");
|
|
57
53
|
return [...fields.slice(0, 4), weekday.join(",")].join(" ");
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/** What SQLite and PostgreSQL spell identically, defined once; each dialect module re-exports it. */
|
|
2
|
-
/** A (possibly schema-qualified) name, quoted. */
|
|
3
1
|
export const ident = (name) => name.split(".").map(part => `"${part.replaceAll('"', '""')}"`).join(".");
|
|
4
|
-
/** A literal piece of text for a LIKE pattern: `\`, `%` and `_` match themselves. Pair with `likeEscape`. */
|
|
5
2
|
export const escapeLike = (value) => value.replace(/[\\%_]/g, "\\$&");
|
|
6
3
|
export const likeEscape = " ESCAPE '\\'";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** PostgreSQL: `$n` placeholders, time as timestamptz (ISO text keeps every microsecond). */
|
|
2
1
|
import { isoDate, isoTimestamptz } from "../scalars.js";
|
|
3
2
|
import { searchLike } from "../search.js";
|
|
4
3
|
import { escapeLike, ident, likeEscape } from "./common.js";
|
|
@@ -13,6 +12,5 @@ export const cutoff = (ms, seconds) => new Date(ms - seconds * 1000);
|
|
|
13
12
|
export const timestamp = isoTimestamptz;
|
|
14
13
|
export const date = isoDate;
|
|
15
14
|
export const cast = (placeholder, type) => type === "timestamptz" || type === "date" ? `CAST(CAST(${placeholder} AS text) AS ${type})` : type === "json" ? `CAST(CAST(${placeholder} AS text) AS jsonb)` : placeholder;
|
|
16
|
-
// ponytail: preserve Core's ILIKE scan; add pg_trgm only when measured search latency needs it.
|
|
17
15
|
export const search = (_table, fields, alias, query) => searchLike(fields.map(field => `${alias}.${ident(field)}`), query, "ILIKE");
|
|
18
16
|
export const dialect = { param, ident, escapeLike, likeEscape, like, search, wireTimestamp, wireDate, now, cutoff, timestamp, date, cast, anyOf };
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
import { ContractError } from "../contracts.js";
|
|
2
|
-
/** SQLite and Cloudflare D1: `?n` placeholders, time as microseconds and dates as days since the epoch. */
|
|
3
2
|
import { encodeDate, encodeTimestamptz } from "../scalars.js";
|
|
4
3
|
import { searchLike } from "../search.js";
|
|
5
4
|
import { escapeLike, ident, likeEscape } from "./common.js";
|
|
6
5
|
export { ident, escapeLike, likeEscape };
|
|
7
6
|
export const param = (n) => `?${n}`;
|
|
8
7
|
export const anyOf = (column, placeholder) => `${column} IN (SELECT value FROM json_each(${placeholder}))`;
|
|
9
|
-
export const like = () => "LIKE";
|
|
10
|
-
/** A write that broke a UNIQUE index or the id's UNIQUE (bun:sqlite and D1 word it the same). */
|
|
8
|
+
export const like = () => "LIKE";
|
|
11
9
|
export const unique = (error) => /UNIQUE constraint failed/.test(String(error?.message));
|
|
12
|
-
/** Positive rollback evidence only. Plain/network/I/O errors are deliberately uncertain. */
|
|
13
10
|
export function definitive(error) {
|
|
14
11
|
if (error instanceof ContractError)
|
|
15
12
|
return error.code === "CONFLICT";
|
|
16
13
|
const e = error;
|
|
17
|
-
// Bun before 1.4 omits `code` on statement errors; its SQLiteError still carries the native errno.
|
|
18
14
|
const native = typeof e?.code === "string" ? e.code.startsWith("SQLITE_") : e?.name === "SQLiteError";
|
|
19
15
|
return native && typeof e.errno === "number" && [1, 19, 20, 25].includes(e.errno & 255);
|
|
20
16
|
}
|
|
21
|
-
// floor, not SQLite's truncating `/` and `%`: a time before 1970 is a negative number of microseconds
|
|
22
17
|
export const wireTimestamp = (v) => ["strftime('%Y-%m-%dT%H:%M:%S', ((", v, ") - ((", v, ") % 1000000 + 1000000) % 1000000) / 1000000, 'unixepoch') || '.' || substr('00000' || (((", v, ") % 1000000 + 1000000) % 1000000), -6) || 'Z'"];
|
|
23
18
|
export const wireDate = (v) => ["date((", v, ") * 86400, 'unixepoch')"];
|
|
24
19
|
export const now = (ms) => ms * 1000;
|
package/dist/kernel/drain.js
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
/** Request-time only: no Worker startup path imports this. */
|
|
2
|
-
/** Read and discard what is left of a request body, at most 1 MiB and 1 s. A response sent over an unread POST body makes local
|
|
3
|
-
* `wrangler dev` break the connection, and the next request fails with "Network connection lost". */
|
|
4
1
|
async function drain(request) {
|
|
5
2
|
let reader;
|
|
6
3
|
const timer = setTimeout(() => reader?.cancel().catch(() => { }), 1000);
|
|
7
4
|
try {
|
|
8
|
-
reader = request.body?.getReader();
|
|
5
|
+
reader = request.body?.getReader();
|
|
9
6
|
for (let read = 0, chunk; reader && !(chunk = await reader.read()).done;)
|
|
10
7
|
if ((read += chunk.value.byteLength) > 1024 * 1024) {
|
|
11
8
|
await reader.cancel();
|
|
12
9
|
return;
|
|
13
10
|
}
|
|
14
11
|
}
|
|
15
|
-
catch {
|
|
12
|
+
catch { }
|
|
16
13
|
finally {
|
|
17
14
|
clearTimeout(timer);
|
|
18
15
|
}
|
|
19
16
|
}
|
|
20
|
-
/** A request handler that drains the body once it has answered or thrown. `undefined` and `null` mean the request is not this
|
|
21
|
-
* handler's, so the next handler can still read it. A handler that answers and reads the request later (in `waitUntil`) must
|
|
22
|
-
* read or lock the body before answering. */
|
|
23
17
|
export const draining = (handle) => async (...args) => {
|
|
24
18
|
let response;
|
|
25
19
|
try {
|
|
@@ -4,7 +4,6 @@ export { definitive } from "../dialect/sqlite.js";
|
|
|
4
4
|
import { context, guard } from "./transaction-context.js";
|
|
5
5
|
const transactions = new WeakSet();
|
|
6
6
|
const queues = new WeakMap();
|
|
7
|
-
/** Only queued transactions make otherwise synchronous statements wait; a rejection cannot poison the queue. */
|
|
8
7
|
function enqueue(db, fn) {
|
|
9
8
|
const result = (queues.get(db) ?? Promise.resolve()).then(fn);
|
|
10
9
|
const tail = result.then(() => { }, () => { });
|
|
@@ -15,11 +14,9 @@ function enqueue(db, fn) {
|
|
|
15
14
|
}
|
|
16
15
|
const available = (db, fn) => { guard(db); return queues.has(db) ? enqueue(db, fn) : fn(); };
|
|
17
16
|
export const read = (db, sql, binds) => available(db, () => db.query(sql).all(...binds));
|
|
18
|
-
/** One statement is atomic on its own; several run in a transaction, and a row statement that does not change exactly one row rolls it back. A unique-index clash is CONFLICT. */
|
|
19
17
|
export async function procedure(db, steps, path) {
|
|
20
18
|
const execute = (handle) => steps.map(s => {
|
|
21
19
|
const statement = handle.query(s.sql);
|
|
22
|
-
// a statement with columns (SELECT, RETURNING) gives rows, and their number is its affected count
|
|
23
20
|
let rows = [], changes;
|
|
24
21
|
try {
|
|
25
22
|
rows = statement.columnNames.length ? statement.all(...s.binds) : [];
|
|
@@ -27,7 +24,6 @@ export async function procedure(db, steps, path) {
|
|
|
27
24
|
changes = rows.length;
|
|
28
25
|
else {
|
|
29
26
|
statement.run(...s.binds);
|
|
30
|
-
// Bun's run().changes includes FTS maintenance writes; SQLite changes() counts the authored statement only.
|
|
31
27
|
changes = s.row ? Number(handle.query("SELECT changes() AS n").all()[0].n) : 0;
|
|
32
28
|
}
|
|
33
29
|
}
|
package/dist/kernel/driver/d1.js
CHANGED
|
@@ -2,7 +2,6 @@ import { checkConflict, conflict } from "../contracts.js";
|
|
|
2
2
|
import { unique } from "../dialect/sqlite.js";
|
|
3
3
|
export { definitive } from "../dialect/sqlite.js";
|
|
4
4
|
export const read = async (db, sql, binds) => (await db.prepare(sql).bind(...binds).all()).results;
|
|
5
|
-
/** The assert table's trigger aborts the whole batch with `CONFLICT op=<k>` when statement k did not change exactly one row. */
|
|
6
5
|
export async function procedure(db, steps, path) {
|
|
7
6
|
const batch = [], at = [];
|
|
8
7
|
steps.forEach((s, i) => {
|
|
@@ -7,31 +7,19 @@ export async function run(db, sql, binds) {
|
|
|
7
7
|
const rows = await read(db, sql, binds);
|
|
8
8
|
return { rows, changes: Number(rows.count ?? 0) };
|
|
9
9
|
}
|
|
10
|
-
/** SQLSTATE of a server error: `errno` on Bun.SQL, `code` on postgres.js. */
|
|
11
10
|
const sqlstate = (error) => { const e = error; return typeof e?.errno === "string" ? e.errno : e?.code; };
|
|
12
11
|
const check = (path, e) => schemaCheckFailed(path, e, e.table ?? e.table_name, e.constraint ?? e.constraint_name);
|
|
13
|
-
/** A structured server ErrorResponse proves rollback; missing evidence is uncertain. */
|
|
14
12
|
export function definitive(error) {
|
|
15
13
|
if (error instanceof ContractError)
|
|
16
14
|
return error.code === "CONFLICT";
|
|
17
15
|
const state = sqlstate(error);
|
|
18
16
|
return typeof state === "string" && /^[0-9A-Z]{5}$/.test(state) && typeof error?.severity === "string" && !/^(08|28|53|57|58|XX)/.test(state);
|
|
19
17
|
}
|
|
20
|
-
/**
|
|
21
|
-
* The error to throw when a PostgreSQL write fails where it may already have committed (autocommit, or COMMIT).
|
|
22
|
-
* A server ErrorResponse (SQLSTATE + severity) outside connection/resource classes rolled back; anything else, such as a
|
|
23
|
-
* lost socket, is OUTCOME_UNKNOWN.
|
|
24
|
-
*/
|
|
25
18
|
export function postgresOutcome(error, path) {
|
|
26
19
|
if (definitive(error))
|
|
27
20
|
return error;
|
|
28
21
|
return new ContractError("OUTCOME_UNKNOWN", "The write outcome is unknown; reconcile before retrying.", path, { cause: error });
|
|
29
22
|
}
|
|
30
|
-
/**
|
|
31
|
-
* One statement is atomic on its own (a row statement touches at most one row, so a failed count check changes nothing)
|
|
32
|
-
* and runs in autocommit. Several run in `begin(serializable)`: a failure before the callback returns was rolled back;
|
|
33
|
-
* only a failed COMMIT can leave the outcome unknown.
|
|
34
|
-
*/
|
|
35
23
|
export async function procedure(db, steps, path) {
|
|
36
24
|
const execute = async (handle) => {
|
|
37
25
|
const results = [];
|
|
@@ -55,18 +43,15 @@ export async function procedure(db, steps, path) {
|
|
|
55
43
|
}
|
|
56
44
|
return transaction(db, execute, undefined, path);
|
|
57
45
|
}
|
|
58
|
-
/** Shared BEGIN/COMMIT outcome handling for Procedures and interactive Store transactions. */
|
|
59
46
|
export async function transaction(db, fn, options = {}, path = "store/transaction") {
|
|
60
47
|
guard(db);
|
|
61
48
|
if (transactions.has(db))
|
|
62
49
|
throw new Error("Nested transactions are not supported.");
|
|
63
|
-
// a transaction handle the app opened (postgres.js types it without `begin`) only suits reads and single statements
|
|
64
50
|
if (typeof db.begin !== "function")
|
|
65
51
|
throw new Error("This PostgreSQL handle is a transaction the app opened and cannot begin another: open the transaction with store(pool, caller).transaction and call Procedures through its tx.");
|
|
66
52
|
let committing = false;
|
|
67
53
|
const state = { outer: db };
|
|
68
54
|
try {
|
|
69
|
-
// printed into BEGIN, so only PostgreSQL's own four levels: a handler may pass request input here
|
|
70
55
|
const level = options.isolation ?? "serializable";
|
|
71
56
|
if (!ISOLATION.includes(level))
|
|
72
57
|
throw new ContractError("INPUT_VALIDATION_FAILED", `isolation '${level}' is not a PostgreSQL isolation level`, path);
|
|
@@ -5,7 +5,6 @@ export function guard(db) {
|
|
|
5
5
|
if (context.getStore()?.outer === db)
|
|
6
6
|
throw new ContractError("INPUT_VALIDATION_FAILED", "Inside store.transaction use tx, not the outer store or handle.", "store/transaction");
|
|
7
7
|
}
|
|
8
|
-
/** One atomic write cannot publish a translation and change its parent's status. */
|
|
9
8
|
export function translationWrite(db, schema, parent) {
|
|
10
9
|
const state = context.getStore();
|
|
11
10
|
if (!state || state.handle !== db)
|
package/dist/kernel/email.js
CHANGED
|
@@ -24,10 +24,6 @@ export function adminInvitationEmail(role, signInUrl) {
|
|
|
24
24
|
signInUrl,
|
|
25
25
|
]);
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Local development only: logs recipients and sign-in secrets, sends nothing.
|
|
29
|
-
* Select at the application's environment boundary; never use in production.
|
|
30
|
-
*/
|
|
31
27
|
export class ConsoleEmailSender {
|
|
32
28
|
async send(args) {
|
|
33
29
|
const { to, subject, text, locale, category } = args;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** Request-only gateway decisions and observation; no database or compiler dependencies. */
|
|
2
1
|
import { forwarded, ContractError, wire } from "./contracts.js";
|
|
3
|
-
import { isHostCaller
|
|
2
|
+
import { isHostCaller } from "./caller-identity.js";
|
|
3
|
+
import { checkFresh } from "./caller-mode.js";
|
|
4
4
|
const logged = new WeakSet();
|
|
5
5
|
export function accessRequest(ctx, options) {
|
|
6
6
|
const present = forwarded(ctx);
|
|
@@ -43,7 +43,6 @@ export function accessRequest(ctx, options) {
|
|
|
43
43
|
logged.add(listener);
|
|
44
44
|
console.error("[mantle onAccess] listener failed");
|
|
45
45
|
} };
|
|
46
|
-
// Schedule observation after the decision without awaiting its promise.
|
|
47
46
|
queueMicrotask(() => { try {
|
|
48
47
|
void Promise.resolve(listener(finished)).catch(log);
|
|
49
48
|
}
|
|
@@ -36,4 +36,4 @@ export declare function gatewayCaller(ctx: RequestContext | undefined, gateway:
|
|
|
36
36
|
/** Host errors are machine-readable and never challenge for application credentials. */
|
|
37
37
|
export declare function hostFailure(error: unknown, caller?: Caller, where?: string, challenge?: string): Response;
|
|
38
38
|
export { isHostCaller } from "./caller-identity.js";
|
|
39
|
-
export { checkFresh } from "./caller-
|
|
39
|
+
export { checkFresh } from "./caller-mode.js";
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
/** Request-only host contract validation. */
|
|
2
1
|
export { draining } from "./drain.js";
|
|
3
|
-
import { forwarded, ContractError
|
|
2
|
+
import { forwarded, ContractError } from "./contracts.js";
|
|
3
|
+
import { surfaceError, surfaceFailure } from "./surface-failure.js";
|
|
4
4
|
import { userCaller } from "./user-caller.js";
|
|
5
5
|
import { identities, isHostCaller } from "./caller-identity.js";
|
|
6
6
|
const hostContract = (message, field) => { throw new ContractError("HOST_CONTRACT", message, "", { status: 412, fields: field ? { field } : undefined }); };
|
|
7
|
-
/** Validate the forwarding contract and reuse the ordinary user validation and brand. */
|
|
8
7
|
export function hostCaller(props, options) {
|
|
9
8
|
const record = (v) => !!v && typeof v === "object" && !Array.isArray(v) && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
|
|
10
9
|
const text = (v) => typeof v === "string" && v.length > 0 && !v.includes("\0") && v.isWellFormed();
|
|
@@ -35,7 +34,6 @@ export function hostCaller(props, options) {
|
|
|
35
34
|
identities.set(caller, Object.freeze({ ...identities.get(base), admin: caller.admin, host: true }));
|
|
36
35
|
return caller;
|
|
37
36
|
}
|
|
38
|
-
/** Resolve only forwarded props, or trip on an entry that did not opt in. */
|
|
39
37
|
export function gatewayCaller(ctx, gateway, fields) {
|
|
40
38
|
const present = forwarded(ctx);
|
|
41
39
|
if (!gateway)
|
|
@@ -44,7 +42,6 @@ export function gatewayCaller(ctx, gateway, fields) {
|
|
|
44
42
|
return { kind: 'anonymous' };
|
|
45
43
|
return hostCaller(ctx.props.mantle, { tenant: gateway.tenant, fields });
|
|
46
44
|
}
|
|
47
|
-
/** Host errors are machine-readable and never challenge for application credentials. */
|
|
48
45
|
export function hostFailure(error, caller, where, challenge) {
|
|
49
46
|
if ((!caller || !isHostCaller(caller)) && !(error instanceof ContractError && error.code === 'HOST_CONTRACT'))
|
|
50
47
|
return surfaceFailure(error, challenge, where);
|
|
@@ -53,4 +50,4 @@ export function hostFailure(error, caller, where, challenge) {
|
|
|
53
50
|
return Response.json({ error: { code, ...(typeof result.error.field === 'string' ? { field: result.error.field } : {}), ...(result.error.path ? { target: result.error.path } : {}) } }, { status: result.status === 401 ? 403 : result.status, headers: { 'cache-control': 'private, no-store' } });
|
|
54
51
|
}
|
|
55
52
|
export { isHostCaller } from "./caller-identity.js";
|
|
56
|
-
export { checkFresh } from "./caller-
|
|
53
|
+
export { checkFresh } from "./caller-mode.js";
|
package/dist/kernel/json-body.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Request-only JSON reader: bound bytes before copying or parsing, independent of UTF-8 chunk boundaries. */
|
|
2
|
-
/** Bound raw bytes before decoding a request body. */
|
|
3
1
|
export async function bytes(request, limit, fail) {
|
|
4
2
|
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
5
3
|
fail("Body limit must be a positive safe integer.");
|
package/dist/kernel/mcp-apps.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { ContractError } from "./contracts.js";
|
|
2
|
-
/** MCP Apps (ADR-0029 D7): UI resources served beside the tools that render in them. */
|
|
3
2
|
export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
4
|
-
/** Flatten contributors; `mcp(...)` reads static resources getters once at construction. */
|
|
5
3
|
export const resourcesOf = (apps) => ({ resources: (Array.isArray(apps) ? apps : [apps]).flatMap(app => app.resources) });
|
|
6
|
-
/** Static contributors are checked at construction; resolved factories are checked per request. A request-bound tool table defers tool-link checks until its first request. */
|
|
7
4
|
export function linkApps(apps, tools, views) {
|
|
8
5
|
const resources = resourcesOf(apps).resources;
|
|
9
6
|
const uris = new Set(), names = new Set(), renders = new Map(), appOnly = new Set();
|
|
@@ -24,7 +21,6 @@ export function linkApps(apps, tools, views) {
|
|
|
24
21
|
refuse(`Tool '${tool}' is claimed by two MCP App resources.`);
|
|
25
22
|
renders.set(tool, resource.uri);
|
|
26
23
|
}
|
|
27
|
-
// an App acts for the user without the model seeing the call, so only a read may be hidden from the model
|
|
28
24
|
for (const tool of resource.appOnly ?? []) {
|
|
29
25
|
if (views && !views.has(tool))
|
|
30
26
|
refuse(`App-only tool '${tool}' must be a View this endpoint serves.`);
|
|
@@ -33,7 +29,6 @@ export function linkApps(apps, tools, views) {
|
|
|
33
29
|
}
|
|
34
30
|
return { resources, renders, appOnly };
|
|
35
31
|
}
|
|
36
|
-
/** One endpoint's Apps bound to its tool table; `previous` is reused while the table's names are unchanged. */
|
|
37
32
|
export function linkSurface(apps, selected, previous) {
|
|
38
33
|
const key = JSON.stringify(selected.map(tool => [tool.name, !!tool.page]).sort(([a], [b]) => a.localeCompare(b)));
|
|
39
34
|
return previous?.key === key ? previous : { key, links: linkApps(apps, new Set(selected.map(tool => tool.name)), new Set(selected.filter(tool => tool.page).map(tool => tool.name))) };
|
package/dist/kernel/mcp-call.js
CHANGED
|
@@ -3,7 +3,6 @@ import { checkPageLimit } from "./paging.js";
|
|
|
3
3
|
export async function callTool(tool, raw, db, caller, handlers) {
|
|
4
4
|
return callToolLazy(tool, raw, () => db, caller, handlers);
|
|
5
5
|
}
|
|
6
|
-
/** Transport input is checked before acquiring an application-owned native handle. */
|
|
7
6
|
export async function callToolLazy(tool, raw, getDb, caller, handlers, dryRun) {
|
|
8
7
|
if (tool.page) {
|
|
9
8
|
const { limit, cursor, ...input } = raw;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** Request-only MCP implementation, excluded from Worker startup. */
|
|
2
1
|
import { authorized, authorizeTarget } from "./authorization.js";
|
|
3
|
-
import { forwarded, isAdmin, ContractError,
|
|
2
|
+
import { forwarded, isAdmin, ContractError, wire } from "./contracts.js";
|
|
3
|
+
import { checkOrigin, surfaceCaller, surfaceError } from "./surface-failure.js";
|
|
4
4
|
import { draining } from "./drain.js";
|
|
5
5
|
import { callToolLazy as callTool } from "./mcp-call.js";
|
|
6
6
|
import { RESOURCE_MIME_TYPE, linkSurface, resourcesOf } from "./mcp-apps.js";
|
|
@@ -19,15 +19,8 @@ function observe(listener, event) {
|
|
|
19
19
|
catch { }
|
|
20
20
|
}
|
|
21
21
|
const paging = (schema) => ({ ...schema, properties: { ...schema.properties, limit: { type: "integer", minimum: 1, maximum: 500 }, cursor: { type: "string" } } });
|
|
22
|
-
/**
|
|
23
|
-
* Characters of `JSON.stringify(page)` an MCP View page may hold. A result carries its rows twice (text block and
|
|
24
|
-
* `structuredContent`) and hosts are reported to truncate near 150k characters, so one copy stays at 60k.
|
|
25
|
-
* ponytail: one fixed number; raise it or make it an option if hosts publish their limits.
|
|
26
|
-
*/
|
|
27
22
|
const MCP_PAGE_BUDGET = 60_000;
|
|
28
|
-
/** The scopes a challenge asks for: the credential's, plus what the request needs. */
|
|
29
23
|
const union = (scopes, extra) => [...new Set([...scopes, ...extra])].join(" ");
|
|
30
|
-
/** The handler for `/mcp` (public tools) and `/mcp/admin` (tools for `admin` roles); other paths are undefined. */
|
|
31
24
|
export function handler(tools, options, apps = {}, reads = []) {
|
|
32
25
|
const allowed = options.allowedOrigins ?? [];
|
|
33
26
|
return draining(async (request, env, ctx) => {
|
|
@@ -38,8 +31,6 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
38
31
|
const url = new URL(request.url), path = url.pathname.replace(/\/+$/, ""), surface = path === "/mcp" ? "public" : path === "/mcp/admin" ? "admin" : undefined;
|
|
39
32
|
if (!surface)
|
|
40
33
|
return;
|
|
41
|
-
// MCP transport MUST: a present Origin is a browser, answered only for loopback or `allowedOrigins`. The request's own
|
|
42
|
-
// host is not implied, since a DNS-rebinding page sends Origin == its rebound Host; a missing Origin is a server client.
|
|
43
34
|
if (request.headers.has("origin")) {
|
|
44
35
|
const sdk = await import("@modelcontextprotocol/server");
|
|
45
36
|
const foreign = sdk.originValidationResponse(request, [...sdk.localhostAllowedOrigins(), ...allowed]);
|
|
@@ -51,7 +42,6 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
51
42
|
}
|
|
52
43
|
}
|
|
53
44
|
const select = async () => (typeof tools === "function" ? await tools(request, env) : tools).filter(tool => tool.surface === surface);
|
|
54
|
-
// Dynamic admin tables stay behind the admin gate; cache classification uses generated metadata only.
|
|
55
45
|
let selected = surface === "public" ? await select() : [];
|
|
56
46
|
let parsedBody, tooLarge = false;
|
|
57
47
|
const cookieOnly = request.headers.has("cookie") && !request.headers.has("authorization");
|
|
@@ -120,7 +110,6 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
120
110
|
}
|
|
121
111
|
if (surface === "admin")
|
|
122
112
|
selected = await select();
|
|
123
|
-
// Before per-caller filtering, so every caller of a deploy sees one catalogue version.
|
|
124
113
|
const version = catalogueVersion(selected);
|
|
125
114
|
const { McpServer, ProtocolError, ProtocolErrorCode, createMcpHandler, fromJsonSchema } = await import("@modelcontextprotocol/server");
|
|
126
115
|
if (request.method === "POST" && !cookieOnly && !options.gateway)
|
|
@@ -176,7 +165,6 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
176
165
|
const sources = (Array.isArray(app) ? app : [app]);
|
|
177
166
|
const dynamic = sources.some(source => typeof source === "function");
|
|
178
167
|
const resolved = resourcesOf(await Promise.all(sources.map(source => typeof source === "function" ? source({ env, request }) : source)));
|
|
179
|
-
// Equal tool names say nothing about request-bound resource identity or ownership.
|
|
180
168
|
const current = linkSurface(resolved, selected, dynamic ? undefined : apps[surface]);
|
|
181
169
|
if (!dynamic)
|
|
182
170
|
apps[surface] = current;
|
|
@@ -193,7 +181,6 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
193
181
|
tool.check?.(caller);
|
|
194
182
|
await authorizeTarget(options.authorize, tool.target ?? { kind: tool.page ? "view" : "procedure", name: tool.name }, caller);
|
|
195
183
|
}
|
|
196
|
-
// an App's HTML is rendered for the same tools the caller's tools/list shows
|
|
197
184
|
if ((Array.isArray(parsedBody) ? parsedBody : [parsedBody]).some(message => message?.method === "tools/list" || message?.method === "resources/read")) {
|
|
198
185
|
const visible = [];
|
|
199
186
|
for (const tool of selected) {
|
|
@@ -218,17 +205,13 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
218
205
|
}
|
|
219
206
|
}
|
|
220
207
|
const serverInfo = { name: "aotter.mantle", ...options.serverInfo, version: options.serverInfo?.version ?? version };
|
|
221
|
-
// Host calls execute in the SDK callback, after transport validation.
|
|
222
208
|
const handler = createMcpHandler(() => {
|
|
223
209
|
const schema = (value) => fromJsonSchema(value, { getValidator() { return (input) => ({ valid: true, data: input, errorMessage: undefined }); } });
|
|
224
|
-
// Tools capability after construction: given to the constructor, the SDK installs its tools/call handler before the wrap below.
|
|
225
210
|
const server = new McpServer(serverInfo, options.instructions ? { instructions: options.instructions } : {});
|
|
226
211
|
server.server.registerCapabilities({ tools: { listChanged: false } });
|
|
227
212
|
if (host)
|
|
228
213
|
server.server.onclose = () => access?.finish(isRecord(parsedBody) && parsedBody.method === "initialize" ? undefined : new ContractError("INPUT_VALIDATION_FAILED", "MCP request refused.", "mcp", { status: 400 }));
|
|
229
214
|
const appMeta = links ? registerApps(server, links, parsedBody, new Set(selected.map(tool => tool.name)), { env, request }) : undefined;
|
|
230
|
-
// A host's stale copy names a tool this catalogue lacks: say so, with the catalogue version, instead of the SDK's bare "not found".
|
|
231
|
-
// Wrapped where the SDK stores it, so transport and request validation still answer first.
|
|
232
215
|
const registered = new Set(), set = server.server.setRequestHandler.bind(server.server);
|
|
233
216
|
server.server.setRequestHandler = ((method, handler) => set(method, method === "tools/call" && typeof handler === "function" ? (request, context) => {
|
|
234
217
|
if (!registered.has(request.params.name))
|
|
@@ -240,7 +223,6 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
240
223
|
if (meta === false)
|
|
241
224
|
continue;
|
|
242
225
|
registered.add(tool.name);
|
|
243
|
-
// Not part of structured output: the version, and on failure the current input contract.
|
|
244
226
|
const about = (contract) => { const text = [tool.version && `tool version ${tool.version}`, contract && `input schema ${JSON.stringify(tool.page ? paging(tool.inputSchema) : tool.inputSchema)}`].filter(Boolean).join("; "); return text ? [{ type: "text", text }] : []; };
|
|
245
227
|
server.registerTool(tool.name, { ...(meta ? { _meta: meta } : {}), ...(tool.title ? { title: text(tool.title, locale) } : {}), description: text(tool.description, locale) ?? text(tool.title, locale) ?? tool.name, inputSchema: schema(tool.page ? paging(tool.inputSchema) : tool.inputSchema), ...(tool.outputSchema ? { outputSchema: schema(tool.outputSchema) } : {}), ...(tool.annotations ? { annotations: tool.annotations } : {}) }, async (raw) => {
|
|
246
228
|
const metadata = options.onObservation ? { kind: "invocation", surface, invocationId: crypto.randomUUID(), tool: tool.name, caller: Object.freeze(caller.kind === "user" ? { ...caller, scopes: Object.freeze([...caller.scopes]) } : { ...caller }) } : undefined;
|
|
@@ -256,8 +238,6 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
256
238
|
let db;
|
|
257
239
|
const run = (input) => callTool(tool, input, () => db ??= options.db(env), caller, options.handlers, access?.dryRun ? () => ({ dryRun: true, event: access.finish() }) : undefined);
|
|
258
240
|
let data = await run(raw);
|
|
259
|
-
// A page is cut before the first row that overflows the budget: the same page re-runs with the limit that fits, so the cursor is the View's own.
|
|
260
|
-
// Data can change between reads, so the re-run is measured again. A View without an order is one page and refuses a smaller limit: it stays whole.
|
|
261
241
|
for (let tries = 0; tool.page && isRecord(data) && Array.isArray(data.rows); tries++) {
|
|
262
242
|
const rows = data.rows, total = JSON.stringify(data).length;
|
|
263
243
|
if (total <= MCP_PAGE_BUDGET)
|
|
@@ -299,7 +279,6 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
299
279
|
}
|
|
300
280
|
});
|
|
301
281
|
}
|
|
302
|
-
// An empty catalogue still answers tools/list and refuses calls the same way.
|
|
303
282
|
if (registered.size === 0) {
|
|
304
283
|
server.server.setRequestHandler("tools/list", () => ({ tools: [] }));
|
|
305
284
|
server.server.setRequestHandler("tools/call", () => ({ content: [] }));
|
|
@@ -319,14 +298,12 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
319
298
|
}
|
|
320
299
|
});
|
|
321
300
|
}
|
|
322
|
-
/** A hash of the tools an endpoint serves (`surface:name:version`, sorted), as 32-bit FNV-1a hex: it changes with any tool's definition. */
|
|
323
301
|
function catalogueVersion(tools) {
|
|
324
302
|
const text = JSON.stringify(tools.map(tool => `${tool.surface}:${tool.name}:${tool.version ?? ""}`).sort());
|
|
325
303
|
return [...text].reduce((h, c) => Math.imul(h ^ c.codePointAt(0), 16777619) >>> 0, 2166136261).toString(16).padStart(8, "0");
|
|
326
304
|
}
|
|
327
305
|
const CLIENT_CAPABILITIES_META_KEY = "io.modelcontextprotocol/clientCapabilities";
|
|
328
306
|
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
329
|
-
/** `unknown`: a stateless request after `initialize` carries no capabilities, so only an explicit refusal hides Apps. */
|
|
330
307
|
function clientUiSupport(message) {
|
|
331
308
|
for (const m of Array.isArray(message) ? message : [message]) {
|
|
332
309
|
if (!isRecord(m) || !isRecord(m.params))
|
|
@@ -344,7 +321,6 @@ function appMeta(r, csp = r.csp) {
|
|
|
344
321
|
const ui = { ...(csp ? { csp } : {}), ...(r.permissions ? { permissions: r.permissions } : {}), ...(r.domain !== undefined ? { domain: r.domain } : {}), ...(r.prefersBorder !== undefined ? { prefersBorder: r.prefersBorder } : {}) };
|
|
345
322
|
return Object.keys(ui).length > 0 ? { ui } : undefined;
|
|
346
323
|
}
|
|
347
|
-
/** Registration uses the official SDK; HTML loaders run only on resources/read. */
|
|
348
324
|
function registerApps(server, links, message, tools, request) {
|
|
349
325
|
const enabled = clientUiSupport(message) !== "unsupported";
|
|
350
326
|
if (enabled)
|
package/dist/kernel/mcp.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import { linkApps, linkSurface, resourcesOf } from "./mcp-apps.js";
|
|
2
2
|
export const callTool = async (...args) => (await import("./mcp-call.js")).callTool(...args);
|
|
3
3
|
export const callToolLazy = async (...args) => (await import("./mcp-call.js")).callToolLazy(...args);
|
|
4
|
-
/** The selected MCP routes; all request work loads only when an MCP request arrives. */
|
|
5
4
|
export function mcp(tools, options, reads = []) {
|
|
6
5
|
let serve;
|
|
7
|
-
// Check known static App configuration now; factories wait for the request caller gate.
|
|
8
6
|
const linked = {};
|
|
9
7
|
const apps = {};
|
|
10
8
|
for (const surface of ["public", "admin"]) {
|
|
11
9
|
const declared = options.apps?.[surface];
|
|
12
10
|
if (declared) {
|
|
13
|
-
// Read each static contributor's resources getter once, including mixed arrays.
|
|
14
11
|
const sources = (Array.isArray(declared) ? declared : [declared])
|
|
15
12
|
.map(source => typeof source === "function" ? source : resourcesOf(source));
|
|
16
13
|
const fixed = resourcesOf(sources.filter(source => typeof source !== "function"));
|
|
17
14
|
const dynamic = sources.some(source => typeof source === "function");
|
|
18
15
|
apps[surface] = dynamic ? sources : fixed;
|
|
19
|
-
// Known static conflicts still fail before any request or dynamic factory runs.
|
|
20
16
|
if (typeof tools === "function")
|
|
21
17
|
linkApps(fixed);
|
|
22
18
|
else {
|
|
@@ -35,7 +31,6 @@ export function mcp(tools, options, reads = []) {
|
|
|
35
31
|
return serve(request, env, ctx);
|
|
36
32
|
};
|
|
37
33
|
}
|
|
38
|
-
/** `allowedOrigins` as the hostnames the official SDK compares; a bad entry fails here, not on each request. */
|
|
39
34
|
function originHostnames(origins = []) {
|
|
40
35
|
return Object.freeze(origins.map(origin => {
|
|
41
36
|
let url;
|