@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.
Files changed (159) hide show
  1. package/README.md +9 -3
  2. package/dist/auth/adapter.js +1 -28
  3. package/dist/auth/admin.js +2 -5
  4. package/dist/auth/caller.js +0 -27
  5. package/dist/auth/consent.js +100 -0
  6. package/dist/auth/hardening.js +0 -7
  7. package/dist/auth/identity.js +0 -11
  8. package/dist/auth/oauth.js +0 -32
  9. package/dist/auth/resource-seeds.js +0 -2
  10. package/dist/auth/route-http.js +33 -0
  11. package/dist/auth/routes.js +5 -151
  12. package/dist/build/compiler.d.ts +19 -609
  13. package/dist/build/compiler.js +3 -6896
  14. package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
  15. package/dist/build/spec/domain/model/UserFields.d.ts +1 -0
  16. package/dist/bun/assets.js +0 -2
  17. package/dist/domain/adminOrder.js +0 -2
  18. package/dist/domain/columns.js +7 -20
  19. package/dist/domain/describe/chunk-SGMRVDGE.js +1 -0
  20. package/dist/domain/describe/index.js +1 -115
  21. package/dist/domain/describe/target-hash.js +1 -12
  22. package/dist/domain/entry.js +0 -14
  23. package/dist/domain/generated-tools.js +0 -1
  24. package/dist/domain/input.js +0 -2
  25. package/dist/domain/paging.js +0 -17
  26. package/dist/domain/recipes.js +10 -7
  27. package/dist/domain/userReferences.js +0 -1
  28. package/dist/domain/wireScalars.js +0 -6
  29. package/dist/infrastructure/adminEntries.js +10 -7
  30. package/dist/infrastructure/adminPaging.js +55 -57
  31. package/dist/infrastructure/artifacts.js +0 -4
  32. package/dist/infrastructure/authSchema.js +0 -9
  33. package/dist/infrastructure/bundle.js +2 -30
  34. package/dist/infrastructure/cli.js +25 -11
  35. package/dist/infrastructure/generate.js +9 -16
  36. package/dist/infrastructure/generateAuth.js +1 -9
  37. package/dist/infrastructure/help.js +55 -0
  38. package/dist/infrastructure/init.js +50 -0
  39. package/dist/infrastructure/inputs.js +0 -9
  40. package/dist/infrastructure/mounts.js +22 -0
  41. package/dist/infrastructure/wrangler.js +0 -6
  42. package/dist/kernel/authorization.js +0 -1
  43. package/dist/kernel/binds.d.ts +16 -4
  44. package/dist/kernel/binds.js +4 -10
  45. package/dist/kernel/caller-identity.d.ts +0 -3
  46. package/dist/kernel/caller-identity.js +0 -15
  47. package/dist/kernel/caller-mode.d.ts +4 -0
  48. package/dist/kernel/caller-mode.js +14 -0
  49. package/dist/kernel/contracts.d.ts +6 -10
  50. package/dist/kernel/contracts.js +5 -41
  51. package/dist/kernel/cron.js +0 -4
  52. package/dist/kernel/dialect/common.js +0 -3
  53. package/dist/kernel/dialect/postgres.js +0 -2
  54. package/dist/kernel/dialect/sqlite.js +1 -6
  55. package/dist/kernel/drain.js +2 -8
  56. package/dist/kernel/driver/bun-sqlite.js +0 -4
  57. package/dist/kernel/driver/d1.js +0 -1
  58. package/dist/kernel/driver/postgres.js +2 -17
  59. package/dist/kernel/driver/transaction-context.js +0 -1
  60. package/dist/kernel/email.js +0 -4
  61. package/dist/kernel/host-access.js +2 -3
  62. package/dist/kernel/host-caller.d.ts +1 -1
  63. package/dist/kernel/host-caller.js +3 -6
  64. package/dist/kernel/json-body.js +0 -2
  65. package/dist/kernel/mcp-apps.js +0 -5
  66. package/dist/kernel/mcp-call.js +0 -1
  67. package/dist/kernel/mcp-handler.js +2 -26
  68. package/dist/kernel/mcp.js +0 -5
  69. package/dist/kernel/media.js +0 -18
  70. package/dist/kernel/paging.js +1 -9
  71. package/dist/kernel/publishing.js +3 -4
  72. package/dist/kernel/r2-media.js +0 -7
  73. package/dist/kernel/rest-dispatch.js +0 -1
  74. package/dist/kernel/rest-router.js +2 -3
  75. package/dist/kernel/rest.js +0 -3
  76. package/dist/kernel/scalars.js +1 -9
  77. package/dist/kernel/search.js +0 -1
  78. package/dist/kernel/site.js +0 -2
  79. package/dist/kernel/store.d.ts +11 -1
  80. package/dist/kernel/store.js +21 -34
  81. package/dist/kernel/surface-failure.d.ts +10 -0
  82. package/dist/kernel/surface-failure.js +29 -0
  83. package/dist/kernel/surface.js +0 -3
  84. package/dist/kernel/sweep.js +0 -1
  85. package/dist/kernel/translations.js +0 -1
  86. package/dist/kernel/user-caller.js +2 -8
  87. package/dist/kernel/user-fields.d.ts +3 -2
  88. package/dist/kernel/user-fields.js +9 -9
  89. package/dist/kernel/validation.js +0 -8
  90. package/dist/kernel/web-gateway.js +0 -2
  91. package/dist/kernel/web-head.js +0 -1
  92. package/dist/kernel/web-index.js +0 -1
  93. package/dist/kernel/web-response.js +0 -1
  94. package/dist/kernel/web-serve.js +97 -0
  95. package/dist/kernel/web.d.ts +1 -1
  96. package/dist/kernel/web.js +10 -91
  97. package/dist/kernel/where.js +0 -6
  98. package/dist/usecase/adminBindings.js +30 -21
  99. package/dist/usecase/adminClient.js +0 -21
  100. package/dist/usecase/adminDescriptors.js +2 -9
  101. package/dist/usecase/generate.js +7 -12
  102. package/dist/usecase/handlers.js +0 -3
  103. package/dist/usecase/mcp-media.js +0 -1
  104. package/dist/usecase/mcp.js +0 -4
  105. package/dist/usecase/paging.js +1 -3
  106. package/dist/usecase/publishing.js +0 -4
  107. package/dist/usecase/rest.js +0 -1
  108. package/dist/usecase/site.js +0 -1
  109. package/dist/usecase/stores.js +4 -14
  110. package/dist/usecase/tool-version.js +0 -3
  111. package/dist/usecase/translations.js +0 -2
  112. package/dist/usecase/validation.js +0 -20
  113. package/docs/examples/README.md +13 -5
  114. package/docs/examples/app-shell/README.md +38 -7
  115. package/docs/examples/app-shell/accept.mjs +91 -0
  116. package/docs/examples/app-shell/gitignore +7 -0
  117. package/docs/examples/app-shell/package.json +7 -6
  118. package/docs/examples/app-shell/smoke-kit.mjs +29 -15
  119. package/docs/examples/app-shell/src/routes.ts +6 -3
  120. package/docs/examples/app-shell/src/service.ts +2 -1
  121. package/docs/examples/club-events/README.md +7 -2
  122. package/docs/examples/club-events/manifests/club-events.yaml +15 -0
  123. package/docs/examples/club-events/smoke.mjs +7 -1
  124. package/docs/examples/purchasing/README.md +12 -3
  125. package/docs/examples/purchasing/manifests/purchasing.yaml +21 -4
  126. package/docs/examples/purchasing/smoke.mjs +14 -0
  127. package/docs/examples/recipes/README.md +5 -3
  128. package/docs/handbook/cloudflare/service-entry.md +3 -0
  129. package/docs/handbook/examples/hub.md +1 -1
  130. package/docs/handbook/guides/agent-setup.md +15 -10
  131. package/docs/handbook/guides/agent-workflow-acceptance.md +113 -103
  132. package/docs/handbook/guides/local-testing.md +40 -0
  133. package/docs/handbook/guides/native-admin.md +25 -6
  134. package/docs/handbook/guides/native-execution.md +5 -4
  135. package/docs/handbook/guides/public-pages.md +44 -4
  136. package/docs/handbook/guides/sql-patterns.md +266 -0
  137. package/docs/handbook/guides/typed-queries.md +138 -0
  138. package/docs/handbook/navigation.json +16 -0
  139. package/docs/handbook/reference/admin-descriptors.md +4 -1
  140. package/docs/handbook/reference/authorization.md +1 -1
  141. package/docs/handbook/reference/diagnostics.md +6 -1
  142. package/docs/handbook/reference/procedure.md +5 -5
  143. package/docs/handbook/reference/schema.md +10 -4
  144. package/docs/handbook/reference/sql.md +64 -0
  145. package/docs/handbook/reference/surface.md +5 -2
  146. package/docs/handbook/reference/view.md +11 -3
  147. package/docs/handbook/releases/index.md +27 -6
  148. package/docs/handbook/start/agents.md +45 -0
  149. package/docs/handbook/start/project-and-cli.md +6 -0
  150. package/docs/handbook/start/quickstart-worker.md +1 -1
  151. package/docs/skills/develop/SKILL.md +6 -5
  152. package/docs/skills/update/SKILL.md +2 -0
  153. package/package.json +23 -3
  154. package/skills/README.md +1 -1
  155. package/skills/mantle/SKILL.md +39 -120
  156. package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
  157. package/skills/mantle/references/local-testing.md +0 -20
  158. package/skills/mantle/references/web.md +0 -41
  159. package/skills/mantle/references/website.md +0 -51
@@ -1,18 +1,11 @@
1
- /** Optional app-mounted auth routes; consent and raw admin routes resolve fresh authority. Better Auth owns tokens. */
2
- import { checkOrigin, surfaceFailure, ContractError } from "../kernel/contracts.js";
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
- return await consentSurface((await import("./oauth.js")).oauthConsents(await loader.get()), options)(request, caller, route);
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;