@aotter/mantle 0.2.0-alpha.2 → 0.2.0-alpha.4

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 (61) hide show
  1. package/dist/admin/design.d.ts +7 -0
  2. package/dist/admin/design.d.ts.map +1 -0
  3. package/dist/admin/design.js +18 -0
  4. package/dist/admin/design.js.map +1 -0
  5. package/dist/admin/developerConsole.d.ts +1 -0
  6. package/dist/admin/developerConsole.d.ts.map +1 -1
  7. package/dist/admin/developerConsole.js.map +1 -1
  8. package/dist/admin/index.d.ts +2 -0
  9. package/dist/admin/index.d.ts.map +1 -1
  10. package/dist/admin/index.js +2 -0
  11. package/dist/admin/index.js.map +1 -1
  12. package/dist/auth/buildAuth.d.ts +6 -5
  13. package/dist/auth/buildAuth.d.ts.map +1 -1
  14. package/dist/auth/buildAuth.js +5 -1
  15. package/dist/auth/buildAuth.js.map +1 -1
  16. package/dist/auth/createMantleAuth.d.ts.map +1 -1
  17. package/dist/auth/createMantleAuth.js +6 -11
  18. package/dist/auth/createMantleAuth.js.map +1 -1
  19. package/dist/cli/generate.d.ts.map +1 -1
  20. package/dist/cli/generate.js +38 -2
  21. package/dist/cli/generate.js.map +1 -1
  22. package/dist/core/observation.d.ts +3 -0
  23. package/dist/core/observation.d.ts.map +1 -0
  24. package/dist/core/observation.js +10 -0
  25. package/dist/core/observation.js.map +1 -0
  26. package/dist/core/withCaller.d.ts +8 -0
  27. package/dist/core/withCaller.d.ts.map +1 -1
  28. package/dist/core/withCaller.js +10 -4
  29. package/dist/core/withCaller.js.map +1 -1
  30. package/dist/mcp/createMcpSurface.d.ts +24 -1
  31. package/dist/mcp/createMcpSurface.d.ts.map +1 -1
  32. package/dist/mcp/createMcpSurface.js +21 -0
  33. package/dist/mcp/createMcpSurface.js.map +1 -1
  34. package/dist/spec/infrastructure/sql/PgQueryParser.js +1 -1
  35. package/dist/spec/infrastructure/sql/PgQueryParser.js.map +1 -1
  36. package/dist/spec/infrastructure/sql/compileSql.js +2 -2
  37. package/dist/spec/infrastructure/sql/compileSql.js.map +1 -1
  38. package/dist/web/createRestSurface.d.ts.map +1 -1
  39. package/dist/web/createRestSurface.js +5 -3
  40. package/dist/web/createRestSurface.js.map +1 -1
  41. package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +20 -0
  42. package/docs/adr/0031-host-cloud-core-contract.md +17 -1
  43. package/docs/adr/0032-store-centric-core-and-service-contract.md +6 -0
  44. package/docs/adr/README.md +1 -1
  45. package/docs/examples/procurement.md +12 -1
  46. package/docs/examples/reference-service/package.json +6 -6
  47. package/docs/examples/reference-service/smoke.mjs +6 -2
  48. package/docs/handbook/cloudflare/authentication.md +84 -1
  49. package/docs/handbook/cloudflare/deploy-and-operate.md +15 -4
  50. package/docs/handbook/concepts/authorization.md +7 -1
  51. package/docs/handbook/concepts/mcp-and-agents.md +13 -1
  52. package/docs/handbook/guides/admin-ui.md +29 -0
  53. package/docs/handbook/releases/index.md +7 -2
  54. package/docs/handbook/start/overview.md +1 -1
  55. package/docs/handbook/start/project-and-cli.md +7 -0
  56. package/docs/handbook/start/quickstart-worker.md +5 -4
  57. package/docs/skills/develop/SKILL.md +10 -0
  58. package/docs/skills/develop/mcp-app.md +1 -1
  59. package/docs/skills/provision/SKILL.md +4 -3
  60. package/docs/skills/update/SKILL.md +6 -0
  61. package/package.json +14 -12
@@ -33,6 +33,7 @@ spec:
33
33
  schema:
34
34
  type: object
35
35
  additionalProperties: false
36
+ readOnly: true
36
37
  required: [requestNumber, requestedBy, item, quantity, needBy, justification, requestStatus]
