@aotter/mantle 0.2.0-alpha.7 → 0.2.0-alpha.8

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 (108) hide show
  1. package/README.md +10 -5
  2. package/dist/auth/caller.js +2 -1
  3. package/dist/auth/identity.d.ts +2 -2
  4. package/dist/auth/oauth.d.ts +33 -1
  5. package/dist/auth/oauth.js +121 -28
  6. package/dist/auth/routes.js +3 -2
  7. package/dist/build/compiler.d.ts +44 -122
  8. package/dist/build/compiler.js +202 -277
  9. package/dist/build/spec/domain/model/ManifestGrammar.d.ts +6 -5
  10. package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
  11. package/dist/build/spec/domain/service/PlanFingerprint.d.ts +2 -0
  12. package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
  13. package/dist/domain/describe/{chunk-ZIQVLA7X.js → chunk-ZVXBGP7Y.js} +69 -8
  14. package/dist/domain/describe/index.js +1 -1
  15. package/dist/domain/describe/target-hash.js +1 -1
  16. package/dist/domain/describe/targets.d.ts +1 -1
  17. package/dist/domain/generated-tools.js +2 -0
  18. package/dist/domain/paging.js +41 -15
  19. package/dist/infrastructure/bundle.js +105 -15
  20. package/dist/infrastructure/cli.js +11 -15
  21. package/dist/infrastructure/generate.d.ts +1 -3
  22. package/dist/infrastructure/generate.js +2 -2
  23. package/dist/infrastructure/generateAuth.js +6 -0
  24. package/dist/infrastructure/inputs.js +8 -3
  25. package/dist/infrastructure/wrangler.js +35 -12
  26. package/dist/kernel/drain.d.ts +5 -0
  27. package/dist/kernel/drain.js +35 -0
  28. package/dist/kernel/driver/postgres.js +3 -0
  29. package/dist/kernel/host-caller.d.ts +1 -0
  30. package/dist/kernel/host-caller.js +1 -0
  31. package/dist/kernel/mcp-apps.d.ts +29 -9
  32. package/dist/kernel/mcp-apps.js +2 -2
  33. package/dist/kernel/mcp-handler.js +100 -17
  34. package/dist/kernel/mcp.d.ts +7 -2
  35. package/dist/kernel/mcp.js +29 -5
  36. package/dist/kernel/rest-router.js +3 -2
  37. package/dist/kernel/site.d.ts +1 -3
  38. package/dist/kernel/site.js +2 -2
  39. package/dist/kernel/web.d.ts +3 -1
  40. package/dist/kernel/web.js +3 -0
  41. package/dist/usecase/adminBindings.js +2 -3
  42. package/dist/usecase/adminClient.js +87 -0
  43. package/dist/usecase/adminDescriptors.js +19 -65
  44. package/dist/usecase/generate.js +5 -4
  45. package/dist/usecase/mcp-media.js +2 -2
  46. package/dist/usecase/mcp.js +16 -12
  47. package/dist/usecase/paging.js +2 -3
  48. package/dist/usecase/publishing.js +2 -2
  49. package/dist/usecase/tool-version.js +9 -0
  50. package/dist/usecase/translations.js +1 -1
  51. package/dist/usecase/validation.js +120 -19
  52. package/dist/usecase/web.js +3 -2
  53. package/docs/examples/README.md +1 -1
  54. package/docs/examples/app-shell/README.md +39 -1
  55. package/docs/examples/app-shell/package.json +14 -6
  56. package/docs/examples/app-shell/smoke-kit.mjs +54 -0
  57. package/docs/examples/app-shell/src/auth.ts +4 -2
  58. package/docs/examples/app-shell/src/service.ts +2 -2
  59. package/docs/examples/app-shell/tsconfig.json +4 -1
  60. package/docs/examples/app-shell/worker-configuration.d.ts +2 -2
  61. package/docs/examples/app-shell/wrangler.jsonc +5 -4
  62. package/docs/examples/generated-web/README.md +6 -2
  63. package/docs/examples/generated-web/wrangler.jsonc +1 -0
  64. package/docs/examples/purchasing/README.md +1 -1
  65. package/docs/examples/purchasing/manifests/purchasing.yaml +1 -1
  66. package/docs/examples/purchasing/smoke.mjs +10 -4
  67. package/docs/examples/purchasing/src/admin.tsx +13 -0
  68. package/docs/examples/purchasing/wrangler.jsonc +5 -4
  69. package/docs/examples/recipes/wrangler.jsonc +5 -4
  70. package/docs/handbook/cloudflare/authentication.md +22 -2
  71. package/docs/handbook/cloudflare/deploy-and-operate.md +21 -0
  72. package/docs/handbook/cloudflare/service-entry.md +55 -28
  73. package/docs/handbook/concepts/authorization.md +1 -1
  74. package/docs/handbook/concepts/mcp-and-agents.md +19 -0
  75. package/docs/handbook/examples/hub.md +1 -1
  76. package/docs/handbook/guides/admin-ui.md +107 -1
  77. package/docs/handbook/guides/agent-setup.md +27 -9
  78. package/docs/handbook/guides/mcp-apps.md +445 -6
  79. package/docs/handbook/guides/native-admin.md +296 -51
  80. package/docs/handbook/guides/oauth.md +61 -5
  81. package/docs/handbook/guides/public-pages.md +2 -2
  82. package/docs/handbook/guides/size-and-startup.md +1 -1
  83. package/docs/handbook/navigation.json +0 -4
  84. package/docs/handbook/reference/admin-descriptors.md +0 -35
  85. package/docs/handbook/reference/procedure.md +5 -1
  86. package/docs/handbook/reference/schema.md +13 -3
  87. package/docs/handbook/reference/surface.md +123 -13
  88. package/docs/handbook/reference/trigger.md +34 -1
  89. package/docs/handbook/reference/view.md +1 -1
  90. package/docs/handbook/releases/index.md +74 -2
  91. package/docs/handbook/start/overview.md +1 -1
  92. package/docs/handbook/start/project-and-cli.md +8 -8
  93. package/docs/handbook/start/quickstart-worker.md +1 -1
  94. package/docs/skills/develop/SKILL.md +4 -3
  95. package/docs/skills/integrate/SKILL.md +1 -1
  96. package/docs/skills/media-gc/SKILL.md +7 -5
  97. package/docs/skills/provision/SKILL.md +4 -1
  98. package/docs/skills/update/SKILL.md +1 -1
  99. package/docs/upgrade-0.1-to-0.2.md +6 -6
  100. package/package.json +2 -1
  101. package/skills/README.md +11 -16
  102. package/skills/mantle/SKILL.md +4 -4
  103. package/skills/mantle/references/local-testing.md +4 -0
  104. package/skills/mantle/references/web.md +1 -1
  105. package/skills/mantle/references/website.md +24 -22
  106. package/docs/examples/app-shell/src/admin.ts +0 -10
  107. package/docs/examples/purchasing/src/admin.ts +0 -12
  108. package/docs/handbook/guides/admin-extensions.md +0 -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": "node_modules/@aotter/mantle-admin/dist/admin",
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": "node_modules/@aotter/mantle-admin/dist/admin",
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,22 @@ Mantle supplies the database adapter, schema generation and caller resolution:
12
12
 
