@aotter/mantle 0.1.0-alpha.1 → 0.1.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -26
- package/dist/admin.d.ts +2 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +2 -0
- package/dist/admin.js.map +1 -0
- package/dist/bun.d.ts +2 -0
- package/dist/bun.d.ts.map +1 -0
- package/dist/bun.js +2 -0
- package/dist/bun.js.map +1 -0
- package/dist/cli/create.d.ts +2 -0
- package/dist/cli/create.d.ts.map +1 -0
- package/dist/cli/create.js +246 -0
- package/dist/cli/create.js.map +1 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +101 -0
- package/dist/cli/generate.js.map +1 -0
- package/dist/cli/harness.d.ts +3 -0
- package/dist/cli/harness.d.ts.map +1 -0
- package/dist/{harness-cli.js → cli/harness.js} +13 -8
- package/dist/cli/harness.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/{cli.js → cli/main.js} +6 -8
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.d.ts.map +1 -0
- package/dist/{skills.js → cli/skills.js} +51 -6
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/{update.js → cli/update.js} +72 -46
- package/dist/cli/update.js.map +1 -0
- package/dist/codegen/emitMantleModule.d.ts +16 -0
- package/dist/codegen/emitMantleModule.d.ts.map +1 -0
- package/dist/codegen/emitMantleModule.js +217 -0
- package/dist/codegen/emitMantleModule.js.map +1 -0
- package/dist/codegen.d.ts +2 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +2 -0
- package/dist/codegen.js.map +1 -0
- package/dist/provision/renderProvisionBundle.d.ts +70 -0
- package/dist/provision/renderProvisionBundle.d.ts.map +1 -0
- package/dist/provision/renderProvisionBundle.js +367 -0
- package/dist/provision/renderProvisionBundle.js.map +1 -0
- package/dist/provision.d.ts +2 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +2 -0
- package/dist/provision.js.map +1 -0
- package/dist/vercel-libsql.d.ts +2 -0
- package/dist/vercel-libsql.d.ts.map +1 -0
- package/dist/vercel-libsql.js +2 -0
- package/dist/vercel-libsql.js.map +1 -0
- package/dist/vercel.d.ts +2 -0
- package/dist/vercel.d.ts.map +1 -0
- package/dist/vercel.js +2 -0
- package/dist/vercel.js.map +1 -0
- package/dist/web.d.ts +2 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +2 -0
- package/dist/web.js.map +1 -0
- package/docs/adapter-guide.md +94 -47
- package/docs/adr/0001-four-atom-manifest-model.md +48 -307
- package/docs/adr/0002-closed-enums-for-bindings.md +6 -15
- package/docs/adr/0007-ai-as-primary-author.md +6 -4
- package/docs/adr/0008-structured-diagnostic-shape.md +2 -2
- package/docs/adr/0009-consumer-supplied-manifests.md +15 -9
- package/docs/adr/0010-locale-and-translates.md +13 -8
- package/docs/adr/0011-adapter-port-spec.md +29 -21
- package/docs/adr/0012-views-as-public-rest.md +5 -9
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +124 -43
- package/docs/adr/0018-core-starters-repository-boundary.md +22 -1
- package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +204 -0
- package/docs/adr/README.md +16 -14
- package/docs/api-mcp-authorization.md +28 -42
- package/docs/assets/mantle-admin-operations.png +0 -0
- package/docs/assets/mantle-hero.jpg +0 -0
- package/docs/auth-hosting-model.md +15 -11
- package/docs/cloudflare-low-level-composition.md +30 -18
- package/docs/deferred-lifecycle-queues.md +20 -38
- package/docs/design-atoms.md +155 -401
- package/docs/labels.md +5 -3
- package/docs/media-uploads.md +9 -2
- package/docs/migration-0.1.2.md +45 -0
- package/docs/performance-harness.md +7 -6
- package/docs/release-process.md +86 -25
- package/docs/schema-indexes.md +1 -1
- package/docs/sealed-pipeline-ownership.md +100 -0
- package/package.json +84 -14
- package/skills/README.md +39 -7
- package/skills/develop/SKILL.md +31 -17
- package/skills/install/SKILL.md +26 -25
- package/skills/media-gc/SKILL.md +85 -0
- package/skills/plugin/SKILL.md +2 -1
- package/skills/provision/SKILL.md +24 -2
- package/skills/theme/SKILL.md +11 -1
- package/skills/update/SKILL.md +1 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/generate.js +0 -181
- package/dist/generate.js.map +0 -1
- package/dist/harness-cli.d.ts +0 -3
- package/dist/harness-cli.d.ts.map +0 -1
- package/dist/harness-cli.js.map +0 -1
- package/dist/skills.d.ts +0 -2
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js.map +0 -1
- package/dist/update.d.ts.map +0 -1
- package/dist/update.js.map +0 -1
- /package/dist/{generate.d.ts → cli/generate.d.ts} +0 -0
- /package/dist/{update.d.ts → cli/update.d.ts} +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**Deciders**: phsu
|
|
8
8
|
|
|
9
|
-
**Related**: [ADR-0001](0001-four-atom-manifest-model.md) (the Schema atom this extends
|
|
9
|
+
**Related**: [ADR-0001](0001-four-atom-manifest-model.md) (the Schema atom this extends).
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -70,7 +70,7 @@ matches the principle that locale is opt-in.
|
|
|
70
70
|
|
|
71
71
|
The boot validator only inspects the manifest at this layer. It
|
|
72
72
|
checks shape (every `localized: true` Schema is well-formed, every
|
|
73
|
-
`translates:` block resolves) and rejects
|
|
73
|
+
`translates:` block resolves) and rejects unsupported keys; it does **not**
|
|
74
74
|
read D1 to confirm that the site actually has any locales configured.
|
|
75
75
|
That cross-check is deferred to runtime (Layer 3).
|
|
76
76
|
|
|
@@ -206,29 +206,35 @@ apiVersion: cms.mantle.aotter.net/v1
|
|
|
206
206
|
kind: Schema
|
|
207
207
|
metadata: { name: products }
|
|
208
208
|
spec:
|
|
209
|
+
title: Products
|
|
210
|
+
localized: false
|
|
209
211
|
schema:
|
|
212
|
+
type: object
|
|
210
213
|
properties:
|
|
211
214
|
slug: { type: string }
|
|
212
215
|
sku: { type: string }
|
|
213
216
|
price: { type: number }
|
|
214
217
|
required: [slug, sku, price]
|
|
215
|
-
|
|
218
|
+
uniqueIndexes: [[slug]]
|
|
216
219
|
---
|
|
217
220
|
apiVersion: cms.mantle.aotter.net/v1
|
|
218
221
|
kind: Schema
|
|
219
222
|
metadata: { name: product-translations }
|
|
220
223
|
spec:
|
|
224
|
+
title: Product translations
|
|
221
225
|
localized: true
|
|
222
226
|
translates:
|
|
223
227
|
parent: products
|
|
224
228
|
on: slug
|
|
225
229
|
schema:
|
|
230
|
+
type: object
|
|
226
231
|
properties:
|
|
227
232
|
slug: { type: string }
|
|
233
|
+
locale: { type: string }
|
|
228
234
|
title: { type: string }
|
|
229
235
|
description: { type: string }
|
|
230
|
-
required: [slug, title]
|
|
231
|
-
|
|
236
|
+
required: [slug, locale, title]
|
|
237
|
+
uniqueIndexes: [[slug, locale]]
|
|
232
238
|
```
|
|
233
239
|
|
|
234
240
|
`Schema.spec.translates` declares the parent/child relationship as
|
|
@@ -245,9 +251,6 @@ all treat the relation as known structure rather than convention:
|
|
|
245
251
|
- Admin UI groups parent + per-locale translation entries together.
|
|
246
252
|
- Boot validate enforces parent existence and join-field presence in
|
|
247
253
|
both parent and child JSON Schemas (manifest shape, no D1 reads).
|
|
248
|
-
- View executor (when `View.join` lands per the future-grammar
|
|
249
|
-
appendix) can auto-join parent + child without per-View
|
|
250
|
-
configuration.
|
|
251
254
|
- AI authoring an entry against the child knows from the manifest
|
|
252
255
|
that there's a parent it must reference by `slug`.
|
|
253
256
|
|
|
@@ -262,6 +265,8 @@ Validation rules introduced:
|
|
|
262
265
|
- `TRANSLATES_REQUIRES_LOCALIZED` — `translates: ...` declared on a
|
|
263
266
|
Schema where `localized` isn't `true`. (A non-localized translation
|
|
264
267
|
table makes no sense.)
|
|
268
|
+
- `TRANSLATES_REQUIRES_CONTENT_FIELD` — the child declares only its join
|
|
269
|
+
field and `locale`, with no locale-specific payload to translate.
|
|
265
270
|
|
|
266
271
|
## Consequences
|
|
267
272
|
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# ADR-0011: Adapter port spec
|
|
2
2
|
|
|
3
|
-
**Status:**
|
|
3
|
+
**Status:** Superseded for Core storage and Admin composition by ADR-0019.
|
|
4
|
+
Retained as the alpha.7 Cloudflare adapter record; `DatabaseDriver` is now an
|
|
5
|
+
implementation detail of the SQLite/D1 `MantleStorageAdapter`, while the asset
|
|
6
|
+
contract belongs to optional `@aotter/mantle-admin`.
|
|
4
7
|
|
|
5
|
-
**Date:** 2026-05-04 (revised 2026-05-09, 2026-05-10, and 2026-08-
|
|
8
|
+
**Date:** 2026-05-04 (revised 2026-05-09, 2026-05-10, 2026-08-11, 2026-08-13, and 2026-08-22).
|
|
6
9
|
|
|
7
10
|
## Context
|
|
8
11
|
|
|
9
12
|
`@aotter/mantle-runtime` is adapter-agnostic. It owns dispatcher, entry-writer, view executor, content-ops, render pipeline, boot validation, and MCP JSON-RPC dispatch. It depends only on `@aotter/mantle-spec` and a small set of TypeScript interfaces it defines itself.
|
|
10
13
|
|
|
11
|
-
`@aotter/mantle-cloudflare` is the only adapter shipping in v0.1.0. It binds the runtime's interfaces against Cloudflare Workers' D1 and ASSETS, and supplies a Better Auth instance (per ADR-0014) for sign-in
|
|
12
|
-
|
|
13
|
-
`@aotter/mantle-netlify` is a v0.2 stub — README only. It exists in the package layout as an engineering forcing function: with N=1 adapter, "adapter-agnostic" silently rots in PR review (a `D1Database` import slips into runtime, then a second, then five). With a second adapter visible in the workspace (even if its impl is a TODO), reviewers have somewhere to point when blocking the slip.
|
|
14
|
+
`@aotter/mantle-cloudflare` is the only adapter shipping in v0.1.0. It binds the runtime's interfaces against Cloudflare Workers' D1 and ASSETS, and supplies a Better Auth instance (per ADR-0014) for sign-in and MCP authorization. Better Auth 1.7 stores OAuth grants in D1; auth storage remains adapter-owned infrastructure, not a runtime port.
|
|
14
15
|
|
|
15
16
|
This ADR fixes the contract so:
|
|
16
17
|
- Future adapter authors have a stable target.
|
|
@@ -21,11 +22,18 @@ The POC accumulated multiple half-decisions about this seam (POC ADR-0015 docume
|
|
|
21
22
|
|
|
22
23
|
## Decision
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
> 0.1.2 amendment: the portable storage input is
|
|
26
|
+
> `PreparedMantleStorage` (`EntryRepository & EntryReader` plus
|
|
27
|
+
> `ViewQueryExecutor`). Hosts either use an official storage adapter with an
|
|
28
|
+
> already-owned client/handle or implement those semantic ports over their own
|
|
29
|
+
> tables. `AssetServer` moved out of Runtime to optional Mantle Admin. The
|
|
30
|
+
> two-port contract below is the historical alpha.7 shape.
|
|
31
|
+
|
|
32
|
+
Alpha.7 had **two required adapter ports**, defined as TypeScript interfaces in `@aotter/mantle-runtime/src/domain/port/`. Concrete adapters provided implementations and injected them into `createCmsRuntime`.
|
|
25
33
|
|
|
26
34
|
| Port | Surface |
|
|
27
35
|
|---|---|
|
|
28
|
-
| `DatabaseDriver` | All persistent state — `entries`, `site_config`, `staff`, `users`,
|
|
36
|
+
| `DatabaseDriver` | All persistent state — `entries`, `site_config`, `staff`, `users`, plus migrations. |
|
|
29
37
|
| `AssetServer` | Static-asset serving for the admin SPA. The runtime hands the adapter an asset path + `Request`; the adapter returns a `Response` with the right MIME and caching. |
|
|
30
38
|
|
|
31
39
|
Rendered public artifacts are not a second storage model. D1 stays canonical;
|
|
@@ -123,7 +131,7 @@ export interface DatabaseDriver {
|
|
|
123
131
|
|
|
124
132
|
The runtime never sees `D1Database`, `Pool` (postgres), or any concrete driver. The `prepare` / `batch` shape is intentionally close to D1's surface (which is itself close to the SQLite C API) — that's the smallest common denominator. Adapters wrap their native driver to this shape.
|
|
125
133
|
|
|
126
|
-
The CF adapter's impl is a thin proxy over `env.DB` (D1). A future Postgres
|
|
134
|
+
The CF adapter's impl is a thin proxy over `env.DB` (D1). A future Postgres adapter can wrap its driver to the same shape.
|
|
127
135
|
|
|
128
136
|
### `AssetServer`
|
|
129
137
|
|
|
@@ -136,12 +144,16 @@ export interface AssetServer {
|
|
|
136
144
|
}
|
|
137
145
|
```
|
|
138
146
|
|
|
139
|
-
CF adapter: wraps `env.ASSETS.fetch(req)`.
|
|
147
|
+
CF adapter: wraps `env.ASSETS.fetch(req)`. Other adapters can use a filesystem or object storage.
|
|
140
148
|
|
|
141
149
|
The admin SPA itself lives in `@aotter/mantle-admin-ui` as a pre-built `dist/`. The adapter binds `AssetServer` to whatever serves that `dist/`; the runtime knows nothing about static asset serving except "ask the port and pass through the response."
|
|
142
150
|
|
|
143
151
|
## How adapters wire ports
|
|
144
152
|
|
|
153
|
+
> Historical alpha.7 example. Current convenience composition is
|
|
154
|
+
> `createMantleWorker`; ADR-0014's 2026-08-22 amendment replaces the OAuth
|
|
155
|
+
> provider shown below with Better Auth 1.7 MCP/CIMD.
|
|
156
|
+
|
|
145
157
|
```ts
|
|
146
158
|
// simplified Cloudflare adapter wiring (post-ADR-0014, amended
|
|
147
159
|
// 2026-05-15 by PR #193's OAuth carve-out).
|
|
@@ -198,7 +210,7 @@ export default createOAuthProvider({
|
|
|
198
210
|
});
|
|
199
211
|
```
|
|
200
212
|
|
|
201
|
-
The runtime gets
|
|
213
|
+
The runtime gets two required adapter ports (`db`, `assets`)
|
|
202
214
|
alongside manifests, handlers, templates, and site defaults. Auth is
|
|
203
215
|
owned by the adapter layer that mounts HTTP/MCP surfaces; the runtime
|
|
204
216
|
receives authenticated context when the adapter dispatches requests.
|
|
@@ -208,19 +220,16 @@ There's no module-global state holding adapter-specific bindings.
|
|
|
208
220
|
|
|
209
221
|
**Hard-enforced boundaries**:
|
|
210
222
|
- `@aotter/mantle-runtime` MUST NOT import `D1Database`, `KVNamespace`, `Fetcher` (CF Workers ASSETS), `@cloudflare/*`, or any other adapter-specific type. CI will lint for this; PR reviewers can grep.
|
|
211
|
-
- A new required port can be added only by amending this ADR and updating
|
|
223
|
+
- A new required port can be added only by amending this ADR and updating every shipping adapter in the same change. Optional feature ports must be documented here and must state when adapters are required to implement them.
|
|
212
224
|
- Removing a port is also possible (if a port is found to overlap or be unnecessary), again by amending this ADR.
|
|
213
225
|
|
|
214
226
|
**Discoverability for adapter authors**:
|
|
215
|
-
- A future
|
|
227
|
+
- A future adapter author reads this ADR + [`docs/adapter-guide.md`](../adapter-guide.md), implements the two required ports, then wires boot and HTTP/MCP surfaces. That's the contract. No hidden state, no implicit assumptions about the HTTP framework.
|
|
216
228
|
|
|
217
229
|
**Test ergonomics**:
|
|
218
230
|
- Each port is small and isolated. Tests can mock individual ports without spinning up D1 or an OAuth provider.
|
|
219
231
|
- The runtime's test suite exercises against in-memory port impls; the adapter's test suite exercises the binding against real CF resources via `wrangler dev` or live deploy.
|
|
220
232
|
|
|
221
|
-
**The Netlify stub's job**:
|
|
222
|
-
- The `@aotter/mantle-netlify` package's README declares a public commitment to an N>=2 adapter world. If a PR adds CF-specific code to runtime, reviewers point at the stub README and reject. The stub doesn't have to ship code to perform its function — its existence is the constraint.
|
|
223
|
-
|
|
224
233
|
## Alternatives considered
|
|
225
234
|
|
|
226
235
|
**(a) Single mega-port** — One `RuntimePorts` interface containing every method (db.prepare, assets.fetch, media.createUpload, …). **Rejected**: leaks the entire surface onto every adapter. Discrete ports keep change blast radius per port.
|
|
@@ -229,7 +238,7 @@ There's no module-global state holding adapter-specific bindings.
|
|
|
229
238
|
|
|
230
239
|
**(c) Function-injection (no interfaces, just functions)** — Runtime accepts a record of functions such as `{ dbPrepare, assetFetch, sessionRead, … }`. **Rejected**: TypeScript interfaces are more discoverable and document grouping.
|
|
231
240
|
|
|
232
|
-
**(d) Plugin pattern (each port is a separate package)** — `@aotter/mantle-port-database`, `@aotter/mantle-port-kv`, etc., and runtime depends on one package per port. **Rejected**: the port set is too small to warrant per-port packages.
|
|
241
|
+
**(d) Plugin pattern (each port is a separate package)** — `@aotter/mantle-port-database`, `@aotter/mantle-port-kv`, etc., and runtime depends on one package per port. **Rejected**: the port set is too small to warrant per-port packages. Ports are TypeScript interfaces in `mantle-runtime`'s `src/domain/port/` directory — that's enough.
|
|
233
242
|
|
|
234
243
|
**(e) gRPC / wire-protocol seam** — Make ports a network protocol so adapters can be in any language. **Rejected**: the runtime is not an external service, it's a TypeScript library that adapters compose into a single Worker / Function. Network seam adds latency, deployment complexity, and operational surface for zero authoring benefit. The ports are in-process; they always will be.
|
|
235
244
|
|
|
@@ -241,27 +250,26 @@ When you're authoring `@aotter/mantle-runtime` code:
|
|
|
241
250
|
2. If a port is missing the method you need, **amend this ADR first** in the same PR, then add the method. Adapters in the same PR.
|
|
242
251
|
3. Tests must use port mocks (in-memory implementations) — never reach into a real D1 from runtime tests.
|
|
243
252
|
|
|
244
|
-
When you're authoring an adapter
|
|
253
|
+
When you're authoring an adapter:
|
|
245
254
|
|
|
246
255
|
1. Read `mantle-runtime/src/domain/port/`. Implement each required port against your runtime's primitives.
|
|
247
256
|
2. Compose the runtime via `createCmsRuntime({ db, assets, manifests, handlers, templates, siteDefaults, ... })`.
|
|
248
257
|
3. Call `runtime.bootInit()` once before serving CMS traffic.
|
|
249
|
-
4. Bind to your HTTP framework
|
|
258
|
+
4. Bind to your HTTP framework.
|
|
250
259
|
5. Provide adapter-owned auth and map sessions/scopes/roles into runtime handler context.
|
|
251
260
|
6. Bundle `@aotter/mantle-admin-ui`'s `dist/` via your runtime's static-asset surface and bind `AssetServer` to it.
|
|
252
261
|
|
|
253
262
|
When you're reviewing a PR:
|
|
254
263
|
|
|
255
264
|
1. Grep the diff for `@cloudflare`, `D1Database`, `KVNamespace`, `Fetcher` — flag any occurrence in `mantle-runtime/`.
|
|
256
|
-
2. If a new port method shows up, check it
|
|
257
|
-
3. If a port shape changed,
|
|
265
|
+
2. If a new port method shows up, check it is also reflected in this ADR.
|
|
266
|
+
3. If a port shape changed, every shipping adapter gets updated in the same PR.
|
|
258
267
|
|
|
259
268
|
## Implementation status
|
|
260
269
|
|
|
261
270
|
- [x] Required port interface files live in `packages/mantle-runtime/src/domain/port/*.ts`.
|
|
262
271
|
- [x] Cloudflare required port implementations live in `packages/adapters/cloudflare/src/bindings/*.ts`.
|
|
263
272
|
- [x] Optional feature port `MediaStorage` (public bucket) is declared but not required by first-run adapters. `PrivateMediaStorage` is v0.2.
|
|
264
|
-
- [x] Netlify stub README references this ADR.
|
|
265
273
|
- [ ] CI lint: forbid `@cloudflare/*` / `D1Database` / `KVNamespace` imports in `mantle-runtime/` (post-v0.1.0; manual review until then)
|
|
266
274
|
|
|
267
275
|
## See also
|
|
@@ -96,8 +96,6 @@ Boot validator gates:
|
|
|
96
96
|
- Every `{ $param: <name> }` ref MUST resolve to a declared param (`VIEW_FILTER_PARAM_REF_UNKNOWN`).
|
|
97
97
|
- Every `{ $param: <name> }` ref MUST appear in `params.required` (`VIEW_FILTER_PARAM_REF_NOT_REQUIRED`).
|
|
98
98
|
|
|
99
|
-
The required-only rule is a v0.1.0 simplification. v0.1.x will promote optional-with-skip semantics (filter clauses referencing missing optional params evaluate to TRUE / no-op) — the runtime compiler already implements drop semantics for forward compatibility, but the parser rejects it today so authors get a clear "not yet" diagnostic.
|
|
100
|
-
|
|
101
99
|
### 6. Response envelope is `{ rows, page, show, hasMore }`
|
|
102
100
|
|
|
103
101
|
```json
|
|
@@ -130,12 +128,11 @@ Query strings arrive as strings; `View.spec.params` declares the JSON Schema typ
|
|
|
130
128
|
|
|
131
129
|
Required params not present → `400 INPUT_VALIDATION_FAILED`. Coercion failure → `400 INPUT_VALIDATION_FAILED`. Unknown query-string keys are silently ignored (lenient v0.1.0; strict mode is a candidate v0.1.x flag).
|
|
132
130
|
|
|
133
|
-
##
|
|
131
|
+
## Current limits
|
|
134
132
|
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
- **DRAFT filter operators** (`contains` / `in` / `like` / `not`). v0.1 keeps comparison operators closed to `eq` / `gt` / `gte` / `lt` / `lte`; field-to-field comparisons remain out of scope.
|
|
133
|
+
- Param refs must be required.
|
|
134
|
+
- Filter operators are closed to `eq` / `gt` / `gte` / `lt` / `lte`;
|
|
135
|
+
field-to-field comparisons are unsupported.
|
|
139
136
|
- **Row-level policy rewriting.** `requires` authorizes the whole View; it does
|
|
140
137
|
not inject per-row visibility predicates. Consumer-specific membership,
|
|
141
138
|
payment, or entitlement checks belong in the optional guard Procedure.
|
|
@@ -149,13 +146,12 @@ Required params not present → `400 INPUT_VALIDATION_FAILED`. Coercion failure
|
|
|
149
146
|
- Cheap pagination + dynamic filters without hand-writing handlers.
|
|
150
147
|
|
|
151
148
|
**Authors lose:**
|
|
152
|
-
-
|
|
149
|
+
- Each named query shape remains an explicit View.
|
|
153
150
|
- No internal-only View surface; choose public/staff or keep the query in a TS
|
|
154
151
|
helper.
|
|
155
152
|
|
|
156
153
|
**Runtime gains:**
|
|
157
154
|
- One executor and response shape cover public/staff REST and MCP reads.
|
|
158
|
-
- Forward-compat for join / group-by / aggregation: envelope generalises by Views declaring `output.kind` later.
|
|
159
155
|
|
|
160
156
|
**Reviewers / future contributors should:**
|
|
161
157
|
- Reject any PR adding `Schema.spec.expose.rest` or a similar Schema-level public-read flag.
|
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
Accepted. Amended 2026-05-14, 2026-05-15, 2026-06-30, 2026-07-15,
|
|
6
|
-
2026-08-03.
|
|
5
|
+
Accepted. Amended 2026-05-14, 2026-05-15, 2026-06-30, 2026-07-15,
|
|
6
|
+
2026-08-03, and 2026-08-22.
|
|
7
7
|
|
|
8
8
|
## Date
|
|
9
9
|
|
|
10
|
-
2026-05-09 (last amended 2026-08-
|
|
10
|
+
2026-05-09 (last amended 2026-08-22)
|
|
11
11
|
|
|
12
12
|
> **Current authority:** the original decision below records the rejected
|
|
13
|
-
> Better-Auth-for-MCP design. The 2026-
|
|
14
|
-
>
|
|
15
|
-
>
|
|
13
|
+
> Better-Auth-for-MCP design. The 2026-08-22 Better Auth 1.7 amendment is
|
|
14
|
+
> authoritative where it conflicts with the 2026-05-15 carve-out; the
|
|
15
|
+
> 2026-07-15 normalized authorization boundary still applies. Operational
|
|
16
|
+
> guidance is maintained against the current adapter/runtime.
|
|
16
17
|
|
|
17
18
|
## Context
|
|
18
19
|
|
|
@@ -44,7 +45,7 @@ A 2026 Workers-friendly auth library — [Better Auth](https://better-auth.com)
|
|
|
44
45
|
- **MCP plugin (`mcp`)** — purpose-built on top of the OAuth 2.1 provider for MCP DCR; auto-mounts `.well-known/oauth-authorization-server` + `.well-known/oauth-protected-resource`, exposes `auth.api.getMcpSession()` for protected-resource validation
|
|
45
46
|
- Account linking with policies (verified-email match + reauth requirement)
|
|
46
47
|
|
|
47
|
-
Better Auth depends on a Kysely / Drizzle / Prisma adapter for the database, not on any Cloudflare-specific service. The auth machinery
|
|
48
|
+
Better Auth depends on a Kysely / Drizzle / Prisma adapter for the database, not on any Cloudflare-specific service. The auth machinery remains platform-agnostic.
|
|
48
49
|
|
|
49
50
|
## Decision (historical baseline; amended below)
|
|
50
51
|
|
|
@@ -64,7 +65,7 @@ Adopt Better Auth as the SDK's full auth surface. It owns:
|
|
|
64
65
|
|
|
65
66
|
The auth runtime stops being an adapter port. `OAuthVerifier` port + `WorkersOAuthVerifier` adapter are deleted. Validating bearer tokens at `/mcp` and `/staff/mcp` becomes `auth.api.getMcpSession(req.raw)` — a direct Better Auth API call, no port indirection.
|
|
66
67
|
|
|
67
|
-
This makes the runtime more platform-agnostic, not less: Better Auth runs on Workers (D1 via Kysely), Bun (sqlite), Node (postgres) without
|
|
68
|
+
This makes the runtime more platform-agnostic, not less: Better Auth runs on Workers (D1 via Kysely), Bun (sqlite), and Node (postgres) without runtime changes.
|
|
68
69
|
|
|
69
70
|
### 2. `staff` table → `user.role` via Better Auth admin plugin
|
|
70
71
|
|
|
@@ -81,23 +82,18 @@ admin({
|
|
|
81
82
|
|
|
82
83
|
The manifest grammar predicate `requires.auth.all: [{ "ctx.staff": ["editor"] }]` evaluates against `session.user.role` at runtime. Closed enum membership unchanged.
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
### 3. Two explicit MCP surfaces
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
```
|
|
91
|
-
predicate contains ctx.staff: [...] → tool exposed on /staff/mcp only
|
|
92
|
-
predicate only ctx.user / no predicate → tool exposed on /mcp only
|
|
93
|
-
```
|
|
87
|
+
`/mcp` and `/staff/mcp` are mounted side-by-side from boot. `/staff/mcp`
|
|
88
|
+
exposes staff authoring/lifecycle tools and `/mcp` exposes declared public
|
|
89
|
+
Views and Procedures. An MCP Trigger explicitly chooses `surface: public |
|
|
90
|
+
staff`; the target's `requires.auth` still gates every call.
|
|
94
91
|
|
|
95
92
|
Tool partition rules:
|
|
96
93
|
|
|
97
94
|
- Per-collection auto-emitted authoring tools (`create_draft_<schema>`, `update_draft_<schema>`) — predicate baked-in to require `ctx.staff: [contributor+]`; route to `/staff/mcp`
|
|
98
95
|
- `list_entries` / `get_entry` / `request_publish` / `archive_entry` / `unpublish_entry` — staff-only (return drafts, mutate state); `/staff/mcp` only
|
|
99
|
-
- `query_view_<name>`
|
|
100
|
-
- v0.2 community / v0.2.x fan-club user-facing writes (comment, reaction, subscribe, ...) — predicate `ctx.user` or `ctx.user.subscription`; `/mcp`
|
|
96
|
+
- `query_view_<name>` follows `View.spec.surface`.
|
|
101
97
|
|
|
102
98
|
### 4. Scope-aware DCR via Better Auth `oauthProvider`
|
|
103
99
|
|
|
@@ -140,7 +136,7 @@ The token can carry `role` via `customAccessTokenClaims` for caller convenience,
|
|
|
140
136
|
|
|
141
137
|
### 6. Single auth surface, no port indirection
|
|
142
138
|
|
|
143
|
-
Auth is no longer an adapter port. `mantle-runtime` does NOT define an auth port and `createCmsRuntime()` does not accept auth. Adapter packages
|
|
139
|
+
Auth is no longer an adapter port. `mantle-runtime` does NOT define an auth port and `createCmsRuntime()` does not accept auth. Adapter packages construct the Better Auth instance with the right database adapter for their platform and keep it in the adapter-owned HTTP/MCP mount layer.
|
|
144
140
|
|
|
145
141
|
The adapter uses Better Auth to validate sessions, MCP bearer tokens, scopes, and roles, then passes authenticated user/staff context into runtime dispatchers. Better Auth remains platform-agnostic, but it is not a runtime dependency.
|
|
146
142
|
|
|
@@ -188,7 +184,7 @@ This makes the implicit explicit. The SDK's auth surface is committee-curated; u
|
|
|
188
184
|
|
|
189
185
|
### 8. Path to `@aotter/mantle-better-auth` separate package (deferred)
|
|
190
186
|
|
|
191
|
-
|
|
187
|
+
Each adapter owns its Better Auth wiring. Today the seam is:
|
|
192
188
|
|
|
193
189
|
- `Auth` interface lives in the adapter (could move to runtime or a separate package without breaking the contract — adapters consume the type, not the implementation).
|
|
194
190
|
- `createAuth.ts` is the only file with `import { betterAuth }` (~290 LOC, no Cloudflare-binding-specific code outside `config.database: D1Database`).
|
|
@@ -201,10 +197,10 @@ The future split looks like:
|
|
|
201
197
|
@aotter/mantle-runtime ← ports + use cases (today)
|
|
202
198
|
@aotter/mantle-better-auth ← createAuth + EmailSender impls + appleClientSecret (new, when needed)
|
|
203
199
|
@aotter/mantle-cloudflare ← Workers adapter; depends on (or accepts) Auth-shape (today)
|
|
204
|
-
|
|
200
|
+
future adapter ← same contract, implemented when needed
|
|
205
201
|
```
|
|
206
202
|
|
|
207
|
-
The pivot point — when to extract — is when
|
|
203
|
+
The pivot point — when to extract — is when a second adapter needs the same wiring. Until then, in-place co-location is cheaper than a new package boundary.
|
|
208
204
|
|
|
209
205
|
## Consequences
|
|
210
206
|
|
|
@@ -241,7 +237,6 @@ The pivot point — when to extract — is when the second adapter (`mantle-netl
|
|
|
241
237
|
- `databaseHooks.user.create.after` for `ensureBootstrapOwner` semantics
|
|
242
238
|
- Two `/.well-known/oauth-protected-resource/*` metadata endpoints (Better Auth helpers)
|
|
243
239
|
- Public View MCP tools: dispatcher emits `query_view_<name>` on `/mcp`.
|
|
244
|
-
- Future manifest grammar tools: dispatcher will read `Procedure.requires.auth.all` to route user-facing tools to `/mcp` or `/staff/mcp`.
|
|
245
240
|
- Skills + docs updates for the dual MCP URL handoff
|
|
246
241
|
|
|
247
242
|
### Backward compatibility
|
|
@@ -263,20 +258,9 @@ User MCP URL: https://<worker>.workers.dev/mcp (give to visitors / t
|
|
|
263
258
|
The publication starter repo's production smoke recipe uses `/mcp/staff`
|
|
264
259
|
for the MCP operator smoke step.
|
|
265
260
|
|
|
266
|
-
### Future-proof for v0.2
|
|
267
|
-
|
|
268
|
-
The end-user MCP via DCR + role-gated content (community / fan-club) requires no architectural change — just:
|
|
269
|
-
|
|
270
|
-
- Enable Better Auth `socialProviders.google` / `.apple` (config-only)
|
|
271
|
-
- Enable `magicLink` and `emailOTP` plugins (config + `EmailSender` wiring already in place)
|
|
272
|
-
- Promote DRAFT manifest grammar from POC ADR-0005 — `Schema.spec.policies.readable: ctx.user` and `requires.auth.all: [{ ctx.user.subscription: [premium] }]`
|
|
273
|
-
- Add `additionalFields: { subscriptionTier: ... }` on user when Stripe entitlement lands
|
|
274
|
-
|
|
275
|
-
No config flag flips, no surface migration. The dispatcher partition rule (predicate → surface) handles new tool emission automatically.
|
|
276
|
-
|
|
277
261
|
### Platform agnosticism
|
|
278
262
|
|
|
279
|
-
By removing `@cloudflare/workers-oauth-provider` and routing auth through Better Auth, the SDK no longer depends on any CF-specific auth service. A future
|
|
263
|
+
By removing `@cloudflare/workers-oauth-provider` and routing auth through Better Auth, the SDK no longer depends on any CF-specific auth service. A future adapter can construct Better Auth against its database while the runtime + dispatcher + skills + prompts stay unchanged. ADR-0011 (adapter port spec) is amended: the `OAuthVerifier` port disappears; auth becomes adapter-owned, with platform-agnostic Better Auth as the type.
|
|
280
264
|
|
|
281
265
|
## Alternatives considered
|
|
282
266
|
|
|
@@ -346,13 +330,6 @@ Phase 2 (v0.1.x):
|
|
|
346
330
|
- Magic-link + email-OTP plugins enabled (need `ResendEmailSender` wired)
|
|
347
331
|
- Account-linking with reauth UI in publication starter
|
|
348
332
|
|
|
349
|
-
Phase 3 (v0.2+, with community / fan-club):
|
|
350
|
-
|
|
351
|
-
- POC ADR-0005 DRAFT grammar promotion: `Schema.spec.policies.readable`, `requires.auth.all: ctx.user.subscription[*]`
|
|
352
|
-
- Subscription tier on user (`additionalFields`)
|
|
353
|
-
- Stripe webhook → entitlement updater
|
|
354
|
-
- Community / fan-club starter manifests
|
|
355
|
-
|
|
356
333
|
## How to apply
|
|
357
334
|
|
|
358
335
|
When reviewing or implementing a change that touches auth, MCP routing, or roles:
|
|
@@ -544,3 +521,107 @@ Dynamic membership, billing, and entitlement state remains consumer-owned.
|
|
|
544
521
|
does not introduce a Policy atom or an entitlement service. See
|
|
545
522
|
[`API and MCP authorization`](../api-mcp-authorization.md) for the public API
|
|
546
523
|
and end-to-end examples.
|
|
524
|
+
|
|
525
|
+
## Amendment — 2026-08-22: Better Auth 1.7 MCP and CIMD convergence
|
|
526
|
+
|
|
527
|
+
Issue #734 revisits the 2026-05-15 compatibility carve-out after Better Auth
|
|
528
|
+
1.7 shipped a dedicated `@better-auth/mcp` package, resource-bound JWT grants,
|
|
529
|
+
and the MCP 2026-07-28 Client ID Metadata Document profile. These are the
|
|
530
|
+
missing capabilities that originally forced Mantle to keep a second OAuth
|
|
531
|
+
authority.
|
|
532
|
+
|
|
533
|
+
The Cloudflare adapter now uses one Better Auth instance for staff identity,
|
|
534
|
+
OAuth authorization, consent, client registration/discovery, token issuance,
|
|
535
|
+
and MCP resource verification. `@cloudflare/workers-oauth-provider` and Core's
|
|
536
|
+
`OAUTH_KV` requirement are removed. This amendment supersedes only the
|
|
537
|
+
2026-05-15 transport carve-out; adapter ownership, the curated `Auth` facade,
|
|
538
|
+
fresh D1 staff-role checks, the single `mcp` compatibility scope, and normalized
|
|
539
|
+
`HandlerContext.auth` remain unchanged.
|
|
540
|
+
|
|
541
|
+
### Provider and resource boundary
|
|
542
|
+
|
|
543
|
+
`createAuth` keeps its general OAuth-provider capability and adds MCP as an
|
|
544
|
+
explicit curated mode, implemented by composing `jwt()` and
|
|
545
|
+
`@better-auth/mcp`. It is not replaced by an MCP-only factory and no Better
|
|
546
|
+
Auth passthrough is exposed. Standard Workers bind one canonical protected
|
|
547
|
+
resource, `${PUBLIC_ORIGIN}/mcp`. Both `/mcp` and `/mcp/staff` accept tokens for
|
|
548
|
+
that resource; `/mcp/staff` remains a stricter server-side role projection, not
|
|
549
|
+
a second OAuth audience.
|
|
550
|
+
|
|
551
|
+
The adapter continues to verify and normalize credentials before calling the
|
|
552
|
+
portable runtime. Better Auth imports remain confined to the default
|
|
553
|
+
implementation. A host app may still implement the `Auth` facade or mount its
|
|
554
|
+
own low-level routes; the generated convenience path does not make Better Auth
|
|
555
|
+
a runtime dependency.
|
|
556
|
+
|
|
557
|
+
MCP request verification reuses Better Auth's DPoP binding primitive and its
|
|
558
|
+
database-backed replay store. Bearer JWTs remain valid; a DPoP-bound JWT is
|
|
559
|
+
accepted only with a matching request proof, method, URL, token hash, and
|
|
560
|
+
single-use proof id. Raw-token callers cannot bypass that request boundary.
|
|
561
|
+
|
|
562
|
+
### CIMD is primary; DCR is bounded compatibility
|
|
563
|
+
|
|
564
|
+
The MCP mode composes:
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
mcp({ resource, loginPage, consentPage, scopes: ["mcp"], ... })
|
|
568
|
+
cimd({
|
|
569
|
+
fetchClientMetadataResource,
|
|
570
|
+
metadataProfile: "mcp-2026-07-28",
|
|
571
|
+
})
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Cloudflare's native `fetch` is the metadata transport only when the Worker has
|
|
575
|
+
`global_fetch_strictly_public` enabled. The runtime flag makes the subrequest
|
|
576
|
+
use Cloudflare's public-Internet routing boundary; Better Auth owns URL
|
|
577
|
+
validation, timeout, response limits, redirect refusal, revalidation, and the
|
|
578
|
+
bounded fetch governor. Mantle does not add a DNS resolver, socket HTTP client,
|
|
579
|
+
or generic transport abstraction.
|
|
580
|
+
|
|
581
|
+
Unauthenticated DCR remains enabled only for older MCP clients. Its lifetime
|
|
582
|
+
stays at the removed provider's 90-day default. Better Auth expires confidential
|
|
583
|
+
registration secrets; Mantle additionally prunes only expired ownerless DCR
|
|
584
|
+
rows (`clientDiscoveryId`, user owner, and reference owner all absent) on
|
|
585
|
+
OAuth traffic. CIMD-owned and operator-managed clients are never cleanup
|
|
586
|
+
candidates. Cleanup is storage hygiene: failures are logged and do not turn a
|
|
587
|
+
valid authorization request into an outage. No cron or second registry is
|
|
588
|
+
introduced.
|
|
589
|
+
|
|
590
|
+
Better Auth's `oauthClient` row is the connected-client authority. It retains
|
|
591
|
+
the discovery provenance and validated name, URI, redirect URIs, application
|
|
592
|
+
type, and private server metadata. The consent UI reads only the public
|
|
593
|
+
secret-free projection. Remote client metadata is not copied into portable
|
|
594
|
+
`HandlerContext` or deferred event envelopes.
|
|
595
|
+
|
|
596
|
+
### Breaking migration
|
|
597
|
+
|
|
598
|
+
All Better Auth packages upgrade together to 1.7. The D1 schema adopts account
|
|
599
|
+
issuer identity, resource/client relationships, resource-bound token and
|
|
600
|
+
consent fields, discovery provenance, and replay storage required by the
|
|
601
|
+
installed plugins. Removed `validAudiences` configuration becomes the one
|
|
602
|
+
explicit MCP resource; generic upstream OAuth adopts the 1.7 social sign-in and
|
|
603
|
+
callback contract.
|
|
604
|
+
|
|
605
|
+
This is an alpha breaking migration. KV registrations, grants, and opaque
|
|
606
|
+
tokens are not migrated into D1 because their issuer/resource provenance cannot
|
|
607
|
+
be established safely; MCP clients reconnect through CIMD or DCR. Existing
|
|
608
|
+
pre-1.7 alpha auth databases are reset and re-bootstrapped rather than receiving
|
|
609
|
+
a guessed account issuer backfill.
|
|
610
|
+
|
|
611
|
+
The authorization endpoints consequently move from `/oauth/*` to Better
|
|
612
|
+
Auth's `/api/auth/oauth2/*` discovery-advertised endpoints. No compatibility
|
|
613
|
+
aliases are retained: existing KV client identifiers are invalid after the
|
|
614
|
+
authority change regardless, and standards-compliant clients rediscover the
|
|
615
|
+
new endpoints.
|
|
616
|
+
|
|
617
|
+
### Downstream ownership
|
|
618
|
+
|
|
619
|
+
Starters remove their obsolete OAuth package and `OAUTH_KV` binding while
|
|
620
|
+
retaining `global_fetch_strictly_public`. Landing opts its custom `createAuth`
|
|
621
|
+
construction into the same MCP mode. Landing may keep an `OAUTH_KV` binding for
|
|
622
|
+
its own launch/bootstrap state; that storage is unrelated to the removed Core
|
|
623
|
+
OAuth store and is not renamed by this decision.
|
|
624
|
+
|
|
625
|
+
This amendment adopts the 2026-07-28 CIMD authorization profile only. Updating
|
|
626
|
+
Mantle's JSON-RPC dispatcher to the complete MCP 2026-07-28 transport revision
|
|
627
|
+
is a separate decision.
|
|
@@ -19,9 +19,30 @@ Core (SDK producer)
|
|
|
19
19
|
-> published npm contract
|
|
20
20
|
mantle-starters (external consumer and bundle producer)
|
|
21
21
|
-> immutable provision bundle
|
|
22
|
-
mantle-landing (provisioner)
|
|
22
|
+
mantle-landing (provisioner) Core CLI (`mantle create`)
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
> **Amended 2026-08-19 (#699).** The chain above is no longer linear. Core's
|
|
26
|
+
> umbrella CLI is now also a consumer of the immutable bundle: `mantle create`
|
|
27
|
+
> resolves the official `v${packageVersion}` starter tag, renders it through
|
|
28
|
+
> the environment-neutral module Core owns, and writes a local project. Core
|
|
29
|
+
> is therefore both the upstream producer of the npm contract and a downstream
|
|
30
|
+
> consumer of the release train it starts.
|
|
31
|
+
>
|
|
32
|
+
> This does not move starter content into Core, so the decision below stands:
|
|
33
|
+
> starters still author the bundles and still validate the published SDK as an
|
|
34
|
+
> external consumer. What changed is the supporting argument. Two consequences
|
|
35
|
+
> are worth stating rather than rediscovering:
|
|
36
|
+
>
|
|
37
|
+
> - **The published-consumer guarantee is now proven twice.** `mantle create`
|
|
38
|
+
> materializes a project that installs the published package, so a break in
|
|
39
|
+
> the npm contract fails in Core's own release smoke as well as in starter CI.
|
|
40
|
+
> - **Reconsideration input 1 is being answered.** #699 replaces the release
|
|
41
|
+
> order so a candidate is published under a temporary dist-tag, validated
|
|
42
|
+
> against the exact packed artifacts, and only then promoted. That was listed
|
|
43
|
+
> below as an unresolved prerequisite for any future merge; when it lands,
|
|
44
|
+
> this ADR should record it as resolved rather than pending.
|
|
45
|
+
|
|
25
46
|
The repositories were originally split because premium starters needed a
|
|
26
47
|
private ACL. That reason does not determine where public starters must live,
|
|
27
48
|
and the private premium repository remains a stub. A later decision, #191,
|