37
38
  properties:
38
39
  requestNumber: { type: string, maxLength: 40, pattern: "^REQ-[A-Z0-9-]+$" }
@@ -154,6 +155,15 @@ spec:
154
155
  could not see the queue. Scope is for rows only their owner may ever see.
155
156
  Here the member View filters on `requestedBy = auth.uid()` instead, and
156
157
  `requires: ctx.user` means an anonymous caller is 401 before the query runs.
158
+ - **Generic Admin access is a separate entrance.** The View's SQL filter and
159
+ `requires` protect that View, not generic Schema entry routes. Root
160
+ `schema.readOnly: true` blocks generic Admin create, patch and delete, so
161
+ every write goes through the declared Procedures. It does not block reads:
162
+ Admin admits contributors by default, and this Schema is not scoped. If
163
+ only owner/editor reviewers may read everyone's requisitions, restrict the
164
+ Admin surface in the application; see [Roles](../handbook/guides/admin-ui.md#roles).
165
+ Keep that restriction inside the existing `withCaller` boundary. Members
166
+ and contributors use the owner-filtered public View instead.
157
167
  - **A Schema whose name is not a plain identifier is quoted in SQL**:
158
168
  `"purchase-requisitions"`.
159
169
 
@@ -204,7 +214,8 @@ staff surface.
204
214
  ## What this leaves out
205
215
 
206
216
  - **Approval chains.** One decision by one staff member. Sequential approvers
207
- need a step field and a `before_update` hook that checks the transition.
217
+ need a step field and SQL conditions on the current step and caller. Use a
218
+ hook or guard only for a check SQL cannot express.
208
219
  - **Budgets.** A live budget check belongs in a `requires.guard` handler.
209
220
  - **Notifications.** See the `after_create` hook in
210
221
  [Intake with bot check and notification](./intake-hooks.md).
@@ -8,14 +8,14 @@
8
8
  "check": "mantle generate && mantle generate --check && tsc --noEmit && node smoke.mjs"
9
9
  },
10
10
  "dependencies": {
11
- "@aotter/mantle": "0.2.0-alpha.2",
12
- "@aotter/mantle-ui": "0.2.0-alpha.2",
13
- "@better-auth/cimd": "1.7.2",
14
- "@better-auth/mcp": "1.7.2",
15
- "@better-auth/oauth-provider": "1.7.2",
11
+ "@aotter/mantle": "0.2.0-alpha.4",
12
+ "@aotter/mantle-ui": "0.2.0-alpha.4",
13
+ "@better-auth/cimd": "1.7.7",
14
+ "@better-auth/mcp": "1.7.7",
15
+ "@better-auth/oauth-provider": "1.7.7",
16
16
  "@modelcontextprotocol/ext-apps": "2.0.0",
17
17
  "@modelcontextprotocol/server": "2.1.0",
18
- "better-auth": "1.7.2"
18
+ "better-auth": "1.7.7"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@cloudflare/workers-types": "5.20260904.1",
@@ -113,10 +113,14 @@ try {
113
113
  // the console: @aotter/mantle-ui/admin served by Admin at /admin, its assets by name, any other path its client-side route
114
114
  const shell = await fetch(`${origin}/admin/c/items`);
115
115
  const html = await shell.text();
116
- const script = /src="(\/admin\/assets\/[^"]+\.js)"/.exec(html)?.[1];
116
+ const script = /src="((?:\/admin\/|\.\/)assets\/[^"]+\.js)"/.exec(html)?.[1];
117
+ const base = /<base href="([^"]+)"/.exec(html)?.[1] ?? "/admin/";
117
118
  assert.equal(shell.headers.get("x-frame-options"), "DENY");
118
119
  assert.ok(script, html.slice(0, 300));
119
- assert.equal((await fetch(`${origin}${script}`)).status, 200);
120
+ const scriptUrl = new URL(script, new URL(base, origin));
121
+ assert.equal(scriptUrl.origin, origin);
122
+ assert.ok(scriptUrl.pathname.startsWith("/admin/assets/"));
123
+ assert.equal((await fetch(scriptUrl)).status, 200);
120
124
  assert.equal((await fetch(`${origin}/admin/assets/missing.js`)).status, 404);
121
125
  // the editor's round trip: it reads the whole entry and writes it back, an unset optional field included as null
122
126
  const [tea] = (await call("GET", "/admin/api/entries?collection=items", { cookie: staff })).body.items;