13
13
  ## `auth.ts`
14
14
 
15
- Copy `src/auth.ts` from the [app shell](../../examples/app-shell/README.md), which CI runs from the packed packages.
15
+ Copy `src/auth.ts` from the [app shell](../../examples/app-shell/README.md), which CI runs from the packed packages;
16
+ installed, it is `node_modules/@aotter/mantle/docs/examples/app-shell/src/auth.ts`. Its essentials:
17
+
18
+ - Imports: `betterAuth` from `better-auth/minimal`; `admin`, `emailOTP` (and `jwt` for MCP) from `better-auth/plugins`;
19
+ `createAccessControl` from `better-auth/plugins/access`; `defaultStatements` from `better-auth/plugins/admin/access`;
20
+ `d1Adapter` from `@aotter/mantle/auth/adapter`; the hardening and bootstrap helpers below. For MCP it also imports `jwt`,
21
+ `mcp` from `@better-auth/mcp`, `cimd` from `@better-auth/cimd` and `oauthGrantExtension`/`oauthGrantRevocation` from
22
+ `@aotter/mantle/auth/oauth`; the file marks that block as deletable.
23
+ - `authOptions(env)` returns plain options and is the module's export for `--auth src/auth.ts`. Generation calls it with an
24
+ empty `env`, so it must not throw: fall back to placeholders for `secret` and origin there.
25
+ - The bootstrap admin: `d1BootstrapAdmin(env.DB, { match: "email", value: env.ADMIN_EMAIL }, () => ({ options: { plugins } }))`
26
+ is assigned to `databaseHooks.user.create.after` and `.update.after` ([Identity helpers](#identity-helpers)).
27
+ - `createAuth(env)` is the same module's second export: it checks its three variables are set and returns
28
+ `betterAuth(authOptions(env))`. The Worker's `authLoader(() => import("./auth.js").then(m => m.createAuth(env)))`
29
+ ([service composition](service-entry.md)) is what calls it.
30
+
16
31
  The application owns Better Auth and its optional plugins. Schema generation reads plain options without constructing
17
32
  Better Auth; apply `schema.sql` during deployment, never at boot. OAuth resource
18
33
  rows are created on first OAuth use from runtime configuration; they require no
@@ -154,7 +169,12 @@ Better Auth adapter code stays outside the Worker startup graph.
154
169
 
155
170
  `@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
171
 
157
- `ctx.admin` role names are open and must match `^[a-z][a-zA-Z0-9_]{0,62}$`. Bare `ctx.admin` admits any admin; a list admits admins holding one of its roles. Better Auth’s `adminRoles` decides who is an admin. Member role names alone grant no privilege.
172
+ Bare `ctx.admin` admits any verified admin layer classified by Better Auth's
173
+ `adminRoles` / `adminUserIds`. Role lists in `requires` are refused; decide
174
+ per-target permissions in `authorize(target, caller)`, using `hasAdminRole`
175
+ when appropriate. Verified role names match `^[a-z][a-zA-Z0-9_-]{0,62}$`.
176
+ Member role names alone grant no privilege. See
177
+ [authorization requirements](../reference/authorization.md).
158
178
 
159
179
  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
180
 
@@ -5,6 +5,27 @@ 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.
@@ -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 { adminAssets } from './admin-assets.js'; // app-owned SPA shell, assets and CSP
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 ORIGIN, AUTH_SECRET and ADMIN_EMAIL secrets in Wrangler.
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.ORIGIN}/mcp`, `${env.ORIGIN}/mcp/admin`],
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
- const asset = await adminAssets(request, env);
78
- if (asset) return asset;
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' } });
@@ -88,28 +91,32 @@ export default {
88
91
  Remove imports and array entries for omitted surfaces. `mcp.ts` exists only when
89
92
  the manifest has an MCP surface, and requires the documented optional MCP peer.
90
93
  Pass the same handlers to each selected surface that exposes ref Procedures.
91
- Bind site/media/identity and extension capabilities through the selected Admin/MCP
94
+ Bind site/media/identity capabilities through the selected Admin/MCP
92
95
  options; do not expose native auth administrative routes without `authRoutes` or
93
96
  `guardAuthAdminRequest`. `withAuthTasks` retains background callbacks on the
94
97
  current request's context; `authHardening` configures the background handler.
95
98
  Select `--admin` to emit `admin-bindings.ts`. Admin's API is a separate
96
99
  `@aotter/mantle-admin` package; the SDK never imports it. The API handles only
97
- `/admin/api`. The app-owned asset route serves the built shell for `/admin` and
98
- client routes, including `/admin/sign-in`, and preserves missing asset 404s.
99
- Allow its built import map through the exact CSP hash documented by the installed
100
- Admin package. Collection statistics use the generated shared `statisticsView`:
100
+ `/admin/api`. `adminShell` (in the block above) serves the built shell for `/admin` and
101
+ client routes, including `/admin/sign-in`; it sets `frame-ancestors 'none'`, `X-Frame-Options: DENY`, `no-store` and `nosniff`
102
+ on the shell, answers a missing file under `/admin` with a 404 `Response`, and returns `undefined` for other paths and
103
+ non-GET requests. The app owns the rest of the CSP and the asset cache headers.
104
+ Collection statistics use the generated shared `statisticsView`:
101
105
  its collection parameter selects only a generated table and visibility policy.
102
106
 
103
107
  ## Workers assets and secrets
104
108
 
105
109
  `mantle generate cloudflare-d1 generated manifests/*.yaml --auth src/auth.ts --admin`
106
- updates the split-build Wrangler keys. Configure the rest explicitly; JSONC
110
+ updates the split-build Wrangler keys (`main` and `base_dir` under `dist/worker`),
111
+ ensures `nodejs_compat` and defaults to `global_fetch_strictly_public` ([public fetch routing](deploy-and-operate.md#public-fetch-routing))
112
+ and, with `--admin`, `assets.directory`: `mantle bundle` builds Admin into
113
+ `dist/client/admin`, served at `/admin`. Configure the rest explicitly; JSONC
107
114
  comments and unrelated settings survive regeneration:
108
115
 
109
116
  ```jsonc
110
117
  {
111
118
  "assets": {
112
- "directory": "node_modules/@aotter/mantle-admin/dist/admin",
119
+ "directory": "dist/client",
113
120
  "binding": "ASSETS",
114
121
  "run_worker_first": true,
115
122
  "html_handling": "none"
@@ -119,16 +126,18 @@ comments and unrelated settings survive regeneration:
119
126
  ```
120
127
 
121
128
  `run_worker_first` lets the application supply shell/security/cache headers;
122
- `none` keeps `index.html` available by name. Admin assets are not a public database
129
+ `none` keeps `index.html` available by name. `adminShell(request, assets)` passes
130
+ paths relative to the Admin directory, so the app fetches them under `/admin/`:
131
+ `path => env.ASSETS.fetch(new Request(new URL("/admin/" + path, request.url), request))`. Admin assets are not a public database
123
132
  API. PostgreSQL uses application-owned Hyperdrive/postgres.js wiring,
124
133
  with Hyperdrive query caching disabled for authority and mutation visibility.
125
134
 
126
135
  Commit `.dev.vars.example` containing variable names and placeholders only:
127
136
 
128
137
  ```dotenv
129
- ORIGIN=http://localhost:8787
138
+ PUBLIC_ORIGIN=http://localhost:8787
130
139
  ADMIN_EMAIL=you@example.test
131
- AUTH_SECRET=replace-with-a-random-secret
140
+ BETTER_AUTH_SECRET=replace-with-a-random-secret
132
141
  ```
133
142
 
134
143
  Copy it to ignored `.dev.vars`; use deployment secrets for production. Configure
@@ -137,8 +146,7 @@ an app-owned [`EmailSender`](../guides/email.md#application-wiring) and call its
137
146
  callbacks. Adapt the auth template's `MAIL.otp`/`MAIL.link` callbacks to this
138
147
  sender; it is an object, not a `.dev.vars` value. The auth
139
148
  module wires `ADMIN_EMAIL` to the bootstrap helper, which requires verified email.
140
- `PUBLIC_ORIGIN` can be your application variable instead of `ORIGIN`; map it
141
- explicitly when constructing auth and keep the trusted origin consistent.
149
+ Name the variables as your `auth.ts` reads them (the app shell uses these three) and keep the trusted origin consistent.
142
150
 
143
151
  ## Scheduling and Bun
144
152
 
@@ -150,22 +158,42 @@ invocation; see the [TTL contract](../guides/native-execution.md#ttl-maintenance
150
158
  larger sweeps' cursors for a later invocation instead of scanning without a bound.
151
159
 
152
160
  On Bun, reuse a process-owned `Database` or `SQL` pool and call the same generated
153
- surfaces from `Bun.serve({fetch})`. The optional host reader is independent of
154
- the query target:
161
+ surfaces from `Bun.serve({fetch})`. Bun runs the server as TypeScript, so
162
+ `mantle bundle` builds only Admin for it: the `bun-*` target that
163
+ `generated/.mantle-inputs.json` records skips the Worker bundle, and Admin goes
164
+ to `dist/client/admin` (`--assets` as on Workers) when the server imports
165
+ `@aotter/mantle-admin/server`. Add it as the build script and run it before
166
+ starting the server:
167
+
168
+ ```json
169
+ { "scripts": { "build": "mantle bundle src/server.ts dist" } }
170
+ ```
171
+
172
+ The optional host reader is independent of the query target:
155
173
 
156
174
  ```ts
157
175
  import { bunAdminAssets } from '@aotter/mantle/bun/assets';
158
- const assets = bunAdminAssets('node_modules/@aotter/mantle-admin/dist/admin');
176
+ const assets = bunAdminAssets('dist/client/admin');
159
177
  const bindings = adminBindings({ handlers });
160
178
  const serveAdmin = admin(bindings.metadata, bindings.views, bindings.operations, {
161
179
  db: () => db, caller, handlers, routes: entries(bindings.schemas, bindings.adapter),
162
180
  });
163
- // Call assets(path) from your separate shell/static route.
181
+ // Call assets(path) from your separate shell/static route. adminShell(request, reader) wants
182
+ // paths without a leading slash and a Response (404 when missing), so adapt the reader:
183
+ const reader = async (path: string) => (await assets('/' + path)) ?? new Response(null, { status: 404 });
164
184
  ```
165
185
 
166
186
  The reader returns a native `Response` or `null`, rejects traversal/control
167
187
  characters and escaping symlinks; the app owns shell, CSP and cache headers.
168
- Use ignored `.env` on Bun. Run cron work with your native scheduler; neither
188
+ Use ignored `.env` on Bun.
189
+
190
+ Admin's build tools and UI packages are dependencies of `@aotter/mantle-admin`,
191
+ so a production install carries them (about 65 MB). For a container image, use a
192
+ multi-stage build: the build stage installs everything and runs `mantle bundle`
193
+ and `bun build src/server.ts --target bun --outdir dist/server`; the runtime stage
194
+ copies only `dist/` (plus any files the server reads at runtime) and starts
195
+ `bun dist/server/server.js`. Neither the Admin assets nor the bundled server read
196
+ `node_modules` once built. Run cron work with your native scheduler; neither
169
197
  imports nor ordinary requests execute DDL, migrations or cron work. OAuth resource
170
198
  rows are inserted on first OAuth use from runtime configuration, with no deploy
171
199
  step; auth initialization and cookie/bearer/MCP/REST requests do no resource work.
@@ -190,13 +218,12 @@ untracked. Deployment uses committed generated source; it does not regenerate.
190
218
  # CI, with the project's exact installed SDK and peer versions:
191
219
  pnpm exec mantle generate cloudflare-d1 generated manifests/*.yaml --auth src/auth.ts --admin --check
192
220
  pnpm exec tsc --noEmit
193
- pnpm exec mantle bundle src/worker.ts dist
221
+ pnpm exec mantle bundle src/worker.ts dist # dist/worker, and dist/client/admin with --admin
194
222
  ```
195
223
 
196
224
  `--check` regenerates in memory, lists missing/changed/obsolete SDK-owned files
197
225
  and stale Wrangler build settings, exits nonzero, and writes nothing. Unmarked
198
226
  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,
227
+ exact Wrangler `--check` requires JSON/JSONC. Use all the same `--site`, `--auth` and `--admin` options as normal generation. The check needs no database,
201
228
  network access, credentials or deployment. Imported configuration modules must
202
229
  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. Application-owned resolvers mint the original Caller with `userCaller` from `@aotter/mantle/user-fields`, using only a verified user and the identity provider’s adminRoles. 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.
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 |
@@ -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/blob/develop/docs/examples/reference-service/README.md) exercises the native package integration. See the [generated public website](../../examples/generated-web/README.md) for a native Worker example.
5
+ The runnable [reference service](https://github.com/aotter/mantle/tree/v0.2.0-alpha.8/docs/examples/reference-service) exercises the native package integration. See the [generated public website](../../examples/generated-web/README.md) for a native Worker example.
@@ -8,4 +8,110 @@ Admin Views provide custom read lists; their SELECT outputs, column lineage and
8
8
 
9
9
  Generic publishing edits apply to drafts; publishing transitions retain their atomic guards and OCC. Read-only collections refuse generic edits while declared Procedures can still run. Ref operations receive the caller/transaction-bound Store.
10
10
 
11
- Use [extensions](admin-extensions.md) for app-owned pages, actions, panels or field renderers. Keep exact conflict/uncertain-outcome handling in the shared UI controller.
11
+ Use [`defineAdmin` components](native-admin.md#customising-admin) for app-owned cells, inputs, record asides and actions; server work is a Procedure, a View or an application route. Keep exact conflict/uncertain-outcome handling in the shared UI controller.
12
+
13
+ ## Theme
14
+
15
+ Admin is themed with CSS variables. Import your own stylesheet from `src/admin.tsx` (`import "./admin.css"`) and override the public tokens on `:root` for light and on `.dark` for dark:
16
+
17
+ ```css
18
+ :root {
19
+ --primary: #0f766e;
20
+ --primary-foreground: #ffffff;
21
+ --radius: 0.5rem;
22
+ }
23
+ .dark {
24
+ --primary: #2dd4bf;
25
+ --primary-foreground: #042f2e;
26
+ }
27
+ ```
28
+
29
+ Admin marks the active theme as both the `.dark` class and `data-theme="dark"` (or `"light"`) on the root. Dark tokens and Tailwind's `dark:` variant apply when either is present, so `[data-theme="dark"] {}` is equivalent to `.dark {}`. Your overrides apply in the browser Admin and, because the entry imports your CSS, in the [MCP App](mcp-apps.md#theme) too, where the host's neutral colours are laid over them.
30
+
31
+ | Token | Meaning |
32
+ | --- | --- |
33
+ | `--background` | Page background |
34
+ | `--foreground` | Default text |
35
+ | `--card` | Card and panel surface |
36
+ | `--card-foreground` | Text on a card |
37
+ | `--popover` | Menu, popover and dialog surface |
38
+ | `--popover-foreground` | Text on a popover |
39
+ | `--primary` | Brand color: primary buttons, active states |
40
+ | `--primary-foreground` | Text on `--primary` |
41
+ | `--secondary` | Secondary button surface |
42
+ | `--secondary-foreground` | Text on `--secondary` |
43
+ | `--muted` | Subtle surface: table stripes, disabled areas |
44
+ | `--muted-foreground` | Secondary text |
45
+ | `--accent` | Hover and selected surface |
46
+ | `--accent-foreground` | Text on `--accent` |
47
+ | `--destructive` | Destructive actions and errors |
48
+ | `--border` | Borders and dividers |
49
+ | `--input` | Form control borders |
50
+ | `--ring` | Focus ring |
51
+ | `--radius` | Base corner radius; the other radii derive from it |
52
+ | `--success` | Success status color |
53
+ | `--warning` | Warning status color |
54
+ | `--info` | Informational status color |
55
+ | `--chart-1` | Chart series 1 |
56
+ | `--chart-2` | Chart series 2 |
57
+ | `--chart-3` | Chart series 3 |
58
+ | `--chart-4` | Chart series 4 |
59
+ | `--chart-5` | Chart series 5 |
60
+ | `--sidebar` | Sidebar surface |
61
+ | `--sidebar-foreground` | Sidebar text |
62
+ | `--sidebar-primary` | Sidebar active item |
63
+ | `--sidebar-primary-foreground` | Text on `--sidebar-primary` |
64
+ | `--sidebar-accent` | Sidebar hover surface |
65
+ | `--sidebar-accent-foreground` | Text on `--sidebar-accent` |
66
+ | `--sidebar-border` | Sidebar borders |
67
+ | `--sidebar-ring` | Sidebar focus ring |
68
+ | `--app-base` | Solid color behind the page |
69
+ | `--app-background` | Page backdrop (any CSS background, such as a gradient) |
70
+
71
+ Every other variable, including `--mantle-blue-*` and `--chrome-glass`, is internal and may change without notice.
72
+
73
+ ## Eject a component
74
+
75
+ `@aotter/mantle-admin` ships a [shadcn registry](https://ui.shadcn.com/docs/registry) of starting-point components for the `defineAdmin` slots. Ejecting copies one into your source: you own that file and edit it freely, while the hooks it calls (`@aotter/mantle-admin/hooks`) and the slot prop types (`@aotter/mantle-admin/app`) keep upgrading with the package. The registry is inside the installed package, so the copy matches the version you run and no registry host is involved (the shadcn CLI itself downloads from npm and needs network). Install from the package, in the app's root:
76
+
77
+ ```bash
78
+ npx shadcn@4 add ./node_modules/@aotter/mantle-admin/dist/registry/admin-cell.json
79
+ ```
80
+
81
+ The file lands in `src/admin/<item>.tsx` (the item's `target` ignores aliases). The CLI needs a `tsconfig.json` and a `components.json` in the app's root; this minimal one works, because no alias or stylesheet is used:
82
+
83
+ ```json
84
+ {
85
+ "$schema": "https://ui.shadcn.com/schema.json",
86
+ "style": "new-york",
87
+ "rsc": false,
88
+ "tsx": true,
89
+ "tailwind": { "config": "", "css": "", "baseColor": "neutral", "cssVariables": true },
90
+ "aliases": { "components": "@/components", "utils": "@/lib/utils" }
91
+ }
92
+ ```
93
+
94
+ The CLI removes a comment above a file's first import, so each item keeps its header below the imports. Import it in `src/admin.tsx` and name the slot, replacing the sample collection, field, View and operation with your own:
95
+
96
+ ```tsx
97
+ import { AdminCell } from "./admin/admin-cell";
98
+
99
+ export default defineAdmin(admin, { components: { "cell:requests.status": AdminCell } });
100
+ ```
101
+
102
+ | Item | Slots | Surface |
103
+ | --- | --- | --- |
104
+ | `admin-cell` | `cell:view:<view>.<column>` | both (also reusable as a browser collection cell) |
105
+ | `admin-input` | `input:<schema>.<field>` | browser |
106
+ | `admin-record-actions` | `record.actions:<schema>` (runs an operation with `useOperation`) | browser |
107
+ | `admin-record-aside` | `record.aside:<schema>` (lists a View's rows with `useView`) | browser |
108
+ | `admin-sidebar` | `Sidebar` (`renderDefault` with a footer link) | browser |
109
+ | `admin-home` | `Home` (`renderDefault` under a section that reads a View) | browser |
110
+
111
+ Each file starts with a comment naming its surface and rules, and imports only `react`, `@aotter/mantle-admin/app` and `@aotter/mantle-admin/hooks`. A `both` component also renders in the Admin MCP App, inside a host's sandboxed iframe, so keep it to these rules:
112
+
113
+ - Read data only through `useView` and `useOperation`; `useEntries`, `useEntry`, `useCollectionTotal`, `useMe` and `useBadges` read `unavailable` there. Declare what a card needs as an admin View.
114
+ - Link only with `<AdminLink>` or `useOpenLink()`; never `window.open` or `target="_blank"`.
115
+ - No `fetch`, no `document.cookie` or `localStorage` authentication, no remote origin that the host's CSP does not allow, and no assumed width.
116
+ - Where `useOperation` returns no `run` (a host without server tools), show the control disabled with a note rather than throwing or vanishing.
117
+ - Branch on `useAdminSurface()` (or the `surface` prop) if you must, and render something sensible in both branches.
@@ -12,24 +12,42 @@ carries.
12
12
 
13
13
  The Mantle plugin carries one OSS authoring skill, `mantle`, and local
14
14
  acceptance references. It does not configure a hosted-service connection.
15
+ Install from the Git tag matching the exact SDK version you selected (currently
16
+ `v0.2.0-alpha.8`). Alpha releases integrate on `develop`; an unqualified
17
+ `aotter/mantle` source follows `main` and can install older, incompatible
18
+ instructions. A skill install does not upgrade the application's SDK.
15
19
 
16
- ```bash
17
- # Claude Code: two separate prompts
18
- /plugin marketplace add aotter/mantle
19
- /plugin install mantle@mantle
20
+ For Codex, pin the marketplace checkout:
20
21
 
21
- # Codex
22
- codex plugin marketplace add aotter/mantle
22
+ ```sh
23
+ codex plugin marketplace add aotter/mantle --ref v0.2.0-alpha.8
23
24
  codex plugin add mantle@mantle
24
25
  ```
25
26
 
26
- Cursor and GitHub Copilot read the plugin manifests from the repository.
27
- Without a plugin host, install the skill alone:
27
+ For Claude Code, first clone the release into a directory you will keep:
28
+
29
+ ```sh
30
+ git clone --depth 1 --branch v0.2.0-alpha.8 https://github.com/aotter/mantle.git mantle-plugin
31
+ ```
32
+
33
+ Then, in Claude Code, use that checkout's absolute path:
34
+
35
+ ```text
36
+ /plugin marketplace add /absolute/path/to/mantle-plugin
37
+ /plugin install mantle@mantle
38
+ ```
39
+
40
+ Cursor and GitHub Copilot use their plugin manifests from the same tagged
41
+ checkout. Without a plugin host, install only the consumer skill by name:
28
42
 
29
43
  ```sh
30
- npx skills add aotter/mantle
44
+ npx skills add https://github.com/aotter/mantle/tree/v0.2.0-alpha.8 --skill mantle
31
45
  ```
32
46
 
47
+ When upgrading the SDK, update the marketplace ref or local tagged checkout and
48
+ refresh/reinstall the plugin using the host's native workflow. Verify the loaded
49
+ manifest version and skill source. Do not reset a checkout containing local edits.
50
+
33
51
  Read the path the installer prints (for project-local Codex,
34
52
  `.agents/skills/mantle/SKILL.md`). The skill installs no SDK, creates no
35
53
  project and copies no handbook.