@aotter/mantle 0.2.0-alpha.9 → 0.2.0
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/README.md +9 -3
- 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 +19 -609
- package/dist/build/compiler.js +3 -6896
- package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
- package/dist/build/spec/domain/model/UserFields.d.ts +1 -0
- package/dist/bun/assets.js +0 -2
- package/dist/domain/adminOrder.js +0 -2
- package/dist/domain/columns.js +7 -20
- 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 +10 -7
- package/dist/domain/userReferences.js +0 -1
- package/dist/domain/wireScalars.js +0 -6
- package/dist/infrastructure/adminEntries.js +10 -7
- package/dist/infrastructure/adminPaging.js +55 -57
- package/dist/infrastructure/artifacts.js +0 -4
- package/dist/infrastructure/authSchema.js +0 -9
- package/dist/infrastructure/bundle.js +2 -30
- package/dist/infrastructure/cli.js +25 -11
- package/dist/infrastructure/generate.js +9 -16
- package/dist/infrastructure/generateAuth.js +1 -9
- package/dist/infrastructure/help.js +55 -0
- package/dist/infrastructure/init.js +50 -0
- package/dist/infrastructure/inputs.js +0 -9
- package/dist/infrastructure/mounts.js +22 -0
- package/dist/infrastructure/wrangler.js +0 -6
- package/dist/kernel/authorization.js +0 -1
- package/dist/kernel/binds.d.ts +16 -4
- package/dist/kernel/binds.js +4 -10
- 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 +6 -10
- package/dist/kernel/contracts.js +5 -41
- 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 +2 -17
- 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 +3 -4
- 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.d.ts +11 -1
- package/dist/kernel/store.js +21 -34
- 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 +2 -8
- package/dist/kernel/user-fields.d.ts +3 -2
- package/dist/kernel/user-fields.js +9 -9
- 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 +30 -21
- package/dist/usecase/adminClient.js +0 -21
- package/dist/usecase/adminDescriptors.js +2 -9
- package/dist/usecase/generate.js +7 -12
- 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 +1 -3
- 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 +4 -14
- 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 +13 -5
- package/docs/examples/app-shell/README.md +38 -7
- package/docs/examples/app-shell/accept.mjs +91 -0
- package/docs/examples/app-shell/gitignore +7 -0
- package/docs/examples/app-shell/package.json +7 -6
- package/docs/examples/app-shell/smoke-kit.mjs +29 -15
- package/docs/examples/app-shell/src/routes.ts +6 -3
- package/docs/examples/app-shell/src/service.ts +2 -1
- package/docs/examples/club-events/README.md +7 -2
- package/docs/examples/club-events/manifests/club-events.yaml +15 -0
- package/docs/examples/club-events/smoke.mjs +7 -1
- package/docs/examples/purchasing/README.md +12 -3
- package/docs/examples/purchasing/manifests/purchasing.yaml +21 -4
- package/docs/examples/purchasing/smoke.mjs +14 -0
- package/docs/examples/recipes/README.md +5 -3
- package/docs/handbook/cloudflare/service-entry.md +3 -0
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/agent-setup.md +15 -10
- package/docs/handbook/guides/agent-workflow-acceptance.md +113 -103
- package/docs/handbook/guides/local-testing.md +40 -0
- package/docs/handbook/guides/native-admin.md +25 -6
- package/docs/handbook/guides/native-execution.md +5 -4
- package/docs/handbook/guides/public-pages.md +44 -4
- package/docs/handbook/guides/sql-patterns.md +266 -0
- package/docs/handbook/guides/typed-queries.md +138 -0
- package/docs/handbook/navigation.json +16 -0
- package/docs/handbook/reference/admin-descriptors.md +4 -1
- package/docs/handbook/reference/authorization.md +1 -1
- package/docs/handbook/reference/diagnostics.md +6 -1
- package/docs/handbook/reference/procedure.md +5 -5
- package/docs/handbook/reference/schema.md +10 -4
- package/docs/handbook/reference/sql.md +64 -0
- package/docs/handbook/reference/surface.md +5 -2
- package/docs/handbook/reference/view.md +11 -3
- package/docs/handbook/releases/index.md +27 -6
- package/docs/handbook/start/agents.md +45 -0
- package/docs/handbook/start/project-and-cli.md +6 -0
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +6 -5
- package/docs/skills/update/SKILL.md +2 -0
- package/package.json +23 -3
- package/skills/README.md +1 -1
- package/skills/mantle/SKILL.md +39 -120
- package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
- package/skills/mantle/references/local-testing.md +0 -20
- package/skills/mantle/references/web.md +0 -41
- package/skills/mantle/references/website.md +0 -51
package/dist/auth/routes.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import { checkOrigin, surfaceFailure
|
|
1
|
+
import { ContractError } from "../kernel/contracts.js";
|
|
2
|
+
import { checkOrigin, surfaceFailure } from "../kernel/surface-failure.js";
|
|
3
3
|
import { draining } from "../kernel/drain.js";
|
|
4
4
|
import { guardAuthAdminRequest } from "./admin.js";
|
|
5
|
+
import { CONSENT, MAX_BODY, json, readBody, text } from "./route-http.js";
|
|
5
6
|
import { resolveCaller } from "./caller.js";
|
|
6
|
-
const PRIVATE = { "cache-control": "private, no-store" };
|
|
7
|
-
const CONSENT = new Set(["GET /oauth/consent", "GET /oauth/consent/data", "POST /oauth/consent", "GET /oauth/consents", "GET /oauth/consents/data", "POST /oauth/consents/revoke"]);
|
|
8
|
-
const MAX_BODY = 1_048_576;
|
|
9
|
-
const text = (body, status) => new Response(body, { status, headers: PRIVATE });
|
|
10
|
-
const json = (body, status = 200) => Response.json(body, { status, headers: PRIVATE });
|
|
11
|
-
const redirect = (location, status = 302) => new Response(null, { status, headers: { location, ...PRIVATE } });
|
|
12
|
-
const tooLarge = () => new ContractError("INPUT_VALIDATION_FAILED", "Request body exceeds 1 MiB.");
|
|
13
7
|
const basePathOf = (options) => (options.basePath ?? "/api/auth").replace(/\/+$/, "");
|
|
14
8
|
const WELL_KNOWN = /^\/\.well-known\/(oauth-authorization-server|oauth-protected-resource)(\/|$)/;
|
|
15
|
-
/** The service's auth entry: a Response for a path it owns, `null` for any other. */
|
|
16
9
|
export function authRoutes(loader, options) {
|
|
17
10
|
const base = basePathOf(options);
|
|
18
11
|
if (options.connectedAppsPage && (!options.connectedAppsPage.startsWith("/") || options.connectedAppsPage.startsWith("//") || /[\\\r\n]/.test(options.connectedAppsPage)))
|
|
@@ -44,127 +37,15 @@ export function authRoutes(loader, options) {
|
|
|
44
37
|
checkOrigin(request, caller);
|
|
45
38
|
if (request.method === "POST" && Number(request.headers.get("content-length")) > MAX_BODY)
|
|
46
39
|
return text("request body too large", 413);
|
|
47
|
-
|
|
40
|
+
const [{ oauthConsents }, { consentSurface }] = await Promise.all([import("./oauth.js"), import("./consent.js")]);
|
|
41
|
+
return await consentSurface(oauthConsents(await loader.get()), options)(request, caller, route);
|
|
48
42
|
}
|
|
49
43
|
catch (error) {
|
|
50
44
|
return surfaceFailure(error, undefined, `auth ${route}`);
|
|
51
45
|
}
|
|
52
46
|
});
|
|
53
47
|
}
|
|
54
|
-
function consentSurface(auth, options) {
|
|
55
|
-
const { listOAuthConsents: list, revokeOAuthConsent: revoke } = auth;
|
|
56
|
-
return async (request, caller, route) => {
|
|
57
|
-
const person = caller.kind === "user" && caller.credential === "session" ? caller.subject : null;
|
|
58
|
-
const locale = /zh[-_]tw/i.test(request.headers.get("accept-language") ?? "") ? "zh-TW" : "en";
|
|
59
|
-
if (route === "GET /oauth/consent/data") {
|
|
60
|
-
if (!person)
|
|
61
|
-
return json({ consent: null }, 401);
|
|
62
|
-
const model = await auth.getOAuthConsentRequest(request).catch(() => null); // an invalid signed query stays secret-free
|
|
63
|
-
return json({ consent: model }, model ? 200 : 400);
|
|
64
|
-
}
|
|
65
|
-
if (route === "GET /oauth/consent") {
|
|
66
|
-
const model = await auth.getOAuthConsentRequest(request).catch(() => null);
|
|
67
|
-
return html(consentHtml(locale, model), model ? 200 : 400);
|
|
68
|
-
}
|
|
69
|
-
if (route === "POST /oauth/consent") {
|
|
70
|
-
if (!person)
|
|
71
|
-
return text("sign in to answer this request", 401);
|
|
72
|
-
const body = await readBody(request);
|
|
73
|
-
const decision = new URLSearchParams(body).get("decision");
|
|
74
|
-
if (decision !== "approve" && decision !== "deny")
|
|
75
|
-
return text("invalid consent decision", 400);
|
|
76
|
-
// the redirect is the one Better Auth validated against the client's registration; nothing in the request names it
|
|
77
|
-
const location = await auth.completeOAuthConsent(new Request(request, { body }), decision === "approve").catch(() => null);
|
|
78
|
-
const target = location && URL.canParse(location) ? new URL(location) : null;
|
|
79
|
-
if (!target || /^(javascript|data|vbscript|blob):$/i.test(target.protocol))
|
|
80
|
-
return text("invalid authorization request", 400);
|
|
81
|
-
// the parsed form, so a browser cannot read `https:\\evil.test` differently from the check
|
|
82
|
-
return redirect(target.href);
|
|
83
|
-
}
|
|
84
|
-
if (!list || !revoke)
|
|
85
|
-
return text("not found", 404);
|
|
86
|
-
if (route === "GET /oauth/consents") {
|
|
87
|
-
if (options.connectedAppsPage)
|
|
88
|
-
return redirect(options.connectedAppsPage);
|
|
89
|
-
if (!person)
|
|
90
|
-
return text("sign in to see your connected apps", 401);
|
|
91
|
-
return html(connectedAppsHtml(locale, await list(person)), 200);
|
|
92
|
-
}
|
|
93
|
-
if (route === "GET /oauth/consents/data") {
|
|
94
|
-
if (!person)
|
|
95
|
-
return json({ consents: [] }, 401);
|
|
96
|
-
return json({ consents: await list(person) });
|
|
97
|
-
}
|
|
98
|
-
if (route === "POST /oauth/consents/revoke") {
|
|
99
|
-
if (!person)
|
|
100
|
-
return text("sign in to revoke a connection", 401);
|
|
101
|
-
const body = await readBody(request);
|
|
102
|
-
const id = new URLSearchParams(body).get("consent_id");
|
|
103
|
-
if (!id)
|
|
104
|
-
return text("invalid consent id", 400);
|
|
105
|
-
if (!await revoke(person, id))
|
|
106
|
-
return text("consent not found", 404);
|
|
107
|
-
return redirect("/oauth/consents", 303);
|
|
108
|
-
}
|
|
109
|
-
return text("not found", 404);
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
// ---- the pages a service without Admin shows: Admin is an optional subpath (ADR-0032 decision 13), OAuth is not tied to it
|
|
113
|
-
/**
|
|
114
|
-
* No `form-action`: browsers check it against every redirect of a form's navigation, so a callback that redirects again
|
|
115
|
-
* (www.cursor.com answers 308 to cursor.com) would hang the approval. The page runs no script and escapes every value.
|
|
116
|
-
*/
|
|
117
|
-
function html(body, status) {
|
|
118
|
-
return new Response(body, {
|
|
119
|
-
status,
|
|
120
|
-
headers: {
|
|
121
|
-
...PRIVATE, "content-type": "text/html; charset=UTF-8", "referrer-policy": "same-origin", "x-content-type-options": "nosniff", "x-frame-options": "DENY",
|
|
122
|
-
"content-security-policy": "default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'",
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
const T = {
|
|
127
|
-
en: {
|
|
128
|
-
lang: "en", title: "Authorize", eyebrow: "Connect an MCP client", heading: (c) => `Connect ${c}?`,
|
|
129
|
-
body: (c) => `${c} will be able to use this site's tools through MCP. What it can view or change is still limited by your account permissions.`,
|
|
130
|
-
returnsTo: "Returns to", scopes: "Requested access", approve: "Connect", deny: "Cancel", invalid: "Invalid authorization request", invalidBody: "Missing or malformed consent payload. Return to your MCP client and try again.",
|
|
131
|
-
apps: "MCP connections", appsBody: "These MCP clients can use this site's tools. Every action is still checked against your current account permissions.", empty: "No MCP clients are connected.", revoke: "Disconnect",
|
|
132
|
-
},
|
|
133
|
-
"zh-TW": {
|
|
134
|
-
lang: "zh-Hant-TW", title: "授權", eyebrow: "連結 MCP 客戶端", heading: (c) => `要連結 ${c} 嗎?`,
|
|
135
|
-
body: (c) => `${c} 將能透過 MCP 使用這個網站提供的工具;它能查看或變更哪些內容,仍會依照你的帳號權限決定。`,
|
|
136
|
-
returnsTo: "完成後返回", scopes: "要求的權限", approve: "連結", deny: "取消", invalid: "無效的授權請求", invalidBody: "缺少或格式錯誤的授權資訊,請返回 MCP 客戶端重試。",
|
|
137
|
-
apps: "MCP 連線", appsBody: "這些 MCP 客戶端可以使用網站提供的工具;每次操作仍會依照你當下的帳號權限檢查。", empty: "目前沒有已連結的 MCP 客戶端。", revoke: "中斷連線",
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
const esc = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
141
|
-
const page = (l, title, main) => `<!doctype html><html lang="${T[l].lang}"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>${title}</title>` +
|
|
142
|
-
`<style>:root{color-scheme:light dark;font:1rem/1.5 system-ui,sans-serif}body{margin:0;padding:2rem 1rem}main{max-width:32rem;margin:10vh auto}code{overflow-wrap:anywhere}button{font:inherit;padding:.5rem 1rem;cursor:pointer}</style></head><body><main>${main}</main></body></html>`;
|
|
143
|
-
function consentHtml(l, model) {
|
|
144
|
-
const t = T[l];
|
|
145
|
-
if (!model)
|
|
146
|
-
return page(l, t.title, `<p>${t.eyebrow}</p><h1>${t.invalid}</h1><p>${t.invalidBody}</p>`);
|
|
147
|
-
const client = esc(model.clientName);
|
|
148
|
-
// where the answer goes and what is asked, so a look-alike client name cannot hide either
|
|
149
|
-
const host = URL.canParse(model.redirectUri) ? new URL(model.redirectUri).host || new URL(model.redirectUri).protocol : model.redirectUri;
|
|
150
|
-
return page(l, t.title, `<p>${t.eyebrow}</p><h1>${t.heading(client)}</h1><p>${t.body(client)}</p>` +
|
|
151
|
-
`<p>${t.returnsTo} <code>${esc(host)}</code></p><p>${t.scopes}</p><ul>${model.scopes.map((sc) => `<li><code>${esc(sc)}</code></li>`).join("")}</ul><form method="post" action="/oauth/consent">` +
|
|
152
|
-
`<input type="hidden" name="oauth_query" value="${esc(model.oauthQuery)}"/><button type="submit" name="decision" value="approve">${t.approve}</button> ` +
|
|
153
|
-
`<button type="submit" name="decision" value="deny">${t.deny}</button></form>`);
|
|
154
|
-
}
|
|
155
|
-
function connectedAppsHtml(l, consents) {
|
|
156
|
-
const t = T[l];
|
|
157
|
-
const apps = consents.length === 0 ? `<p>${t.empty}</p>` : consents.map((c) => `<section><h2>${esc(c.clientName)}</h2><code>${esc(c.clientId)}</code><form method="post" action="/oauth/consents/revoke">` +
|
|
158
|
-
`<input type="hidden" name="consent_id" value="${esc(c.id)}"/><button type="submit">${t.revoke}</button></form></section>`).join("");
|
|
159
|
-
return page(l, t.apps, `<h1>${t.apps}</h1><p>${t.appsBody}</p>${apps}`);
|
|
160
|
-
}
|
|
161
48
|
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
162
|
-
/**
|
|
163
|
-
* A dynamic registration whose every redirect URI is http on a loopback host and that names no application_type is a
|
|
164
|
-
* native app (RFC 8252 7.3): local MCP clients register this way. Better Auth reads the missing type as `web`, which
|
|
165
|
-
* needs https on a non-loopback host, so it would refuse them all. Mark it native, as the client could itself; a
|
|
166
|
-
* registration naming a type, mixing hosts or using any other redirect is passed through unchanged.
|
|
167
|
-
*/
|
|
168
49
|
export async function loopbackClientsAreNative(request) {
|
|
169
50
|
const body = await readBody(request.clone()).then(JSON.parse).catch(error => { if (error instanceof ContractError)
|
|
170
51
|
throw error; return null; });
|
|
@@ -183,33 +64,6 @@ export async function loopbackClientsAreNative(request) {
|
|
|
183
64
|
headers.delete("content-length");
|
|
184
65
|
return new Request(request.url, { method: "POST", headers, body: JSON.stringify({ ...body, application_type: "native" }) });
|
|
185
66
|
}
|
|
186
|
-
/** Bounded streaming read even without Content-Length; the native parser still owns JSON/form semantics. */
|
|
187
|
-
async function readBody(request) {
|
|
188
|
-
if (Number(request.headers.get("content-length")) > MAX_BODY)
|
|
189
|
-
throw tooLarge();
|
|
190
|
-
const reader = request.body?.getReader();
|
|
191
|
-
if (!reader)
|
|
192
|
-
return "";
|
|
193
|
-
const decoder = new TextDecoder();
|
|
194
|
-
let size = 0, text = "";
|
|
195
|
-
try {
|
|
196
|
-
for (;;) {
|
|
197
|
-
const { value, done } = await reader.read();
|
|
198
|
-
if (done)
|
|
199
|
-
return text + decoder.decode();
|
|
200
|
-
size += value.byteLength;
|
|
201
|
-
if (size > MAX_BODY) {
|
|
202
|
-
void reader.cancel();
|
|
203
|
-
throw tooLarge();
|
|
204
|
-
}
|
|
205
|
-
text += decoder.decode(value, { stream: true });
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
finally {
|
|
209
|
-
reader.releaseLock();
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
/** Better Auth and host fetch implementations may merge several Set-Cookie values into one header. */
|
|
213
67
|
export async function normalizeAuthResponseCookies(response) {
|
|
214
68
|
if (!response.headers.has("set-cookie"))
|
|
215
69
|
return response;
|