@@ -42,6 +42,54 @@ passes `bunAuthDatabase(sql)` and `bunDatabaseDriver(sql)` (or the bun:sqlite
42
42
 
43
43
  ### Local sign-in
44
44
 
45
+ Install auth and MCP dependencies at the versions declared in the installed
46
+ Core package's `peerDependencies`; the generator prints a versioned install
47
+ command and refuses incompatible installed peers before writing generated files.
48
+ The preset owns convergence; a normal fresh local database needs no manual auth
49
+ migration. `auth.ready` initializes Better Auth's context; auth tables are prepared
50
+ lazily before authenticated operations. Mantle runs Better Auth's explicit schema
51
+ validation after that preparation, including when the schema ledger is current;
52
+ an incompatible existing schema still rejects authentication.
53
+
54
+ #### Upgrading an existing 1.7.0–1.7.2 auth database
55
+
56
+ Those Better Auth versions created a required `account.issuer` column that
57
+ 1.7.7 no longer writes. Automatic additive convergence preserves that column;
58
+ it cannot make a required unused column safe for future inserts. Before starting
59
+ the upgraded service, the database owner must migrate this legacy constraint.
60
+ Fresh databases need no such step.
61
+
62
+ For PostgreSQL, first inspect the service's own database/schema and back it up:
63
+
64
+ ```sql
65
+ SELECT table_schema, is_nullable, column_default
66
+ FROM information_schema.columns
67
+ WHERE table_name = 'account' AND column_name = 'issuer';
68
+ ```
69
+
70
+ If the legacy column is required and has no default, apply this in the service's
71
+ auth schema (the example assumes it is selected by `search_path`):
72
+
73
+ ```sql
74
+ BEGIN;
75
+ ALTER TABLE "account" ALTER COLUMN "issuer" DROP NOT NULL;
76
+ COMMIT;
77
+ ```
78
+
79
+ This retains every row and existing issuer value. It changes only the obsolete
80
+ constraint; new accounts may leave that column null. Restart the service after
81
+ the migration so Better Auth checks the repaired schema in a fresh context.
82
+ Verify existing users, roles, sessions and content rows, then sign in again.
83
+ Do not drop auth tables or disable schema validation. This applies to native Bun
84
+ PostgreSQL and Cloudflare/Hyperdrive PostgreSQL alike.
85
+
86
+ For D1/SQLite, `ALTER COLUMN` is unavailable. Use a database-owner-reviewed table
87
+ rebuild that retains every column/value, index and foreign-key relationship while
88
+ making the legacy issuer nullable; do not run the PostgreSQL statement there.
89
+ See Better Auth's [1.7 upgrade guide](https://www.better-auth.com/docs/guides/1-7-upgrade-guide).
90
+ Mantle's portable auth facade does not silently perform engine-specific table
91
+ rebuilds or remove historical fields.
92
+
45
93
  ```sh
46
94
  cp .dev.vars.example .dev.vars # ADMIN_EMAIL, a random BETTER_AUTH_SECRET, PUBLIC_ORIGIN=http://127.0.0.1:8787
47
95
  pnpm exec wrangler dev --local
@@ -57,6 +105,17 @@ origin and both secrets exist. Otherwise it uses
57
105
  `createSetupIncompleteAuth`, which refuses sign-in with a message, so a
58
106
  deployed service never prints codes to its log.
59
107
 
108
+ On Bun, copy `.env.example` to `.env`, set `PUBLIC_ORIGIN=http://127.0.0.1:3000`
109
+ and run `bun src/index.ts`; send the same HTTP requests to port 3000. Codes print
110
+ to the Bun console. Normalize test email addresses to lowercase when matching
111
+ that output. Treat codes and session cookies as secrets in test logs.
112
+
113
+ Local clients share the loopback socket IP and therefore share sign-in limits.
114
+ Multi-user smoke tests must respect the retry window (HTTP 429) rather than
115
+ spoofing forwarding headers or disabling production limits. `rateLimit: {
116
+ window: 60, max: 100 }` changes the general quota, but plugin-specific OTP limits
117
+ still apply; changing it is not a way to bypass those limits.
118
+
60
119
  ### MCP clients for members
61
120
 
62
121
  `/admin/sign-in` suits staff. When members connect an MCP client to the public
@@ -75,7 +134,31 @@ const { url } = await res.json();
75
134
  location.assign(url ?? "/account");
76
135
  ```
77
136
 
78
- Dynamic client registration takes a client that names no `application_type`
137
+ The generated preset does not enable dynamic client registration (DCR).
138
+ An `access_denied` response from `/api/auth/oauth2/register` does not mean
139
+ member OAuth is broken. An owner can register a known local client through
140
+ the public `MantleAuth` API, without opening DCR:
141
+
142
+ ```ts
143
+ const client = await auth.registerOAuthClient({
144
+ requestHeaders: ownerRequest.headers,
145
+ clientName: "Local MCP client",
146
+ redirectUris: ["http://127.0.0.1:3456/callback"],
147
+ applicationType: "native",
148
+ tokenEndpointAuthMethod: "none",
149
+ grantTypes: ["authorization_code", "refresh_token"],
150
+ responseTypes: ["code"],
151
+ scope: ["mcp"],
152
+ requirePKCE: true,
153
+ });
154
+ ```
155
+
156
+ Use a real owner session, keep persisted user consent, and configure the MCP
157
+ client with the returned `clientId`. This belongs in an owner-controlled
158
+ management path, not an anonymous registration handler. Choose DCR separately
159
+ according to the host's registration policy.
160
+
161
+ When DCR is enabled, registration takes a client that names no `application_type`
79
162
  and registers only `http` loopback redirects (`localhost`, `127.0.0.1`,
80
163
  `[::1]`) as `native` (RFC 8252), which is how local MCP clients register; any
81
164
  other registration is checked as Better Auth checks it (`web` needs `https` on a
@@ -99,10 +99,21 @@ you built (`PLAN_FINGERPRINT_MISMATCH`). The fingerprint is in
99
99
 
100
100
  ## Mantle Cloud
101
101
 
102
- Deploying a 0.2.0 service to Mantle Cloud needs Cloud's host protocol 3, which
103
- uploads the compiled plan and the service entry. Mantle Cloud does not accept
104
- 0.2.0 services yet. The plugin's `mantle` skill and its helper script check
105
- the plan and the installed Core version so a project is ready when it does.
102
+ Mantle Cloud accepts its pinned 0.2 service contract through host protocol 4. A verified source receipt is required before save.
103
+ Connect Cloud MCP and follow the plugin's `mantle` skill and its
104
+ `references/cloud.md` workflow. Call `cloud_host_contract` for the selected
105
+ project before installing or packing: Cloud returns the exact Core version
106
+ and revision. Do not substitute a moving npm tag or upload a self-hosted
107
+ Worker's platform wiring. Cloud supplies identity and D1 storage; PostgreSQL
108
+ plans are not accepted.
109
+
110
+ The skill's `scripts/mantle-cloud.mjs` helper orchestrates save, paired
111
+ preview and publish through the connected MCP tools. Follow its literal
112
+ `nextAction`, keeping grants on stdin and preserving the project and operation
113
+ IDs on retry. A saved or paired version is not a serving deployment; publish
114
+ only under the user's authorization and verify Cloud's returned serving state.
115
+ Its offline `check` reports `cloud: "not_checked"`, which proves local
116
+ readiness only. Use Cloud's workflow, not `wrangler deploy`, for that target.
106
117
 
107
118
  ## Moving data from 0.1.x
108
119
 
@@ -117,7 +117,13 @@ The writer comes from the caller, never from an input. See
117
117
 
118
118
  Admin's gate admits any staff role, and each Admin route names the least role
119
119
  it needs. Admin reads and writes through `runtime.store.as(caller)`, so scope
120
- and `requires` apply to staff exactly as to anyone. See
120
+ applies to generic entry routes. A View or Procedure's `requires` applies when
121
+ that atom runs, including Admin reports and operations; it does not restrict
122
+ generic entry reads or writes. Root Schema `readOnly: true` blocks generic
123
+ writes, while reads still follow Admin's role gate and Schema scope.
124
+ For an unscoped table shared by members and reviewers, an owner-filtered View
125
+ does not hide its rows from contributors using generic Admin routes. Compose
126
+ a stricter Admin surface in the application when the product requires it. See
121
127
  [Customize Admin](../guides/admin-ui.md).
122
128
 
123
129
  ## Further reading
@@ -50,7 +50,7 @@ again.
50
50
 
51
51
  `createMantleAuth` runs Better Auth's OAuth provider with `mcpResource:
52
52
  {PUBLIC_ORIGIN}/mcp`. An MCP client discovers it from the 401 challenge's
53
- `resource_metadata` (`/.well-known/oauth-protected-resource/mcp`), registers,
53
+ `resource_metadata` (`/.well-known/oauth-protected-resource/mcp`), uses a registered client,
54
54
  sends the user through sign-in and consent (Admin's `/admin/oauth/consent`, or the plain `/oauth/consent`), and calls `/mcp`
55
55
  with the token. The resolver (`createCallerResolver(auth, { jwtBearer: { audience, scopes: ["mcp"] } })`)
56
56
  verifies it on every request and reads the user's current role; nothing is
@@ -123,6 +123,18 @@ On the public surface, a member-facing App is the application's to build; the
123
123
  SDK attaches none. The `develop` skill's
124
124
  [MCP App recipe](../../skills/develop/mcp-app.md) lists the pieces.
125
125
 
126
+ ## Native metadata observations
127
+
128
+ `createMcpSurface` optionally takes `onObservation(event)`. Registered tool dispatch emits `kind: "invocation"` with `phase: "attempt"` then `phase: "completion"`, sharing `invocationId` with the runtime's MCP cause. Metadata includes time, surface, MCP tool name and a frozen copy of the resolved Caller; completion adds monotonic `durationMs`, `outcome` (`succeeded`, `failed`, `denied`) and an optional diagnostic code. Runtime auth, guard or input-validation refusal is a dispatched invocation with a failed/denied completion; it does not assert that a business handler executed or committed. The Caller contract uses opaque subject/credential record IDs, never emails or raw tokens. Input, output and diagnostic messages are absent. A host must still allowlist what it exports.
129
+
130
+ The surface's existing authorization gates emit `kind: "request-refused"` with time, surface and HTTP status. A refused batch emits one request refusal and no invocation observations. Unknown/client-hidden tools and protocol validation errors do not fabricate executed invocations. These are separate measurements: request rejections must not be added to executed-call totals.
131
+
132
+ `withCaller` optionally takes `onRefusal(event)` for identity or session-origin rejection before a surface runs. It emits only time, status and `reason: "credential" | "origin"`; it cannot identify a tool in a body it has not parsed. A host registers both hooks to cover the two boundaries without resolving identity twice.
133
+
134
+ Callbacks can return `void` or a delivery promise. Delivery promises are not awaited; throws/rejections are contained and never change a response. Synchronous listener work still runs inline: keep it short. The host owns request-scoped background scheduling, transport, failure reporting and retention. The simplest request binding is to construct `createMcpSurface` and `withCaller` inside the host's request handler, with callbacks closing over that request's background-task function; reuse the already-created runtime, not a new runtime/Auth. Surface construction traverses its plan's tool catalog. A host that reuses surfaces can instead use its existing request-local context facility, such as AsyncLocalStorage, to retrieve the current execution context. Never close over the first request's context or share its delivery promise with another request. With Streamable HTTP, tool execution and completion observations may happen after `fetch` returns its Response, while its body is consumed; keep the request-bound context available through that lifetime.
135
+
136
+ Attempt/completion can arrive out of order, a completion may be absent, and capture is best-effort. `succeeded` describes native execution/serialization, not successful transport delivery afterward. `durationMs` uses the host's `performance.now()` clock; its precision and advancement depend on the host. This is observability, not a billing or transactional audit guarantee. Omitting both callbacks preserves current dispatch/auth behavior and adds no provider dependency or storage statement. These callbacks cover this native MCP surface, not Admin WebMCP or every runtime source. The two refusal hooks have different shapes because caller rejection occurs before a surface has identified a tool; the adopter may project them into its own request-rejection metadata.
137
+
126
138
  ## Further reading
127
139
 
128
140
  - [Reads: Views, REST and MCP](./views.md)
@@ -121,6 +121,35 @@ and manages media, `owner` manages staff, site settings and the developer
121
121
  console. Operations and staff Views are further limited by their own
122
122
  `requires`.
123
123
 
124
+ Those atom predicates do not restrict generic Schema entry routes. A root
125
+ Schema `readOnly: true` blocks generic writes, not reads. In a service where
126
+ only owner/editor reviewers may read an unscoped table, restrict the Admin
127
+ surface in the application-owned `src/service.ts`:
128
+
129
+ ```ts
130
+ // Replace the preset's Admin composition, keeping its existing options.
131
+ const adminSurface = createAdminSurface(runtime, { /* existing Admin options */ });
132
+ const admin = withCaller(resolver, async (request, caller) => {
133
+ if (new URL(request.url).pathname.startsWith("/admin/api/") &&
134
+ caller.kind === "user" && caller.role === "contributor") {
135
+ return Response.json({ error: {
136
+ code: "AUTH_DENIED", message: "Admin access requires an owner or editor role.",
137
+ } }, { status: 403 });
138
+ }
139
+ return adminSurface(request, caller);
140
+ });
141
+ ```
142
+
143
+ The preset already imports `withCaller` from `@aotter/mantle`. Reuse its
144
+ resolver so credential validation and the session origin guard run once;
145
+ Admin retains its own gates for members and anonymous callers. Contributors
146
+ can use an application-owned page over an owner-filtered public View. Keep
147
+ sign-in, OAuth consent and UI assets reachable; restricting the whole `/admin`
148
+ prefix would also block member MCP consent at `/admin/oauth/consent`. This is
149
+ host surface composition, not a Procedure `ref` handler or a new Manifest
150
+ permission key. Exercise generic entry reads, reports and operations with
151
+ each role, including after revoking a role in an existing session.
152
+
124
153
  ## Verify a change
125
154
 
126
155
  1. Run `mantle generate`, then `mantle generate --check`.
@@ -56,8 +56,13 @@ by hand.
56
56
  `emit-openapi`, `skills` and `mantle-harness` are removed.
57
57
  - **The Admin console** moves to `@aotter/mantle-ui/admin`, served at `/admin`
58
58
  by the preset through the service's `ASSETS` binding.
59
- - **Not yet in 0.2.0:** Mantle-rendered public pages, the MCP interaction App
60
- tools, and deploying to Mantle Cloud.
59
+ - **Mantle Cloud** uses its pinned Core contract and host protocol 4 through
60
+ the plugin's `mantle` Cloud workflow, requiring a verified source receipt
61
+ before save, with separate preview and publish
62
+ states. Its host contract decides the accepted version; offline readiness
63
+ is not a deployed service.
64
+ - **Not yet in 0.2.0:** Mantle-rendered public pages and the MCP interaction
65
+ App tools.
61
66
 
62
67
  ## 0.1.4 — 2026-09-24
63
68
 
@@ -43,6 +43,7 @@ After that they are your files.
43
43
  | Look up a field | [Feature table](../reference/features.md), then the [Schema](../reference/schema.md), [View](../reference/view.md), [Procedure](../reference/procedure.md) and [Trigger](../reference/trigger.md) references |
44
44
  | Wire sign-in, media or deploy | [Authentication](../cloudflare/authentication.md), [Media uploads with R2](../cloudflare/media-r2.md), [Deploy and operate](../cloudflare/deploy-and-operate.md) |
45
45
  | Run on ChatGPT Sites | [Mantle on ChatGPT Sites](../cloudflare/chatgpt-sites.md) |
46
+ | Save, preview and publish on Mantle Cloud | [Mantle Cloud](../cloudflare/deploy-and-operate.md#mantle-cloud), then the plugin's `mantle` Cloud workflow |
46
47
  | See whole services | [Examples](../examples/hub.md) and the runnable reference service |
47
48
  | Move a 0.1.x project | `docs/upgrade-0.1-to-0.2.md` in the installed package |
48
49
 
@@ -60,4 +61,3 @@ check the code.
60
61
  `createMantle`. The Bun preset has no scheduler.
61
62
  - **Site settings and media on PostgreSQL.** Only the `sqlite` dialect's
62
63
  storage takes `SiteDefaults`; see [Site defaults](../reference/site-config.md).
63
- - **Deploying to Mantle Cloud.** See [Deploy and operate](../cloudflare/deploy-and-operate.md).
@@ -59,6 +59,13 @@ and stops before writing anything, naming the install command for your
59
59
  package manager. It never installs anything. The host and dialect rows, and the
60
60
  staff MCP App, are checked only while the preset is still to be written.
61
61
 
62
+ Use the versions in the installed Core's `package.json` `peerDependencies`,
63
+ not registry latest, for optional auth and MCP packages. The diagnostic's
64
+ install command includes those versions and incompatible installed peers are
65
+ refused before generation. Keep `@aotter/mantle-ui` at Core's
66
+ exact version. A package manager accepting an incompatible peer does not make
67
+ it a supported combination; inspect peer warnings before starting the service.
68
+
62
69
  | Selection | Packages |
63
70
  |---|---|
64
71
  | always | `@aotter/mantle` |
@@ -112,12 +112,13 @@ pnpm exec mantle generate
112
112
  ```
113
113
 
114
114
  With no flags the selection is identity `mantle` and every feature. The first
115
- run names the packages it needs, with the install command, and writes nothing
116
- until they are installed:
115
+ run names the packages it needs, with versioned install commands from the
116
+ installed Core's `peerDependencies`, and writes nothing until they are installed
117
+ at compatible versions. Copy those commands rather than installing registry
118
+ latest; `@aotter/mantle-ui` must match Core's exact version:
117
119
 
118
120
  ```sh
119
- pnpm add better-auth @better-auth/oauth-provider @better-auth/mcp @better-auth/cimd \
120
- @modelcontextprotocol/server @modelcontextprotocol/ext-apps @aotter/mantle-ui
121
+ # Run each versioned pnpm add command printed above first.
121
122
  pnpm exec mantle generate
122
123
  cp .dev.vars.example .dev.vars # set ADMIN_EMAIL and a random BETTER_AUTH_SECRET
123
124
  pnpm exec wrangler dev --local
@@ -66,6 +66,11 @@ Choose in this order, stopping at the first that works:
66
66
  published-only into public Views. Never repeat them.
67
67
  - `scope` hides rows from staff too. When staff must see every row, store
68
68
  `auth.uid()` in a field and filter on it instead.
69
+ - That filter protects the View, not generic Admin Schema reads. Root
70
+ `schema.readOnly: true` blocks generic writes only. If a contributor must
71
+ not read other users' rows, restrict the application-owned Admin surface
72
+ inside its existing `withCaller` boundary; see
73
+ `docs/handbook/guides/admin-ui.md` (Roles). Keep business transitions in SQL.
69
74
 
70
75
  ## Handlers
71
76
 
@@ -112,6 +117,11 @@ Then exercise what changed over HTTP: the REST View or Trigger, `/mcp`
112
117
  sign-in and the Admin route (`docs/handbook/cloudflare/authentication.md`).
113
118
  Check the stored rows, not only a 200. Before deploying a Schema change on
114
119
  SQLite, run `mantle generate --check --database <file>` to see the storage SQL.
120
+ For a workflow, verify valid and invalid state transitions, ownership on every
121
+ mounted surface (including generic Admin reads and writes), concurrent writes
122
+ with one expected version, and persisted data and sessions after a restart.
123
+ Exercise the actual UI when the product includes one; serving HTML alone does
124
+ not verify its forms or operations.
115
125
 
116
126
  ## Rules
117
127
 
@@ -19,7 +19,7 @@ decision. Build it case by case from these pieces.
19
19
  | `SchemaFields` and the primitives | `@aotter/mantle-ui/kit`, styled by `@aotter/mantle-ui/kit.css` | Admin's form controls |
20
20
 
21
21
  The App's own build installs what it imports: `@modelcontextprotocol/ext-apps`
22
- at the exact version `@aotter/mantle` depends on, React and the kit's peers
22
+ at the exact version in the installed Core's `peerDependencies`, React and the kit's peers
23
23
  (`@aotter/mantle-ui`'s README lists them). `docs/handbook/concepts/mcp-and-agents.md`
24
24
  (MCP Apps) describes the catalog and the staff App these share.
25
25
 
@@ -23,7 +23,7 @@ Ask which target, unless the project already shows one.
23
23
  |---|---|---|
24
24
  | Cloudflare Workers | `wrangler.jsonc` | the rest of this skill |
25
25
  | ChatGPT Sites | the Site's hosting config | installed `docs/handbook/cloudflare/chatgpt-sites.md`; publish through Sites, never `wrangler deploy` |
26
- | Mantle Cloud | — | not available for 0.2 services yet; the plugin's `mantle` skill says when it is |
26
+ | Mantle Cloud | Cloud project link | the plugin's `mantle` skill and its Cloud workflow; connect Cloud MCP and use its exact host contract, not `wrangler deploy` |
27
27
  | Bun (host `bun`) | `.env` | self-hosted; installed `docs/handbook/start/project-and-cli.md` (Bun), not this skill |
28
28
 
29
29
  Self-hosting is always an option; never present a hosted target as required.
@@ -58,8 +58,9 @@ Confirm the active Cloudflare account with the user before creating anything.
58
58
  and `bootstrapOwner: { match: "github-login", value }`. The user creates a
59
59
  GitHub OAuth App with callback `<PUBLIC_ORIGIN>/api/auth/callback/github`.
60
60
  Remove the preset's loopback-only check once the method is real.
61
- 4. **Secrets.** Never in chat, files or logs. Use a connector, or hidden
62
- input:
61
+ 4. **Secrets.** Never commit them or put them in chat, logs or `wrangler.jsonc`
62
+ `vars`. Store production values in the deployment's secret store. Use a
63
+ connector, or hidden input:
63
64
 
64
65
  ```bash
65
66
  openssl rand -hex 32 | pnpm exec wrangler secret put BETTER_AUTH_SECRET
@@ -27,6 +27,12 @@ Upgrade deliberately; never overwrite application-owned code.
27
27
  `@aotter/mantle-ui/admin`), and update the lockfile with the
