@aotter/mantle 0.2.0-alpha.8 → 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 +26 -3
- package/dist/auth/adapter.js +10 -34
- 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 +8 -153
- 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 +17 -32
- package/dist/infrastructure/cli.js +25 -11
- package/dist/infrastructure/generate.js +18 -17
- package/dist/infrastructure/generateAuth.js +48 -5
- 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 +18 -9
- package/dist/kernel/authorization.js +0 -1
- package/dist/kernel/binds.d.ts +16 -7
- 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 +7 -11
- package/dist/kernel/contracts.js +8 -44
- package/dist/kernel/cron.js +0 -4
- package/dist/kernel/dialect/common.d.ts +0 -1
- 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 +3 -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 +1 -6
- package/dist/kernel/mcp-call.js +0 -1
- package/dist/kernel/mcp-handler.js +3 -27
- package/dist/kernel/mcp.d.ts +5 -1
- package/dist/kernel/mcp.js +2 -6
- 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 +4 -11
- 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 +39 -0
- 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 -125
- 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 +1 -5
- package/dist/usecase/rest.js +0 -1
- package/dist/usecase/site.js +0 -2
- 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 +8 -20
- package/docs/examples/README.md +13 -5
- package/docs/examples/app-shell/README.md +43 -8
- 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/auth.ts +3 -2
- package/docs/examples/app-shell/src/routes.ts +6 -3
- package/docs/examples/app-shell/src/service.ts +4 -3
- package/docs/examples/club-events/README.md +7 -2
- package/docs/examples/club-events/manifests/club-events.yaml +22 -0
- package/docs/examples/club-events/smoke.mjs +7 -1
- package/docs/examples/club-events/src/handlers.ts +1 -1
- package/docs/examples/purchasing/README.md +12 -3
- package/docs/examples/purchasing/manifests/purchasing.yaml +36 -3
- package/docs/examples/purchasing/smoke.mjs +14 -0
- package/docs/examples/purchasing/src/handlers.ts +1 -1
- package/docs/examples/recipes/README.md +5 -3
- package/docs/handbook/cloudflare/authentication.md +21 -4
- package/docs/handbook/cloudflare/chatgpt-sites.md +4 -2
- package/docs/handbook/cloudflare/deploy-and-operate.md +24 -1
- package/docs/handbook/cloudflare/media-r2.md +2 -3
- package/docs/handbook/cloudflare/service-entry.md +17 -4
- package/docs/handbook/concepts/runtime-and-adapters.md +1 -1
- 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 +232 -0
- package/docs/handbook/guides/local-testing.md +40 -0
- package/docs/handbook/guides/mcp-apps.md +78 -1
- package/docs/handbook/guides/native-admin.md +29 -6
- package/docs/handbook/guides/native-execution.md +5 -4
- package/docs/handbook/guides/oauth.md +58 -0
- 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 +173 -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 +11 -4
- package/docs/handbook/reference/trigger.md +9 -0
- 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 +15 -1
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +44 -16
- package/docs/skills/integrate/SKILL.md +4 -9
- package/docs/skills/provision/SKILL.md +15 -12
- package/docs/skills/update/SKILL.md +21 -18
- package/package.json +24 -4
- package/skills/README.md +1 -1
- package/skills/mantle/SKILL.md +48 -107
- package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
- package/docs/skills/develop/mcp-app.md +0 -39
- package/skills/.keep +0 -0
- package/skills/mantle/references/local-testing.md +0 -15
- package/skills/mantle/references/web.md +0 -41
- package/skills/mantle/references/website.md +0 -49
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Keep runtime inputs while disabling the provider's eager and bulk lazy seeding. No schema or database work. */
|
|
2
|
-
// OAuth provider 1.7.7 shares plugin.options with its eager/lazy seed closures; its seedResources/checkIdentifier helpers are not exported.
|
|
3
1
|
const seeds = new WeakMap();
|
|
4
2
|
export function resourceSeeds(options) {
|
|
5
3
|
const inputs = (seeds.get(options) ?? options.resources ?? []);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ContractError } from "../kernel/contracts.js";
|
|
2
|
+
export const PRIVATE = { "cache-control": "private, no-store" };
|
|
3
|
+
export 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"]);
|
|
4
|
+
export const MAX_BODY = 1_048_576;
|
|
5
|
+
export const text = (body, status) => new Response(body, { status, headers: PRIVATE });
|
|
6
|
+
export const json = (body, status = 200) => Response.json(body, { status, headers: PRIVATE });
|
|
7
|
+
export const redirect = (location, status = 302) => new Response(null, { status, headers: { location, ...PRIVATE } });
|
|
8
|
+
export const tooLarge = () => new ContractError("INPUT_VALIDATION_FAILED", "Request body exceeds 1 MiB.");
|
|
9
|
+
export async function readBody(request) {
|
|
10
|
+
if (Number(request.headers.get("content-length")) > MAX_BODY)
|
|
11
|
+
throw tooLarge();
|
|
12
|
+
const reader = request.body?.getReader();
|
|
13
|
+
if (!reader)
|
|
14
|
+
return "";
|
|
15
|
+
const decoder = new TextDecoder();
|
|
16
|
+
let size = 0, text = "";
|
|
17
|
+
try {
|
|
18
|
+
for (;;) {
|
|
19
|
+
const { value, done } = await reader.read();
|
|
20
|
+
if (done)
|
|
21
|
+
return text + decoder.decode();
|
|
22
|
+
size += value.byteLength;
|
|
23
|
+
if (size > MAX_BODY) {
|
|
24
|
+
void reader.cancel();
|
|
25
|
+
throw tooLarge();
|
|
26
|
+
}
|
|
27
|
+
text += decoder.decode(value, { stream: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
reader.releaseLock();
|
|
32
|
+
}
|
|
33
|
+
}
|
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)))
|
|
@@ -28,8 +21,9 @@ export function authRoutes(loader, options) {
|
|
|
28
21
|
if (basePathOf(auth.options) !== base)
|
|
29
22
|
throw new Error("authRoutes basePath differs from betterAuth options.");
|
|
30
23
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
const bounded = request.body ? new Request(request, { body: await readBody(request) }) : request;
|
|
25
|
+
await guardAuthAdminRequest(bounded, () => resolve(bounded), base);
|
|
26
|
+
const registration = bounded.method === "POST" && pathname === `${base}/oauth2/register` ? await loopbackClientsAreNative(bounded) : bounded;
|
|
33
27
|
return await normalizeAuthResponseCookies(await auth.handler(registration));
|
|
34
28
|
}
|
|
35
29
|
catch (error) {
|
|
@@ -43,127 +37,15 @@ export function authRoutes(loader, options) {
|
|
|
43
37
|
checkOrigin(request, caller);
|
|
44
38
|
if (request.method === "POST" && Number(request.headers.get("content-length")) > MAX_BODY)
|
|
45
39
|
return text("request body too large", 413);
|
|
46
|
-
|
|
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);
|
|
47
42
|
}
|
|
48
43
|
catch (error) {
|
|
49
44
|
return surfaceFailure(error, undefined, `auth ${route}`);
|
|
50
45
|
}
|
|
51
46
|
});
|
|
52
47
|
}
|
|
53
|
-
function consentSurface(auth, options) {
|
|
54
|
-
const { listOAuthConsents: list, revokeOAuthConsent: revoke } = auth;
|
|
55
|
-
return async (request, caller, route) => {
|
|
56
|
-
const person = caller.kind === "user" && caller.credential === "session" ? caller.subject : null;
|
|
57
|
-
const locale = /zh[-_]tw/i.test(request.headers.get("accept-language") ?? "") ? "zh-TW" : "en";
|
|
58
|
-
if (route === "GET /oauth/consent/data") {
|
|
59
|
-
if (!person)
|
|
60
|
-
return json({ consent: null }, 401);
|
|
61
|
-
const model = await auth.getOAuthConsentRequest(request).catch(() => null); // an invalid signed query stays secret-free
|
|
62
|
-
return json({ consent: model }, model ? 200 : 400);
|
|
63
|
-
}
|
|
64
|
-
if (route === "GET /oauth/consent") {
|
|
65
|
-
const model = await auth.getOAuthConsentRequest(request).catch(() => null);
|
|
66
|
-
return html(consentHtml(locale, model), model ? 200 : 400);
|
|
67
|
-
}
|
|
68
|
-
if (route === "POST /oauth/consent") {
|
|
69
|
-
if (!person)
|
|
70
|
-
return text("sign in to answer this request", 401);
|
|
71
|
-
const body = await readBody(request);
|
|
72
|
-
const decision = new URLSearchParams(body).get("decision");
|
|
73
|
-
if (decision !== "approve" && decision !== "deny")
|
|
74
|
-
return text("invalid consent decision", 400);
|
|
75
|
-
// the redirect is the one Better Auth validated against the client's registration; nothing in the request names it
|
|
76
|
-
const location = await auth.completeOAuthConsent(new Request(request, { body }), decision === "approve").catch(() => null);
|
|
77
|
-
const target = location && URL.canParse(location) ? new URL(location) : null;
|
|
78
|
-
if (!target || /^(javascript|data|vbscript|blob):$/i.test(target.protocol))
|
|
79
|
-
return text("invalid authorization request", 400);
|
|
80
|
-
// the parsed form, so a browser cannot read `https:\\evil.test` differently from the check
|
|
81
|
-
return redirect(target.href);
|
|
82
|
-
}
|
|
83
|
-
if (!list || !revoke)
|
|
84
|
-
return text("not found", 404);
|
|
85
|
-
if (route === "GET /oauth/consents") {
|
|
86
|
-
if (options.connectedAppsPage)
|
|
87
|
-
return redirect(options.connectedAppsPage);
|
|
88
|
-
if (!person)
|
|
89
|
-
return text("sign in to see your connected apps", 401);
|
|
90
|
-
return html(connectedAppsHtml(locale, await list(person)), 200);
|
|
91
|
-
}
|
|
92
|
-
if (route === "GET /oauth/consents/data") {
|
|
93
|
-
if (!person)
|
|
94
|
-
return json({ consents: [] }, 401);
|
|
95
|
-
return json({ consents: await list(person) });
|
|
96
|
-
}
|
|
97
|
-
if (route === "POST /oauth/consents/revoke") {
|
|
98
|
-
if (!person)
|
|
99
|
-
return text("sign in to revoke a connection", 401);
|
|
100
|
-
const body = await readBody(request);
|
|
101
|
-
const id = new URLSearchParams(body).get("consent_id");
|
|
102
|
-
if (!id)
|
|
103
|
-
return text("invalid consent id", 400);
|
|
104
|
-
if (!await revoke(person, id))
|
|
105
|
-
return text("consent not found", 404);
|
|
106
|
-
return redirect("/oauth/consents", 303);
|
|
107
|
-
}
|
|
108
|
-
return text("not found", 404);
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
// ---- the pages a service without Admin shows: Admin is an optional subpath (ADR-0032 decision 13), OAuth is not tied to it
|
|
112
|
-
/**
|
|
113
|
-
* No `form-action`: browsers check it against every redirect of a form's navigation, so a callback that redirects again
|
|
114
|
-
* (www.cursor.com answers 308 to cursor.com) would hang the approval. The page runs no script and escapes every value.
|
|
115
|
-
*/
|
|
116
|
-
function html(body, status) {
|
|
117
|
-
return new Response(body, {
|
|
118
|
-
status,
|
|
119
|
-
headers: {
|
|
120
|
-
...PRIVATE, "content-type": "text/html; charset=UTF-8", "referrer-policy": "same-origin", "x-content-type-options": "nosniff", "x-frame-options": "DENY",
|
|
121
|
-
"content-security-policy": "default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'",
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
const T = {
|
|
126
|
-
en: {
|
|
127
|
-
lang: "en", title: "Authorize", eyebrow: "Connect an MCP client", heading: (c) => `Connect ${c}?`,
|
|
128
|
-
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.`,
|
|
129
|
-
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.",
|
|
130
|
-
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",
|
|
131
|
-
},
|
|
132
|
-
"zh-TW": {
|
|
133
|
-
lang: "zh-Hant-TW", title: "授權", eyebrow: "連結 MCP 客戶端", heading: (c) => `要連結 ${c} 嗎?`,
|
|
134
|
-
body: (c) => `${c} 將能透過 MCP 使用這個網站提供的工具;它能查看或變更哪些內容,仍會依照你的帳號權限決定。`,
|
|
135
|
-
returnsTo: "完成後返回", scopes: "要求的權限", approve: "連結", deny: "取消", invalid: "無效的授權請求", invalidBody: "缺少或格式錯誤的授權資訊,請返回 MCP 客戶端重試。",
|
|
136
|
-
apps: "MCP 連線", appsBody: "這些 MCP 客戶端可以使用網站提供的工具;每次操作仍會依照你當下的帳號權限檢查。", empty: "目前沒有已連結的 MCP 客戶端。", revoke: "中斷連線",
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
const esc = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
140
|
-
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>` +
|
|
141
|
-
`<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>`;
|
|
142
|
-
function consentHtml(l, model) {
|
|
143
|
-
const t = T[l];
|
|
144
|
-
if (!model)
|
|
145
|
-
return page(l, t.title, `<p>${t.eyebrow}</p><h1>${t.invalid}</h1><p>${t.invalidBody}</p>`);
|
|
146
|
-
const client = esc(model.clientName);
|
|
147
|
-
// where the answer goes and what is asked, so a look-alike client name cannot hide either
|
|
148
|
-
const host = URL.canParse(model.redirectUri) ? new URL(model.redirectUri).host || new URL(model.redirectUri).protocol : model.redirectUri;
|
|
149
|
-
return page(l, t.title, `<p>${t.eyebrow}</p><h1>${t.heading(client)}</h1><p>${t.body(client)}</p>` +
|
|
150
|
-
`<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">` +
|
|
151
|
-
`<input type="hidden" name="oauth_query" value="${esc(model.oauthQuery)}"/><button type="submit" name="decision" value="approve">${t.approve}</button> ` +
|
|
152
|
-
`<button type="submit" name="decision" value="deny">${t.deny}</button></form>`);
|
|
153
|
-
}
|
|
154
|
-
function connectedAppsHtml(l, consents) {
|
|
155
|
-
const t = T[l];
|
|
156
|
-
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">` +
|
|
157
|
-
`<input type="hidden" name="consent_id" value="${esc(c.id)}"/><button type="submit">${t.revoke}</button></form></section>`).join("");
|
|
158
|
-
return page(l, t.apps, `<h1>${t.apps}</h1><p>${t.appsBody}</p>${apps}`);
|
|
159
|
-
}
|
|
160
48
|
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
161
|
-
/**
|
|
162
|
-
* A dynamic registration whose every redirect URI is http on a loopback host and that names no application_type is a
|
|
163
|
-
* native app (RFC 8252 7.3): local MCP clients register this way. Better Auth reads the missing type as `web`, which
|
|
164
|
-
* needs https on a non-loopback host, so it would refuse them all. Mark it native, as the client could itself; a
|
|
165
|
-
* registration naming a type, mixing hosts or using any other redirect is passed through unchanged.
|
|
166
|
-
*/
|
|
167
49
|
export async function loopbackClientsAreNative(request) {
|
|
168
50
|
const body = await readBody(request.clone()).then(JSON.parse).catch(error => { if (error instanceof ContractError)
|
|
169
51
|
throw error; return null; });
|
|
@@ -182,33 +64,6 @@ export async function loopbackClientsAreNative(request) {
|
|
|
182
64
|
headers.delete("content-length");
|
|
183
65
|
return new Request(request.url, { method: "POST", headers, body: JSON.stringify({ ...body, application_type: "native" }) });
|
|
184
66
|
}
|
|
185
|
-
/** Bounded streaming read even without Content-Length; the native parser still owns JSON/form semantics. */
|
|
186
|
-
async function readBody(request) {
|
|
187
|
-
if (Number(request.headers.get("content-length")) > MAX_BODY)
|
|
188
|
-
throw tooLarge();
|
|
189
|
-
const reader = request.body?.getReader();
|
|
190
|
-
if (!reader)
|
|
191
|
-
return "";
|
|
192
|
-
const decoder = new TextDecoder();
|
|
193
|
-
let size = 0, text = "";
|
|
194
|
-
try {
|
|
195
|
-
for (;;) {
|
|
196
|
-
const { value, done } = await reader.read();
|
|
197
|
-
if (done)
|
|
198
|
-
return text + decoder.decode();
|
|
199
|
-
size += value.byteLength;
|
|
200
|
-
if (size > MAX_BODY) {
|
|
201
|
-
void reader.cancel();
|
|
202
|
-
throw tooLarge();
|
|
203
|
-
}
|
|
204
|
-
text += decoder.decode(value, { stream: true });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
finally {
|
|
208
|
-
reader.releaseLock();
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
/** Better Auth and host fetch implementations may merge several Set-Cookie values into one header. */
|
|
212
67
|
export async function normalizeAuthResponseCookies(response) {
|
|
213
68
|
if (!response.headers.has("set-cookie"))
|
|
214
69
|
return response;
|