@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.
- package/dist/admin/design.d.ts +7 -0
- package/dist/admin/design.d.ts.map +1 -0
- package/dist/admin/design.js +18 -0
- package/dist/admin/design.js.map +1 -0
- package/dist/admin/developerConsole.d.ts +1 -0
- package/dist/admin/developerConsole.d.ts.map +1 -1
- package/dist/admin/developerConsole.js.map +1 -1
- package/dist/admin/index.d.ts +2 -0
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +2 -0
- package/dist/admin/index.js.map +1 -1
- package/dist/auth/buildAuth.d.ts +6 -5
- package/dist/auth/buildAuth.d.ts.map +1 -1
- package/dist/auth/buildAuth.js +5 -1
- package/dist/auth/buildAuth.js.map +1 -1
- package/dist/auth/createMantleAuth.d.ts.map +1 -1
- package/dist/auth/createMantleAuth.js +6 -11
- package/dist/auth/createMantleAuth.js.map +1 -1
- package/dist/cli/generate.d.ts.map +1 -1
- package/dist/cli/generate.js +38 -2
- package/dist/cli/generate.js.map +1 -1
- package/dist/core/observation.d.ts +3 -0
- package/dist/core/observation.d.ts.map +1 -0
- package/dist/core/observation.js +10 -0
- package/dist/core/observation.js.map +1 -0
- package/dist/core/withCaller.d.ts +8 -0
- package/dist/core/withCaller.d.ts.map +1 -1
- package/dist/core/withCaller.js +10 -4
- package/dist/core/withCaller.js.map +1 -1
- package/dist/mcp/createMcpSurface.d.ts +24 -1
- package/dist/mcp/createMcpSurface.d.ts.map +1 -1
- package/dist/mcp/createMcpSurface.js +21 -0
- package/dist/mcp/createMcpSurface.js.map +1 -1
- package/dist/spec/infrastructure/sql/PgQueryParser.js +1 -1
- package/dist/spec/infrastructure/sql/PgQueryParser.js.map +1 -1
- package/dist/spec/infrastructure/sql/compileSql.js +2 -2
- package/dist/spec/infrastructure/sql/compileSql.js.map +1 -1
- package/dist/web/createRestSurface.d.ts.map +1 -1
- package/dist/web/createRestSurface.js +5 -3
- package/dist/web/createRestSurface.js.map +1 -1
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +20 -0
- package/docs/adr/0031-host-cloud-core-contract.md +17 -1
- package/docs/adr/0032-store-centric-core-and-service-contract.md +6 -0
- package/docs/adr/README.md +1 -1
- package/docs/examples/procurement.md +12 -1
- package/docs/examples/reference-service/package.json +6 -6
- package/docs/examples/reference-service/smoke.mjs +6 -2
- package/docs/handbook/cloudflare/authentication.md +84 -1
- package/docs/handbook/cloudflare/deploy-and-operate.md +15 -4
- package/docs/handbook/concepts/authorization.md +7 -1
- package/docs/handbook/concepts/mcp-and-agents.md +13 -1
- package/docs/handbook/guides/admin-ui.md +29 -0
- package/docs/handbook/releases/index.md +7 -2
- package/docs/handbook/start/overview.md +1 -1
- package/docs/handbook/start/project-and-cli.md +7 -0
- package/docs/handbook/start/quickstart-worker.md +5 -4
- package/docs/skills/develop/SKILL.md +10 -0
- package/docs/skills/develop/mcp-app.md +1 -1
- package/docs/skills/provision/SKILL.md +4 -3
- package/docs/skills/update/SKILL.md +6 -0
- 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
|
|
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.
|
|
12
|
-
"@aotter/mantle-ui": "0.2.0-alpha.
|
|
13
|
-
"@better-auth/cimd": "1.7.
|
|
14
|
-
"@better-auth/mcp": "1.7.
|
|
15
|
-
"@better-auth/oauth-provider": "1.7.
|
|
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.
|
|
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="(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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`),
|
|
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
|
-
- **
|
|
60
|
-
|
|
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
|
|
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
|
|
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
|
|
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 |
|
|
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
|
|
62
|
-
|
|
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.
|
|
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.
|
|
92
|
-
"@better-auth/core": "1.7.
|
|
93
|
-
"@better-auth/mcp": "1.7.
|
|
94
|
-
"@better-auth/oauth-provider": "1.7.
|
|
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.
|
|
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.
|
|
110
|
-
"@better-auth/mcp": "1.7.
|
|
111
|
-
"@better-auth/oauth-provider": "1.7.
|
|
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.
|
|
116
|
+
"better-auth": "1.7.7"
|
|
115
117
|
},
|
|
116
118
|
"peerDependenciesMeta": {
|
|
117
119
|
"@better-auth/cimd": {
|