28
28
  package manager. Review the dependency diff and any peer changes
29
29
  `mantle generate` names.
30
+ With identity `mantle`, inspect auth database upgrade requirements in
31
+ `docs/handbook/cloudflare/authentication.md` before starting the new service.
32
+ Better Auth 1.7.0–1.7.2's required legacy `account.issuer` needs a
33
+ data-preserving host-specific constraint migration when moving to 1.7.7;
34
+ fresh-database success alone does not verify an upgrade. Back up the service's
35
+ own database and retain existing users, sessions, roles and content.
30
36
  5. Regenerate and check:
31
37
 
32
38
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aotter/mantle",
3
- "version": "0.2.0-alpha.2",
3
+ "version": "0.2.0-alpha.4",
4
4
  "description": "Mantle: a manifest-driven service core. Store authored as SQL, surfaces as Fetch functions, and the mantle CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://mantle.tools/",
@@ -85,33 +85,35 @@
85
85
  "libpg-query": "18.1.5",
86
86
  "pgsql-deparser": "18.3.8",
87
87
  "yaml": "^2.9.0",
88
- "zod": "^4.5.4"
88
+ "zod": "^4.5.4",
89
+ "semver": "^7.8.5"
89
90
  },
90
91
  "devDependencies": {
91
- "@better-auth/cimd": "1.7.2",
92
- "@better-auth/core": "1.7.2",
93
- "@better-auth/mcp": "1.7.2",
94
- "@better-auth/oauth-provider": "1.7.2",
92
+ "@better-auth/cimd": "1.7.7",
93
+ "@better-auth/core": "1.7.7",
94
+ "@better-auth/mcp": "1.7.7",
95
+ "@better-auth/oauth-provider": "1.7.7",
95
96
  "@better-auth/utils": "0.4.2",
96
97
  "@modelcontextprotocol/client": "2.1.0",
97
98
  "@modelcontextprotocol/ext-apps": "2.0.0",
98
99
  "@modelcontextprotocol/server": "2.1.0",
99
100
  "@types/node": "^26",
100
101
  "@types/pg": "8.23.1",
101
- "better-auth": "1.7.2",
102
+ "better-auth": "1.7.7",
102
103
  "bun-types": "1.3.14",
103
104
  "pg": "8.23.1",
104
105
  "typescript": "^6.0.3",
105
106
  "vitest": "^4.1.11",
106
- "wrangler": "4.126.0"
107
+ "wrangler": "4.126.0",
108
+ "@types/semver": "^7.7.1"
107
109
  },
108
110
  "peerDependencies": {
109
- "@better-auth/cimd": "1.7.2",
110
- "@better-auth/mcp": "1.7.2",
111
- "@better-auth/oauth-provider": "1.7.2",
111
+ "@better-auth/cimd": "1.7.7",
112
+ "@better-auth/mcp": "1.7.7",
113
+ "@better-auth/oauth-provider": "1.7.7",
112
114
  "@modelcontextprotocol/ext-apps": "2.0.0",
113
115
  "@modelcontextprotocol/server": "2.1.0",
114
- "better-auth": "1.7.2"
116
+ "better-auth": "1.7.7"
115
117
  },
116
118
  "peerDependenciesMeta": {
117
119
  "@better-auth/cimd": {