@aotter/mantle 0.2.0-alpha.7 → 0.2.0-alpha.9
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 +27 -5
- package/dist/auth/adapter.js +9 -6
- package/dist/auth/caller.js +2 -1
- package/dist/auth/identity.d.ts +2 -2
- package/dist/auth/oauth.d.ts +33 -1
- package/dist/auth/oauth.js +121 -28
- package/dist/auth/routes.js +6 -4
- package/dist/build/compiler.d.ts +44 -122
- package/dist/build/compiler.js +202 -277
- package/dist/build/spec/domain/model/ManifestGrammar.d.ts +6 -5
- package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
- package/dist/build/spec/domain/service/PlanFingerprint.d.ts +2 -0
- package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
- package/dist/domain/describe/{chunk-ZIQVLA7X.js → chunk-ZVXBGP7Y.js} +69 -8
- package/dist/domain/describe/index.js +1 -1
- package/dist/domain/describe/target-hash.js +1 -1
- package/dist/domain/describe/targets.d.ts +1 -1
- package/dist/domain/generated-tools.js +2 -0
- package/dist/domain/paging.js +41 -15
- package/dist/infrastructure/bundle.js +118 -15
- package/dist/infrastructure/cli.js +11 -15
- package/dist/infrastructure/generate.d.ts +1 -3
- package/dist/infrastructure/generate.js +11 -3
- package/dist/infrastructure/generateAuth.js +57 -0
- package/dist/infrastructure/inputs.js +8 -3
- package/dist/infrastructure/wrangler.js +50 -12
- package/dist/kernel/binds.d.ts +0 -3
- package/dist/kernel/contracts.d.ts +1 -1
- package/dist/kernel/contracts.js +4 -4
- package/dist/kernel/dialect/common.d.ts +0 -1
- package/dist/kernel/drain.d.ts +5 -0
- package/dist/kernel/drain.js +35 -0
- package/dist/kernel/driver/postgres.js +6 -2
- package/dist/kernel/host-caller.d.ts +1 -0
- package/dist/kernel/host-caller.js +1 -0
- package/dist/kernel/mcp-apps.d.ts +29 -9
- package/dist/kernel/mcp-apps.js +3 -3
- package/dist/kernel/mcp-handler.js +101 -18
- package/dist/kernel/mcp.d.ts +12 -3
- package/dist/kernel/mcp.js +31 -6
- package/dist/kernel/rest-router.js +3 -2
- package/dist/kernel/scalars.js +3 -2
- package/dist/kernel/site.d.ts +1 -3
- package/dist/kernel/site.js +2 -2
- package/dist/kernel/web-index.js +40 -0
- package/dist/kernel/web.d.ts +3 -1
- package/dist/kernel/web.js +6 -37
- package/dist/usecase/adminBindings.js +2 -3
- package/dist/usecase/adminClient.js +87 -0
- package/dist/usecase/adminDescriptors.js +19 -65
- package/dist/usecase/generate.js +5 -4
- package/dist/usecase/mcp-media.js +2 -2
- package/dist/usecase/mcp.js +16 -12
- package/dist/usecase/paging.js +2 -3
- package/dist/usecase/publishing.js +3 -3
- package/dist/usecase/site.js +0 -1
- package/dist/usecase/tool-version.js +9 -0
- package/dist/usecase/translations.js +1 -1
- package/dist/usecase/validation.js +128 -19
- package/dist/usecase/web.js +3 -2
- package/docs/examples/README.md +1 -1
- package/docs/examples/app-shell/README.md +45 -3
- package/docs/examples/app-shell/package.json +14 -6
- package/docs/examples/app-shell/smoke-kit.mjs +54 -0
- package/docs/examples/app-shell/src/auth.ts +6 -3
- package/docs/examples/app-shell/src/service.ts +3 -3
- package/docs/examples/app-shell/tsconfig.json +4 -1
- package/docs/examples/app-shell/worker-configuration.d.ts +2 -2
- package/docs/examples/app-shell/wrangler.jsonc +5 -4
- package/docs/examples/club-events/manifests/club-events.yaml +7 -0
- package/docs/examples/club-events/src/handlers.ts +1 -1
- package/docs/examples/generated-web/README.md +6 -2
- package/docs/examples/generated-web/wrangler.jsonc +1 -0
- package/docs/examples/purchasing/README.md +1 -1
- package/docs/examples/purchasing/manifests/purchasing.yaml +17 -1
- package/docs/examples/purchasing/smoke.mjs +10 -4
- package/docs/examples/purchasing/src/admin.tsx +13 -0
- package/docs/examples/purchasing/src/handlers.ts +1 -1
- package/docs/examples/purchasing/wrangler.jsonc +5 -4
- package/docs/examples/recipes/wrangler.jsonc +5 -4
- package/docs/handbook/cloudflare/authentication.md +43 -6
- package/docs/handbook/cloudflare/chatgpt-sites.md +4 -2
- package/docs/handbook/cloudflare/deploy-and-operate.md +45 -1
- package/docs/handbook/cloudflare/media-r2.md +2 -3
- package/docs/handbook/cloudflare/service-entry.md +68 -31
- package/docs/handbook/concepts/authorization.md +1 -1
- package/docs/handbook/concepts/mcp-and-agents.md +19 -0
- package/docs/handbook/concepts/runtime-and-adapters.md +1 -1
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/admin-ui.md +107 -1
- package/docs/handbook/guides/agent-setup.md +27 -9
- package/docs/handbook/guides/agent-workflow-acceptance.md +222 -0
- package/docs/handbook/guides/mcp-apps.md +522 -6
- package/docs/handbook/guides/native-admin.md +300 -51
- package/docs/handbook/guides/oauth.md +119 -5
- package/docs/handbook/guides/public-pages.md +2 -2
- package/docs/handbook/guides/size-and-startup.md +1 -1
- package/docs/handbook/guides/typed-queries.md +35 -0
- package/docs/handbook/navigation.json +0 -4
- package/docs/handbook/reference/admin-descriptors.md +0 -35
- package/docs/handbook/reference/procedure.md +6 -2
- package/docs/handbook/reference/schema.md +13 -3
- package/docs/handbook/reference/surface.md +129 -15
- package/docs/handbook/reference/trigger.md +43 -1
- package/docs/handbook/reference/view.md +1 -1
- package/docs/handbook/releases/index.md +74 -2
- package/docs/handbook/start/overview.md +1 -1
- package/docs/handbook/start/project-and-cli.md +16 -8
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +46 -18
- package/docs/skills/integrate/SKILL.md +4 -9
- package/docs/skills/media-gc/SKILL.md +7 -5
- package/docs/skills/provision/SKILL.md +18 -12
- package/docs/skills/update/SKILL.md +19 -18
- package/docs/upgrade-0.1-to-0.2.md +6 -6
- package/package.json +2 -1
- package/skills/README.md +11 -16
- package/skills/mantle/SKILL.md +39 -17
- package/skills/mantle/references/local-testing.md +9 -0
- package/skills/mantle/references/web.md +1 -1
- package/skills/mantle/references/website.md +26 -22
- package/docs/examples/app-shell/src/admin.ts +0 -10
- package/docs/examples/purchasing/src/admin.ts +0 -12
- package/docs/handbook/guides/admin-extensions.md +0 -20
- package/docs/skills/develop/mcp-app.md +0 -39
- package/skills/.keep +0 -0
|
@@ -25,7 +25,7 @@ spec:
|
|
|
25
25
|
title: Purchase requests
|
|
26
26
|
lifecycle: operational
|
|
27
27
|
indexes: [[authorId, createdAt, id], [requestStatus, createdAt, id]]
|
|
28
|
-
# Sidebar: in the Approvals group (src/admin.
|
|
28
|
+
# Sidebar: in the Approvals group (src/admin.tsx), after the queue of requests waiting for a decision.
|
|
29
29
|
uiSchema: { list: { primaryField: title, columns: [department, amount, requestStatus], filterField: requestStatus }, nav: { group: approvals, order: 2 } }
|
|
30
30
|
schema:
|
|
31
31
|
type: object
|
|
@@ -117,6 +117,7 @@ kind: Procedure
|
|
|
117
117
|
metadata: { name: submit-request }
|
|
118
118
|
spec:
|
|
119
119
|
title: Submit a purchase request
|
|
120
|
+
description: Submit a purchase request against an existing department budget.
|
|
120
121
|
requires: { auth: { all: [ctx.user] } }
|
|
121
122
|
input:
|
|
122
123
|
type: object
|
|
@@ -140,6 +141,7 @@ kind: Procedure
|
|
|
140
141
|
metadata: { name: change-amount }
|
|
141
142
|
spec:
|
|
142
143
|
title: Change the amount of my request
|
|
144
|
+
description: Change the amount of the signed-in member's submitted purchase request.
|
|
143
145
|
requires: { auth: { all: [ctx.user] } }
|
|
144
146
|
restrict: { requests: "author_id = auth.uid()" }
|
|
145
147
|
input:
|
|
@@ -291,11 +293,25 @@ spec:
|
|
|
291
293
|
---
|
|
292
294
|
apiVersion: cms.mantle.aotter.net/v2
|
|
293
295
|
kind: Trigger
|
|
296
|
+
metadata: { name: submit-request-mcp }
|
|
297
|
+
spec:
|
|
298
|
+
source: { kind: mcp, surface: public }
|
|
299
|
+
target: { procedure: submit-request }
|
|
300
|
+
---
|
|
301
|
+
apiVersion: cms.mantle.aotter.net/v2
|
|
302
|
+
kind: Trigger
|
|
294
303
|
metadata: { name: change-amount-http }
|
|
295
304
|
spec:
|
|
296
305
|
source: { kind: http, method: POST, path: '/api/requests/{id}/amount' }
|
|
297
306
|
target: { procedure: change-amount }
|
|
298
307
|
---
|
|
308
|
+
apiVersion: cms.mantle.aotter.net/v2
|
|
309
|
+
kind: Trigger
|
|
310
|
+
metadata: { name: change-amount-mcp }
|
|
311
|
+
spec:
|
|
312
|
+
source: { kind: mcp, surface: public }
|
|
313
|
+
target: { procedure: change-amount }
|
|
314
|
+
---
|
|
299
315
|
# Admin MCP tools: an AI agent connected to /mcp/admin can approve or reject.
|
|
300
316
|
apiVersion: cms.mantle.aotter.net/v2
|
|
301
317
|
kind: Trigger
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
4
|
import { createHmac } from "node:crypto";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
-
import { smoke } from "./smoke-kit.mjs";
|
|
6
|
+
import { assertMcpDiscovery, smoke } from "./smoke-kit.mjs";
|
|
7
|
+
import { developerConsole } from "./generated/admin-descriptors.ts";
|
|
7
8
|
|
|
8
9
|
const SECRET = "local-supplier-secret", DAILY = "0 9 * * *", DAY_US = 86_400 * 1_000_000;
|
|
9
10
|
|
|
@@ -22,7 +23,7 @@ const supplier = createServer((request, response) => {
|
|
|
22
23
|
await new Promise(resolve => supplier.listen(0, "127.0.0.1", resolve));
|
|
23
24
|
|
|
24
25
|
try {
|
|
25
|
-
await smoke(async ({ owner, call, mcp, signIn, sql, cron, waitForLog, step, logs }) => {
|
|
26
|
+
await smoke(async ({ origin, owner, call, mcp, signIn, sql, cron, waitForLog, step, logs }) => {
|
|
26
27
|
const query = sql;
|
|
27
28
|
const row = async id => (await query(`SELECT requeststatus AS status, version, supplierorderid AS "supplierOrderId" FROM requests WHERE id = '${id}'`))[0];
|
|
28
29
|
const statusAndOrder = async id => { const { status, supplierOrderId } = await row(id); return { status, supplierOrderId }; };
|
|
@@ -77,9 +78,14 @@ try {
|
|
|
77
78
|
|
|
78
79
|
const version = async id => (await row(id)).version;
|
|
79
80
|
assert.equal((await operation(alice, "approve-request", { id: laptop, expectedVersion: await version(laptop) })).status, 403);
|
|
80
|
-
|
|
81
|
+
// Generated static metadata uses the MCP tool names; the app chooses the surface and test identity.
|
|
82
|
+
const declared = developerConsole.interfaces.callable;
|
|
83
|
+
const publicTools = await assertMcpDiscovery(`${origin}/mcp`, { headers: { cookie: alice, origin }, tools: declared.filter(tool => tool.surface === "public") });
|
|
81
84
|
assert.ok(!publicTools.includes("approve_request") && !publicTools.includes("reject_request"), publicTools.join());
|
|
82
|
-
const adminTools =
|
|
85
|
+
const adminTools = await assertMcpDiscovery(`${origin}/mcp/admin`, { headers: { cookie: admin, origin }, tools: declared.filter(tool => tool.surface === "admin") });
|
|
86
|
+
// Denial is separate from successful initialized discovery.
|
|
87
|
+
assert.equal((await mcp("/mcp/admin")).status, 401);
|
|
88
|
+
assert.equal((await mcp("/mcp/admin", alice)).status, 403);
|
|
83
89
|
assert.ok(adminTools.includes("approve_request") && adminTools.includes("reject_request"), adminTools.join());
|
|
84
90
|
for (const hidden of ["confirm_order", "mark_ordered", "approve_request_write", "reject_request_write"])
|
|
85
91
|
assert.ok(!adminTools.includes(hidden) && !publicTools.includes(hidden), hidden);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineAdmin } from "@aotter/mantle-admin/app";
|
|
2
|
+
import { admin } from "../generated/admin-client.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Admin entry, built by `mantle bundle`. The approver's daily work gets its own sidebar group above Admin's
|
|
6
|
+
* built-in ones (content 10, operations 20, reports 30; More stays last); manifests place `pending-requests` and
|
|
7
|
+
* `requests` in it with `uiSchema.nav`. `tsc` refuses a manifest group left undeclared here.
|
|
8
|
+
* AGENT: add groups for how the staff work; titles take one string or a { locale: text } map.
|
|
9
|
+
*/
|
|
10
|
+
export default defineAdmin(admin, {
|
|
11
|
+
nav: { groups: { approvals: { title: { en: "Approvals", "zh-TW": "簽核" }, order: 5 } } },
|
|
12
|
+
pages: [{ path: "guide", title: "Approver guide", element: <p>Approve a request when the amount fits the budget; reject it with a reason otherwise.</p>, nav: { group: "approvals", order: 9 } }],
|
|
13
|
+
});
|
|
@@ -14,7 +14,7 @@ export const handlers: Handlers = {
|
|
|
14
14
|
} catch (error) {
|
|
15
15
|
// The declared budget check is a business refusal, not a server error. Nothing was written.
|
|
16
16
|
// The approval's only Schema check is the budget one.
|
|
17
|
-
if (error instanceof ContractError && error.code === "CONFLICT" && error.
|
|
17
|
+
if (error instanceof ContractError && error.code === "CONFLICT" && error.fields?.kind === "check" && error.fields.schema === "budgets")
|
|
18
18
|
throw new ContractError("CONFLICT", "The department budget cannot cover this request.", "procedures/approve-request");
|
|
19
19
|
throw error;
|
|
20
20
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
3
|
"name": "mantle-app",
|
|
4
|
-
"main": "dist/index.js",
|
|
4
|
+
"main": "dist/worker/index.js",
|
|
5
5
|
"compatibility_date": "2026-09-01",
|
|
6
6
|
"compatibility_flags": [
|
|
7
|
-
"nodejs_compat"
|
|
7
|
+
"nodejs_compat",
|
|
8
|
+
"global_fetch_strictly_public"
|
|
8
9
|
],
|
|
9
10
|
"vars": {
|
|
10
11
|
"EMPLOYEE_DOMAIN": "example.com"
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
}
|
|
37
38
|
],
|
|
38
39
|
"assets": {
|
|
39
|
-
"directory": "
|
|
40
|
+
"directory": "dist/client",
|
|
40
41
|
"binding": "ASSETS",
|
|
41
42
|
"run_worker_first": true,
|
|
42
43
|
"html_handling": "none"
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
},
|
|
49
50
|
"no_bundle": true,
|
|
50
51
|
"find_additional_modules": true,
|
|
51
|
-
"base_dir": "dist",
|
|
52
|
+
"base_dir": "dist/worker",
|
|
52
53
|
"build": {
|
|
53
54
|
"command": "mantle bundle src/index.ts dist",
|
|
54
55
|
"watch_dir": [
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
3
|
"name": "mantle-app",
|
|
4
|
-
"main": "dist/index.js",
|
|
4
|
+
"main": "dist/worker/index.js",
|
|
5
5
|
"compatibility_date": "2026-09-01",
|
|
6
6
|
"compatibility_flags": [
|
|
7
|
-
"nodejs_compat"
|
|
7
|
+
"nodejs_compat",
|
|
8
|
+
"global_fetch_strictly_public"
|
|
8
9
|
],
|
|
9
10
|
"secrets": {
|
|
10
11
|
"required": [
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"MEDIA_S3_ENDPOINT": "https://mantle-app-media.ACCOUNT_ID.r2.cloudflarestorage.com"
|
|
44
45
|
},
|
|
45
46
|
"assets": {
|
|
46
|
-
"directory": "
|
|
47
|
+
"directory": "dist/client",
|
|
47
48
|
"binding": "ASSETS",
|
|
48
49
|
"run_worker_first": true,
|
|
49
50
|
"html_handling": "none"
|
|
@@ -53,7 +54,7 @@
|
|
|
53
54
|
},
|
|
54
55
|
"no_bundle": true,
|
|
55
56
|
"find_additional_modules": true,
|
|
56
|
-
"base_dir": "dist",
|
|
57
|
+
"base_dir": "dist/worker",
|
|
57
58
|
"build": {
|
|
58
59
|
"command": "mantle bundle src/index.ts dist",
|
|
59
60
|
"watch_dir": [
|
|
@@ -12,7 +12,24 @@ Mantle supplies the database adapter, schema generation and caller resolution:
|
|
|
12
12
|
|
|
13
13
|
## `auth.ts`
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
> **Verification status:** the GitHub social-provider and magic-link recipes have not yet been exercised end to end against real deployed provider flows.
|
|
16
|
+
|
|
17
|
+
Copy `src/auth.ts` from the [app shell](../../examples/app-shell/README.md), which CI runs from the packed packages;
|
|
18
|
+
installed, it is `node_modules/@aotter/mantle/docs/examples/app-shell/src/auth.ts`. Its essentials:
|
|
19
|
+
|
|
20
|
+
- Imports: `betterAuth` from `better-auth/minimal`; `admin`, `emailOTP` (and `jwt` for MCP) from `better-auth/plugins`;
|
|
21
|
+
`createAccessControl` from `better-auth/plugins/access`; `defaultStatements` from `better-auth/plugins/admin/access`;
|
|
22
|
+
`d1Adapter` from `@aotter/mantle/auth/adapter`; the hardening and bootstrap helpers below. For MCP it also imports `jwt`,
|
|
23
|
+
`mcp` from `@better-auth/mcp`, `cimd` from `@better-auth/cimd` and `oauthGrantExtension`/`oauthGrantRevocation` from
|
|
24
|
+
`@aotter/mantle/auth/oauth`; the file marks that block as deletable.
|
|
25
|
+
- `authOptions(env)` returns plain options and is the module's export for `--auth src/auth.ts`. Generation calls it with an
|
|
26
|
+
empty `env`, so it must not throw: fall back to placeholders for `secret` and origin there.
|
|
27
|
+
- The bootstrap admin: `d1BootstrapAdmin(env.DB, { match: "email", value: env.ADMIN_EMAIL }, () => ({ options: { plugins } }))`
|
|
28
|
+
is assigned to `databaseHooks.user.create.after` and `.update.after` ([Identity helpers](#identity-helpers)).
|
|
29
|
+
- `createAuth(env)` is the same module's second export: it checks its three variables are set and returns
|
|
30
|
+
`betterAuth(authOptions(env))`. The Worker's `authLoader(() => import("./auth.js").then(m => m.createAuth(env)))`
|
|
31
|
+
([service composition](service-entry.md)) is what calls it.
|
|
32
|
+
|
|
16
33
|
The application owns Better Auth and its optional plugins. Schema generation reads plain options without constructing
|
|
17
34
|
Better Auth; apply `schema.sql` during deployment, never at boot. OAuth resource
|
|
18
35
|
rows are created on first OAuth use from runtime configuration; they require no
|
|
@@ -63,6 +80,7 @@ after success; invitations still work when email is absent.
|
|
|
63
80
|
`databaseHooks.user.create.after` (and user.update.after for a later email verification). Only the first matching signup wins (case-insensitive); email matching requires emailVerified=true (required for bootstrap).
|
|
64
81
|
D1 uses one conditional UPDATE; PostgreSQL uses a transaction advisory lock before UPDATE, avoiding concurrent winners.
|
|
65
82
|
A blocked promotion logs a warning. A GitHub rule requires a configured trusted GitHub provider/profile mapper.
|
|
83
|
+
The promotion runs after Better Auth creates the user, so the first sign-in response (and a cached session payload) can still show the previous role, while authorization re-reads the role on every request and `/admin/api/me` and later sessions show the admin role.
|
|
66
84
|
Helpers run no boot queries or DDL.
|
|
67
85
|
|
|
68
86
|
## Worker entry
|
|
@@ -154,7 +172,12 @@ Better Auth adapter code stays outside the Worker startup graph.
|
|
|
154
172
|
|
|
155
173
|
`@aotter/mantle/auth/admin` exports `guardAuthAdminRequest(request, resolveFreshCaller, basePath?)` for selected auth transports. It resolves authority only for raw/encoded admin paths, requires an admin cookie session and checks mutation origin. Both the app-owned template and OAuth routes use this same guard.
|
|
156
174
|
|
|
157
|
-
|
|
175
|
+
Bare `ctx.admin` admits any verified admin layer classified by Better Auth's
|
|
176
|
+
`adminRoles` / `adminUserIds`. Role lists in `requires` are refused; decide
|
|
177
|
+
per-target permissions in `authorize(target, caller)`, using `hasAdminRole`
|
|
178
|
+
when appropriate. Verified role names match `^[a-z][a-zA-Z0-9_-]{0,62}$`.
|
|
179
|
+
Member role names alone grant no privilege. See
|
|
180
|
+
[authorization requirements](../reference/authorization.md).
|
|
158
181
|
|
|
159
182
|
The generated auth DDL adds a native `(createdAt, id)` member-paging index (respecting schema field/table names). Unfiltered forward/backward paging orders native timestamps through that index; PostgreSQL cursors retain all six fractional digits and bind as text before timestamp casting. Member search (`%term%` over id/name/email, casting UUID ids to text only in the search expression) is a full scan; the index does not accelerate it.
|
|
160
183
|
|
|
@@ -164,10 +187,24 @@ The default is one instance with `admin` and `user` roles. An app that keeps two
|
|
|
164
187
|
staff and customers) can run one Better Auth instance per population in the same Worker; every Mantle primitive
|
|
165
188
|
takes the instance it works on:
|
|
166
189
|
|
|
167
|
-
- Give each instance its own `basePath`, `advanced.cookiePrefix`, `secret` and table names (`user.modelName`,
|
|
168
|
-
`session.modelName`,
|
|
169
|
-
one `--auth` per module to `mantle generate`; the tables are merged into one migration, and two
|
|
170
|
-
define the same table differently are refused. The native adapter follows each instance's mapping.
|
|
190
|
+
- Give each instance its own `basePath`, `advanced.cookiePrefix`, `secret` and core table names (`user.modelName`,
|
|
191
|
+
`session.modelName`, `account.modelName`, `verification.modelName`). Keep each instance's options in its own
|
|
192
|
+
module and pass one `--auth` per module to `mantle generate`; the tables are merged into one migration, and two
|
|
193
|
+
instances that define the same table differently are refused. The native adapter follows each instance's mapping.
|
|
194
|
+
- Better Auth plugin factories share their schema object, so differing plugin field/model mappings across instances
|
|
195
|
+
in one Worker override each other. Use one of two supported ways:
|
|
196
|
+
1. Separate the instances by core table names only and leave every plugin mapping at its default.
|
|
197
|
+
2. When plugin tables must differ (`jwt`'s `jwks`, `@better-auth/oauth-provider`'s `oauth*` tables), map them
|
|
198
|
+
explicitly in every instance, the default one included, and give each instance its own copy of the plugin's
|
|
199
|
+
schema right after constructing it:
|
|
200
|
+
`const keys = jwt({ schema: { jwks: { modelName: "staff_jwks" } } }); const plugin = { ...keys, schema: structuredClone(keys.schema) };`
|
|
201
|
+
|
|
202
|
+
`mantle generate` refuses anything else when two or more `--auth` instances use the same plugin (`jwt`, `admin`,
|
|
203
|
+
`oauth-provider`, `two-factor`, `phone-number`, `anonymous`, `siwe`, `device-authorization`) and a mapping exists
|
|
204
|
+
while an instance keeps the shared schema object, or two instances resolve a mapping to the same name (generate
|
|
205
|
+
cannot tell an identical mapping from an inherited one, so mapped names must differ between instances). Build
|
|
206
|
+
every instance's plugins in the same order in the Worker as in `--auth`; an instance built earlier than a mapped
|
|
207
|
+
one can still inherit its mapping, which generate cannot see.
|
|
171
208
|
- One `authLoader(load, cookiePrefix)` per instance; call `resolveCaller(request, loader)` with the instance the
|
|
172
209
|
route belongs to, and mount `authRoutes(loader, ...)` per instance (it guards that instance's `/admin/*`). The
|
|
173
210
|
loader takes `cookiePrefix` separately on purpose: it checks the cookie before loading Better Auth, so anonymous
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Mantle on ChatGPT Sites
|
|
2
2
|
|
|
3
|
+
> **Verification status:** this guide is experimental and has not yet been exercised end to end on a real ChatGPT Site.
|
|
4
|
+
|
|
3
5
|
Treat the Site as an existing TypeScript host. Import selected generated Views/Procedures, pass the host's native handle and verified Caller, and compose only the surfaces it supports. Use [partial integration](../../skills/integrate/SKILL.md); Mantle supplies no Sites-specific identity or deployment adapter.
|
|
4
6
|
|
|
5
7
|
Choose postgres or sqlite according to the host's actual SQL executor. Custom handles are app-owned; a structural match alone does not guarantee driver semantics. [Database boundaries](../concepts/runtime-and-adapters.md) defines the supported drivers.
|
|
6
8
|
|
|
7
|
-
The runtime never creates, alters or checks tables. Apply initial `schema.sql` and selected auth/site DDL only to an empty database. For later changes the agent diffs regenerated SQL, writes and rehearses a host-native migration on a copy, and applies it before publishing new code.
|
|
9
|
+
The runtime never creates, alters or checks tables. Apply initial `schema.sql` and selected auth/site DDL only to an empty database. For later changes the agent diffs regenerated SQL, writes and rehearses a host-native migration on a copy, and applies it before publishing new code. Destructive/type changes require explicit data migration; Mantle drops nothing.
|
|
8
10
|
|
|
9
|
-
Use the Site's native publication workflow. Configure trusted origin and verified identity there; never construct a system/admin Caller from browser input. Serving Admin also requires its
|
|
11
|
+
Use the Site's native publication workflow. Configure trusted origin and verified identity there; never construct a system/admin Caller from browser input. Serving Admin also requires its app-built assets and shell security contract. Scheduling and storage provisioning belong to the host.
|
|
@@ -5,13 +5,34 @@ description: Install schema explicitly, migrate before deploying, and operate a
|
|
|
5
5
|
|
|
6
6
|
Run the same generation command with `--check`, typecheck the application and test its selected routes before deploying. Keep generated TypeScript, SQL and Wrangler edits committed; keep bundle `dist/` untracked. The app owns the database, auth secrets, trusted origin and assets.
|
|
7
7
|
|
|
8
|
+
## Public fetch routing
|
|
9
|
+
|
|
10
|
+
Workers use `compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"]`.
|
|
11
|
+
The public-fetch flag routes native `fetch` through Cloudflare's public Internet
|
|
12
|
+
boundary. OAuth verification needs it to fetch JWKS on the service's own public
|
|
13
|
+
hostname; CIMD also requires this boundary for client metadata. `nodejs_compat`
|
|
14
|
+
alone does not establish it.
|
|
15
|
+
|
|
16
|
+
`mantle generate cloudflare-*` adds these flags without replacing other flags,
|
|
17
|
+
including in environments with their own `compatibility_flags` array. The same
|
|
18
|
+
command with `--check` refuses missing flags without writing. JSON/JSONC configs
|
|
19
|
+
are updated; TOML receives manual-edit instructions. Keep both flags in every
|
|
20
|
+
TOML environment override. An explicit `global_fetch_private_origin` is preserved
|
|
21
|
+
for applications without OAuth/CIMD, but conflicts with selected OAuth/CIMD auth
|
|
22
|
+
and with the public-fetch flag: generation refuses that config without rewriting
|
|
23
|
+
it. Such applications must use public routing for their OAuth/CIMD transport.
|
|
24
|
+
|
|
25
|
+
After deployment, complete a real OAuth sign-in and use the resulting bearer
|
|
26
|
+
for MCP `initialize` and `tools/list`. Local development and cookie-only Admin
|
|
27
|
+
checks do not verify production public-hostname routing.
|
|
28
|
+
|
|
8
29
|
## Install and change schemas
|
|
9
30
|
|
|
10
31
|
The runtime never creates, alters or checks tables. Boot and requests run no DDL, introspection or schema checks.
|
|
11
32
|
|
|
12
33
|
For the first install, apply generated `schema.sql`, including selected auth/site DDL, to an empty database. Apply selected seed SQL explicitly.
|
|
13
34
|
|
|
14
|
-
For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
|
|
35
|
+
For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
|
|
15
36
|
|
|
16
37
|
Destructive changes and type changes need explicit data migration. Mantle drops nothing. Initial `schema.sql` is not an upgrade script.
|
|
17
38
|
|
|
@@ -19,6 +40,29 @@ For D1, create a migration with `pnpm exec wrangler d1 migrations create <databa
|
|
|
19
40
|
|
|
20
41
|
For PostgreSQL, use the app's migration tool and deployment credentials. Plan lock waits and long index builds there; request credentials need no DDL privileges. Configure session settings on the role/database as described in [native execution](../guides/native-execution.md#postgresql-settings). Hyperdrive query caching must be disabled for authority and mutation visibility.
|
|
21
42
|
|
|
43
|
+
## Latency placement and D1 read replication
|
|
44
|
+
|
|
45
|
+
Measure these options against the application's real traffic; neither is a
|
|
46
|
+
default latency guarantee. [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/)
|
|
47
|
+
can move fetch execution closer to observed back-end services:
|
|
48
|
+
|
|
49
|
+
```jsonc
|
|
50
|
+
"placement": { "mode": "smart" }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For globally distributed, read-heavy D1 traffic, enable
|
|
54
|
+
[read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/)
|
|
55
|
+
on the database and pass one request-scoped `env.DB.withSession(...)` handle to
|
|
56
|
+
the selected generated Views, Procedures and Store. Those D1 entry points accept
|
|
57
|
+
`D1DatabaseSession`. Without the Sessions API, queries continue to use the primary.
|
|
58
|
+
|
|
59
|
+
Use `first-unconstrained` only when a replica snapshot is acceptable,
|
|
60
|
+
`first-primary` when the flow must start from the latest primary state, or carry
|
|
61
|
+
a bookmark between logical sessions for sequential consistency. Reuse the same
|
|
62
|
+
session within that logical flow; all writes still go to the primary. Keep fresh
|
|
63
|
+
authentication/authorization checks and mutation/read-after-write paths on their
|
|
64
|
+
required current-authority path rather than weakening them for replica latency.
|
|
65
|
+
|
|
22
66
|
## Operate
|
|
23
67
|
|
|
24
68
|
Enable Workers Logs and traces in production. The top-level `observability.enabled` alone does not enable traces:
|
|
@@ -20,9 +20,8 @@ Each media purpose in `siteDefaults.media.purposes` lists mime **slots** and a b
|
|
|
20
20
|
`required` entries use `<input accept>` grammar (full mime, comma list, or a subtype shorthand such as `webp`);
|
|
21
21
|
`maxBytes` must cover every mime the slots allow, keyed by the full mime type.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
or credential management. Import `r2MediaStorage` from `@aotter/mantle/media/r2`:
|
|
23
|
+
Applications own their R2 binding, public bucket URL and S3 credentials; the SDK
|
|
24
|
+
package does not manage them. Import `r2MediaStorage` from `@aotter/mantle/media/r2`:
|
|
26
25
|
|
|
27
26
|
```ts
|
|
28
27
|
const storage = r2MediaStorage({
|
|
@@ -31,16 +31,18 @@ import { authLoader, resolveCaller } from '@aotter/mantle/auth';
|
|
|
31
31
|
import { authRoutes } from '@aotter/mantle/auth/routes';
|
|
32
32
|
import { withAuthTasks } from '@aotter/mantle/auth/hardening';
|
|
33
33
|
import { checkOrigin, surfaceFailure, type Caller } from '@aotter/mantle/contracts';
|
|
34
|
-
import { admin } from '@aotter/mantle-admin/server';
|
|
34
|
+
import { admin, adminShell } from '@aotter/mantle-admin/server';
|
|
35
|
+
import { adminMcpApps } from '@aotter/mantle-admin/mcp';
|
|
35
36
|
import { entries } from '@aotter/mantle-admin/server/entries';
|
|
36
37
|
import { adminBindings } from '../generated/admin-bindings.js';
|
|
37
|
-
import {
|
|
38
|
+
import { mcpAppCatalogues } from '../generated/admin-descriptors.js';
|
|
38
39
|
import { mcp } from '../generated/mcp.js';
|
|
39
40
|
import { rest } from '../generated/rest.js';
|
|
40
41
|
import { handlers } from './handlers.js'; // omit when there are no ref Procedures
|
|
41
42
|
|
|
42
43
|
// Env comes from worker-configuration.d.ts: declare DB, ASSETS, the email provider binding
|
|
43
|
-
// and the
|
|
44
|
+
// and the PUBLIC_ORIGIN, BETTER_AUTH_SECRET and ADMIN_EMAIL secrets in Wrangler.
|
|
45
|
+
// createAuth(env) is exported by the app's src/auth.ts (see the auth template).
|
|
44
46
|
let auth: import('@aotter/mantle/auth').AuthLoader<ReturnType<typeof import('./auth.js').createAuth>> | undefined;
|
|
45
47
|
export default {
|
|
46
48
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
@@ -50,7 +52,7 @@ export default {
|
|
|
50
52
|
let authority: Promise<Caller> | undefined, readAuthority: Promise<Caller> | undefined;
|
|
51
53
|
const resolve = (readOnly: boolean) => resolveCaller(request, auth!, {
|
|
52
54
|
readOnly,
|
|
53
|
-
audience: [`${env.
|
|
55
|
+
audience: [`${env.PUBLIC_ORIGIN}/mcp`, `${env.PUBLIC_ORIGIN}/mcp/admin`],
|
|
54
56
|
responseHeaders: headers,
|
|
55
57
|
}).then(value => { checkOrigin(request, value); return value; });
|
|
56
58
|
const caller = (_request?: Request, _env?: Env, classification = { readOnly: false }) =>
|
|
@@ -60,9 +62,9 @@ export default {
|
|
|
60
62
|
const routes = [
|
|
61
63
|
authRoutes(auth, { methods: [{ kind: 'email-otp' }], resolver: caller }),
|
|
62
64
|
admin(bindings.metadata, bindings.views, bindings.operations, {
|
|
63
|
-
...options, routes: entries(bindings.schemas, bindings.adapter),
|
|
65
|
+
...options, developerViews: bindings.developerViews, routes: entries(bindings.schemas, bindings.adapter),
|
|
64
66
|
}),
|
|
65
|
-
mcp(options),
|
|
67
|
+
mcp({ ...options, apps: { admin: adminMcpApps({ catalogue: mcpAppCatalogues.admin }) } }),
|
|
66
68
|
rest(options),
|
|
67
69
|
];
|
|
68
70
|
try {
|
|
@@ -74,8 +76,9 @@ export default {
|
|
|
74
76
|
for (const cookie of headers.getSetCookie()) result.headers.append('set-cookie', cookie);
|
|
75
77
|
return result;
|
|
76
78
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// Serves the built Admin shell for /admin and its client routes; undefined for any other path.
|
|
80
|
+
const shell = await adminShell(request, path => env.ASSETS.fetch(new Request(new URL('/admin/' + path, request.url), request)));
|
|
81
|
+
if (shell) return shell;
|
|
79
82
|
// Your public frontend/web.ts routes can use an anonymous Caller without
|
|
80
83
|
// loading auth; their generated SQL still controls publication visibility.
|
|
81
84
|
return new Response('Not found', { status: 404, headers: { 'cache-control': 'no-store' } });
|
|
@@ -85,31 +88,45 @@ export default {
|
|
|
85
88
|
};
|
|
86
89
|
```
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
Keep the MCP and REST entries together for the normal symmetric path. For a deliberate
|
|
92
|
+
asymmetric exception, remove the omitted surface's imports and array entry. `mcp.ts`
|
|
93
|
+
exists only when the manifest has an MCP surface and requires the documented optional
|
|
94
|
+
MCP peer. An application that deliberately does not mount MCP need not import generated
|
|
95
|
+
`mcp.ts`; outside the entry import graph, it contributes nothing to the bundle.
|
|
90
96
|
Pass the same handlers to each selected surface that exposes ref Procedures.
|
|
91
|
-
Bind site/media/identity
|
|
97
|
+
Bind site/media/identity capabilities through the selected Admin/MCP
|
|
92
98
|
options; do not expose native auth administrative routes without `authRoutes` or
|
|
93
99
|
`guardAuthAdminRequest`. `withAuthTasks` retains background callbacks on the
|
|
94
100
|
current request's context; `authHardening` configures the background handler.
|
|
95
101
|
Select `--admin` to emit `admin-bindings.ts`. Admin's API is a separate
|
|
96
102
|
`@aotter/mantle-admin` package; the SDK never imports it. The API handles only
|
|
97
|
-
`/admin/api`.
|
|
98
|
-
client routes, including `/admin/sign-in`,
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
`/admin/api`. `adminShell` (in the block above) serves the built shell for `/admin` and
|
|
104
|
+
client routes, including `/admin/sign-in`; it sets `frame-ancestors 'none'`, `X-Frame-Options: DENY`, `no-store` and `nosniff`
|
|
105
|
+
on the shell, answers a missing file under `/admin` with a 404 `Response`, and returns `undefined` for other paths and
|
|
106
|
+
non-GET requests. The app owns the rest of the CSP and the asset cache headers.
|
|
107
|
+
Collection statistics use the generated shared `statisticsView`:
|
|
101
108
|
its collection parameter selects only a generated table and visibility policy.
|
|
102
109
|
|
|
103
110
|
## Workers assets and secrets
|
|
104
111
|
|
|
112
|
+
`mantle generate --admin --wrangler wrangler.jsonc` sets the selected assets
|
|
113
|
+
directory. A new assets block defaults to `binding: "ASSETS"`, `run_worker_first: true`
|
|
114
|
+
and `html_handling: "none"`. Existing blocks keep their binding and routing; when
|
|
115
|
+
`run_worker_first` is already true, a missing `html_handling` defaults to `none`.
|
|
116
|
+
A different binding needs a custom Admin HTML loader. Deploy the built assets and
|
|
117
|
+
Worker together.
|
|
118
|
+
|
|
105
119
|
`mantle generate cloudflare-d1 generated manifests/*.yaml --auth src/auth.ts --admin`
|
|
106
|
-
updates the split-build Wrangler keys
|
|
107
|
-
|
|
120
|
+
updates the split-build Wrangler keys (`main` and `base_dir` under `dist/worker`),
|
|
121
|
+
ensures `nodejs_compat` and defaults to `global_fetch_strictly_public` ([public fetch routing](deploy-and-operate.md#public-fetch-routing))
|
|
122
|
+
and, with `--admin`, `assets.directory`: `mantle bundle` builds Admin into
|
|
123
|
+
`dist/client/admin`, served at `/admin`. Configure existing assets blocks explicitly;
|
|
124
|
+
JSONC comments and unrelated settings survive regeneration:
|
|
108
125
|
|
|
109
126
|
```jsonc
|
|
110
127
|
{
|
|
111
128
|
"assets": {
|
|
112
|
-
"directory": "
|
|
129
|
+
"directory": "dist/client",
|
|
113
130
|
"binding": "ASSETS",
|
|
114
131
|
"run_worker_first": true,
|
|
115
132
|
"html_handling": "none"
|
|
@@ -119,16 +136,18 @@ comments and unrelated settings survive regeneration:
|
|
|
119
136
|
```
|
|
120
137
|
|
|
121
138
|
`run_worker_first` lets the application supply shell/security/cache headers;
|
|
122
|
-
`none` keeps `index.html` available by name.
|
|
139
|
+
`none` keeps `index.html` available by name. `adminShell(request, assets)` passes
|
|
140
|
+
paths relative to the Admin directory, so the app fetches them under `/admin/`:
|
|
141
|
+
`path => env.ASSETS.fetch(new Request(new URL("/admin/" + path, request.url), request))`. Admin assets are not a public database
|
|
123
142
|
API. PostgreSQL uses application-owned Hyperdrive/postgres.js wiring,
|
|
124
143
|
with Hyperdrive query caching disabled for authority and mutation visibility.
|
|
125
144
|
|
|
126
145
|
Commit `.dev.vars.example` containing variable names and placeholders only:
|
|
127
146
|
|
|
128
147
|
```dotenv
|
|
129
|
-
|
|
148
|
+
PUBLIC_ORIGIN=http://localhost:8787
|
|
130
149
|
ADMIN_EMAIL=you@example.test
|
|
131
|
-
|
|
150
|
+
BETTER_AUTH_SECRET=replace-with-a-random-secret
|
|
132
151
|
```
|
|
133
152
|
|
|
134
153
|
Copy it to ignored `.dev.vars`; use deployment secrets for production. Configure
|
|
@@ -137,8 +156,7 @@ an app-owned [`EmailSender`](../guides/email.md#application-wiring) and call its
|
|
|
137
156
|
callbacks. Adapt the auth template's `MAIL.otp`/`MAIL.link` callbacks to this
|
|
138
157
|
sender; it is an object, not a `.dev.vars` value. The auth
|
|
139
158
|
module wires `ADMIN_EMAIL` to the bootstrap helper, which requires verified email.
|
|
140
|
-
`
|
|
141
|
-
explicitly when constructing auth and keep the trusted origin consistent.
|
|
159
|
+
Name the variables as your `auth.ts` reads them (the app shell uses these three) and keep the trusted origin consistent.
|
|
142
160
|
|
|
143
161
|
## Scheduling and Bun
|
|
144
162
|
|
|
@@ -150,22 +168,42 @@ invocation; see the [TTL contract](../guides/native-execution.md#ttl-maintenance
|
|
|
150
168
|
larger sweeps' cursors for a later invocation instead of scanning without a bound.
|
|
151
169
|
|
|
152
170
|
On Bun, reuse a process-owned `Database` or `SQL` pool and call the same generated
|
|
153
|
-
surfaces from `Bun.serve({fetch})`.
|
|
154
|
-
the
|
|
171
|
+
surfaces from `Bun.serve({fetch})`. Bun runs the server as TypeScript, so
|
|
172
|
+
`mantle bundle` builds only Admin for it: the `bun-*` target that
|
|
173
|
+
`generated/.mantle-inputs.json` records skips the Worker bundle, and Admin goes
|
|
174
|
+
to `dist/client/admin` (`--assets` as on Workers) when the server imports
|
|
175
|
+
`@aotter/mantle-admin/server`. Add it as the build script and run it before
|
|
176
|
+
starting the server:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{ "scripts": { "build": "mantle bundle src/server.ts dist" } }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The optional host reader is independent of the query target:
|
|
155
183
|
|
|
156
184
|
```ts
|
|
157
185
|
import { bunAdminAssets } from '@aotter/mantle/bun/assets';
|
|
158
|
-
const assets = bunAdminAssets('
|
|
186
|
+
const assets = bunAdminAssets('dist/client/admin');
|
|
159
187
|
const bindings = adminBindings({ handlers });
|
|
160
188
|
const serveAdmin = admin(bindings.metadata, bindings.views, bindings.operations, {
|
|
161
189
|
db: () => db, caller, handlers, routes: entries(bindings.schemas, bindings.adapter),
|
|
162
190
|
});
|
|
163
|
-
// Call assets(path) from your separate shell/static route.
|
|
191
|
+
// Call assets(path) from your separate shell/static route. adminShell(request, reader) wants
|
|
192
|
+
// paths without a leading slash and a Response (404 when missing), so adapt the reader:
|
|
193
|
+
const reader = async (path: string) => (await assets('/' + path)) ?? new Response(null, { status: 404 });
|
|
164
194
|
```
|
|
165
195
|
|
|
166
196
|
The reader returns a native `Response` or `null`, rejects traversal/control
|
|
167
197
|
characters and escaping symlinks; the app owns shell, CSP and cache headers.
|
|
168
|
-
Use ignored `.env` on Bun.
|
|
198
|
+
Use ignored `.env` on Bun.
|
|
199
|
+
|
|
200
|
+
Admin's build tools and UI packages are dependencies of `@aotter/mantle-admin`,
|
|
201
|
+
so a production install carries them (about 65 MB). For a container image, use a
|
|
202
|
+
multi-stage build: the build stage installs everything and runs `mantle bundle`
|
|
203
|
+
and `bun build src/server.ts --target bun --outdir dist/server`; the runtime stage
|
|
204
|
+
copies only `dist/` (plus any files the server reads at runtime) and starts
|
|
205
|
+
`bun dist/server/server.js`. Neither the Admin assets nor the bundled server read
|
|
206
|
+
`node_modules` once built. Run cron work with your native scheduler; neither
|
|
169
207
|
imports nor ordinary requests execute DDL, migrations or cron work. OAuth resource
|
|
170
208
|
rows are inserted on first OAuth use from runtime configuration, with no deploy
|
|
171
209
|
step; auth initialization and cookie/bearer/MCP/REST requests do no resource work.
|
|
@@ -190,13 +228,12 @@ untracked. Deployment uses committed generated source; it does not regenerate.
|
|
|
190
228
|
# CI, with the project's exact installed SDK and peer versions:
|
|
191
229
|
pnpm exec mantle generate cloudflare-d1 generated manifests/*.yaml --auth src/auth.ts --admin --check
|
|
192
230
|
pnpm exec tsc --noEmit
|
|
193
|
-
pnpm exec mantle bundle src/worker.ts dist
|
|
231
|
+
pnpm exec mantle bundle src/worker.ts dist # dist/worker, and dist/client/admin with --admin
|
|
194
232
|
```
|
|
195
233
|
|
|
196
234
|
`--check` regenerates in memory, lists missing/changed/obsolete SDK-owned files
|
|
197
235
|
and stale Wrangler build settings, exits nonzero, and writes nothing. Unmarked
|
|
198
236
|
application files are preserved. TOML generation prints manual edits, so an
|
|
199
|
-
exact Wrangler `--check` requires JSON/JSONC. Use all the same `--site`, `--auth
|
|
200
|
-
`--admin` and `--admin-extensions` options as normal generation. The check needs no database,
|
|
237
|
+
exact Wrangler `--check` requires JSON/JSONC. Use all the same `--site`, `--auth` and `--admin` options as normal generation. The check needs no database,
|
|
201
238
|
network access, credentials or deployment. Imported configuration modules must
|
|
202
239
|
remain pure: exporting `authOptions(env)` avoids constructing Better Auth at build time.
|
|
@@ -16,4 +16,4 @@ The compiler adds row policy to every Schema relation, including joins and neste
|
|
|
16
16
|
|
|
17
17
|
Field `scope` hides rows even from admin callers; use View/Procedure [`restrict`](../reference/view.md#restrict) to limit member access while Admin collection reads see all rows allowed by the Schema's scope.
|
|
18
18
|
|
|
19
|
-
The app owns custom identity and roles. Better Auth’s admin plugin `adminRoles` (default `["admin"]`) classifies comma-separated `user.role` values. `resolveCaller` carries this layer as `admin`; `isAdmin(caller)` and `hasAdminRole(caller, name)` consult it.
|
|
19
|
+
The app owns custom identity and roles. Better Auth’s admin plugin `adminRoles` (default `["admin"]`) classifies comma-separated `user.role` values. `resolveCaller` carries this layer as `admin`; `isAdmin(caller)` and `hasAdminRole(caller, name)` consult it. Callers come from `resolveCaller` over Better Auth; an existing identity provider connects through Better Auth's OAuth or SSO plugins, since no public API builds a Caller from another provider's session (`authenticatedAs` in `@aotter/mantle/testing` is for tests). Setting `admin: true` on a plain object grants no privilege. Member role names alone do not grant privilege. Optional [Better Auth](../cloudflare/authentication.md) helpers use fresh primary authority by default. Admin additionally requires an admin cookie session. UI visibility never replaces a server authorization check.
|
|
@@ -16,3 +16,22 @@ The app owns OAuth configuration, discovery and consent; follow [OAuth](../guide
|
|
|
16
16
|
[MCP Apps](../guides/mcp-apps.md) add static UI resources through the public contributor hook. Admin uses the same hook from its separate package. Credentials never go in App HTML. WebMCP catalogues are generated display/tool metadata for the mounted Admin product; server checks remain authoritative.
|
|
17
17
|
|
|
18
18
|
See [surface contracts](../reference/surface.md) for tool schemas, challenges and scalar conversion.
|
|
19
|
+
|
|
20
|
+
## When a host shows old tools
|
|
21
|
+
|
|
22
|
+
A fresh `tools/list` always returns the deployed tools, but many hosts keep their own copy and do not ask again after a deploy. Mantle sends no list-changed notifications: tools change only at deploy, and the hosts that keep copies do not act on them. Instead each generated tool's description ends with its own version, and the results of a call that reaches the tool carry the deployed version, so a model holding an older definition can see it, correct its call or ask the user to refresh. Failures also state the input schema the tool is listed with now, except authorization refusals. A call naming a tool the catalogue no longer has gets the protocol's unknown-tool error, whose message gives `serverInfo.version`: unless the application sets it, a hash of every tool the endpoint serves, before per-caller filtering. Refusals before a call reaches its tool, such as authentication challenges, carry neither.
|
|
23
|
+
|
|
24
|
+
In Admin, WebMCP results from the tool route carry the version, and refusals add it with the input schema, except authorization refusals; a tool removed since the page loaded is refused as not found, without a catalogue version. Admin's two navigation tools, `admin_get_context` and `admin_navigate`, carry no version: the page defines and runs them without a server call, so it cannot list one definition and run another, and a reload after a deploy replaces both. The exact shapes are in [surface contracts](../reference/surface.md#tool-versions).
|
|
25
|
+
|
|
26
|
+
How each client refreshes its copy (as of 2026-10):
|
|
27
|
+
|
|
28
|
+
| Client | Refresh |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| claude.ai connectors | A server-side copy keyed by URL, kept about an hour (users report up to 24 hours). Only adding the connector again under a new URL, such as `?v=2`, forces a refresh, and that needs authorization again. |
|
|
31
|
+
| Claude Code | `/mcp` reconnect, or a new session |
|
|
32
|
+
| ChatGPT developer mode | Refresh the connection, then start a new chat |
|
|
33
|
+
| Published ChatGPT apps | A daily scan, or Rescan, followed by review; keep tools compatible meanwhile |
|
|
34
|
+
| Cursor | Quit fully and reopen |
|
|
35
|
+
| VS Code | Run "MCP: Reset Cached Tools", or restart the server |
|
|
36
|
+
| Gemini CLI | `/mcp reload` |
|
|
37
|
+
| MCP Inspector | The refresh button |
|
|
@@ -11,7 +11,7 @@ The runtime never creates, alters or checks tables. Boot and requests run no DDL
|
|
|
11
11
|
|
|
12
12
|
For the first install, apply generated `schema.sql`, including selected auth/site DDL, to an empty database. Apply selected seed SQL explicitly.
|
|
13
13
|
|
|
14
|
-
For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
|
|
14
|
+
For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
|
|
15
15
|
|
|
16
16
|
Destructive changes and type changes need explicit data migration. Mantle drops nothing. Initial `schema.sql` is not an upgrade script.
|
|
17
17
|
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
Use the [Worker quickstart](../start/quickstart-worker.md) for a complete native manifest and entry, [typed queries](../guides/typed-queries.md) for embedding, and [service composition](../cloudflare/service-entry.md) for optional surfaces.
|
|
4
4
|
|
|
5
|
-
The runnable [reference service](https://github.com/aotter/mantle/
|
|
5
|
+
The runnable [reference service](https://github.com/aotter/mantle/tree/v0.2.0-alpha.9/docs/examples/reference-service) exercises the native package integration. See the [generated public website](../../examples/generated-web/README.md) for a native Worker example.
|