@aotter/mantle 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +9 -5
- package/dist/generate.js +19 -2
- package/dist/generate.js.map +1 -1
- package/dist/harness-cli.js +1 -1
- package/dist/harness-cli.js.map +1 -1
- package/docs/adapter-guide.md +1 -1
- package/docs/adr/0001-four-atom-manifest-model.md +37 -304
- package/docs/adr/0002-closed-enums-for-bindings.md +6 -15
- package/docs/adr/0007-ai-as-primary-author.md +3 -1
- package/docs/adr/0008-structured-diagnostic-shape.md +2 -2
- package/docs/adr/0009-consumer-supplied-manifests.md +6 -5
- package/docs/adr/0010-locale-and-translates.md +13 -8
- package/docs/adr/0011-adapter-port-spec.md +13 -19
- package/docs/adr/0012-views-as-public-rest.md +5 -9
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +13 -37
- package/docs/adr/README.md +4 -4
- package/docs/api-mcp-authorization.md +7 -0
- package/docs/design-atoms.md +66 -255
- package/docs/labels.md +4 -2
- package/docs/release-process.md +14 -3
- package/docs/schema-indexes.md +1 -1
- package/package.json +8 -8
- package/skills/develop/SKILL.md +15 -6
- package/skills/plugin/SKILL.md +1 -1
- package/skills/provision/SKILL.md +4 -0
- package/skills/theme/SKILL.md +1 -1
|
@@ -96,26 +96,19 @@ The vocabulary is exactly:
|
|
|
96
96
|
|
|
97
97
|
- `ctx.user` — caller is signed in as an end-user
|
|
98
98
|
- `ctx.staff: [<role>, ...]` — caller is signed in as staff in one of these roles
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`contains:`, quota predicates) is DRAFT — see
|
|
102
|
-
[ADR-0001](0001-four-atom-manifest-model.md) § Future grammar
|
|
103
|
-
discipline.
|
|
99
|
+
- `ctx.auth` — caller supplied an adapter-verified credential
|
|
100
|
+
- `ctx.auth.scope: <scope>` — credential carries the exact scope
|
|
104
101
|
|
|
105
102
|
### Adding a new entry is an explicit grammar-revise round
|
|
106
103
|
|
|
107
|
-
New `x-mantle-bind` values or `ctx.*` predicates do not get added
|
|
108
|
-
ad-hoc. They go through the discipline gate documented in
|
|
109
|
-
[ADR-0001](0001-four-atom-manifest-model.md) § Future grammar
|
|
110
|
-
discipline:
|
|
104
|
+
New `x-mantle-bind` values or `ctx.*` predicates do not get added ad hoc:
|
|
111
105
|
|
|
112
106
|
1. A documented use case showing the existing closed set cannot
|
|
113
107
|
express the requirement.
|
|
114
108
|
2. A design pass on what the new value's runtime semantics are
|
|
115
109
|
(where does it come from? when is it null? what happens at
|
|
116
110
|
the storage layer?).
|
|
117
|
-
3. A spec doc revision
|
|
118
|
-
classification.
|
|
111
|
+
3. A spec doc revision.
|
|
119
112
|
4. Code that updates the validator to accept the new value.
|
|
120
113
|
|
|
121
114
|
This treats the closed set as load-bearing infrastructure, not
|
|
@@ -208,10 +201,8 @@ attributes.
|
|
|
208
201
|
- New manifest using a value not in the enum: parse error,
|
|
209
202
|
exact diagnostic shape with `candidates` populated. AI authors
|
|
210
203
|
fix in one turn.
|
|
211
|
-
- New use case wanting an entry not in the enum:
|
|
212
|
-
|
|
213
|
-
discipline. Document the use case in the spec PR; do not
|
|
214
|
-
fast-track.
|
|
204
|
+
- New use case wanting an entry not in the enum: document the concrete
|
|
205
|
+
semantics and add validation, runtime behavior, and docs together.
|
|
215
206
|
- Lookups (e.g. "stamp the team_id"): handler-side TS, not
|
|
216
207
|
binding metadata. The Procedure handler has the lookup
|
|
217
208
|
context anyway.
|
|
@@ -100,6 +100,8 @@ What it catches:
|
|
|
100
100
|
`View.filter` refer only to declared properties of that Schema
|
|
101
101
|
- `Schema.uniqueIndexes` and `Schema.indexes` contain valid ordered
|
|
102
102
|
tuples of scalar properties declared in `spec.schema.properties`
|
|
103
|
+
- `Schema.searchableFields` contains unique top-level string properties
|
|
104
|
+
- `Schema.uiSchema.list.filterField` names an indexed operational string enum
|
|
103
105
|
- `x-mantle-bind: <value>` is in the closed enum
|
|
104
106
|
- `requires.auth.all` predicates are in the v0.1 vocabulary
|
|
105
107
|
- `Trigger.source.path` does not collide with another Trigger
|
|
@@ -264,7 +266,7 @@ Concrete artifacts today:
|
|
|
264
266
|
admin: write a post, update a draft, request publish, list
|
|
265
267
|
submissions.
|
|
266
268
|
- Future ops-role verbs that don't exist yet but have an obvious
|
|
267
|
-
operator use case ("translate this draft", "
|
|
269
|
+
operator use case ("translate this draft", "publish this entry").
|
|
268
270
|
When added, they land as **new SDK builtin tools** — declared in
|
|
269
271
|
the MCP catalog source under the closed Day-1 set, not
|
|
270
272
|
auto-generated from consumer Procedures.
|
|
@@ -126,7 +126,7 @@ code string. That was retired because:
|
|
|
126
126
|
### `path` format
|
|
127
127
|
|
|
128
128
|
- For static validation: filesystem path + JSON Pointer fragment,
|
|
129
|
-
e.g. `manifests/
|
|
129
|
+
e.g. `manifests/site.yaml#/3/spec/from`.
|
|
130
130
|
- For a consumer test diagnostic: test file path + assertion location when
|
|
131
131
|
available, e.g. `tests/handlers/contact.test.ts:42`.
|
|
132
132
|
- For boot-time: manifest pointer (no on-disk path because boot
|
|
@@ -250,7 +250,7 @@ Diagnostic candidate (alternative (d) below) applies to zod —
|
|
|
250
250
|
and AI authors see the same structure.
|
|
251
251
|
- Single source of truth. The interface, code constants, and
|
|
252
252
|
formatter all live in `@aotter/mantle-spec`; the runtime
|
|
253
|
-
package and adapters import them. A future
|
|
253
|
+
package and adapters import them. A future adapter
|
|
254
254
|
inherits the shape for free.
|
|
255
255
|
|
|
256
256
|
### Costs
|
|
@@ -22,9 +22,10 @@ YAML machinery into the runtime bundle and coupled consumers to Wrangler
|
|
|
22
22
|
|
|
23
23
|
## Decision
|
|
24
24
|
|
|
25
|
-
Consumers own
|
|
26
|
-
validates
|
|
27
|
-
types
|
|
25
|
+
Consumers own exactly `manifests/site.yaml`. The installed `mantle` CLI parses
|
|
26
|
+
and validates its multi-document YAML, then writes the machine-owned runtime
|
|
27
|
+
module and handler types. A missing or differently named file fails before
|
|
28
|
+
generation:
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
pnpm exec mantle generate
|
|
@@ -72,7 +73,7 @@ runtime boot validation covers cross-manifest and registered-handler facts.
|
|
|
72
73
|
|
|
73
74
|
The external [`aotter/mantle-starters`](https://github.com/aotter/mantle-starters)
|
|
74
75
|
repository owns the blank project and typed overlays. Materialized projects
|
|
75
|
-
carry their own `manifests
|
|
76
|
+
carry their own `manifests/site.yaml`, generated module, handlers, and package scripts;
|
|
76
77
|
they consume the exact packed Core artifact rather than a workspace link.
|
|
77
78
|
|
|
78
79
|
## Consequences
|
|
@@ -128,7 +129,7 @@ runtime.
|
|
|
128
129
|
|
|
129
130
|
When authoring or reviewing a generated project:
|
|
130
131
|
|
|
131
|
-
1. Edit
|
|
132
|
+
1. Edit only `manifests/site.yaml`; do not create feature-named manifest files.
|
|
132
133
|
2. Run the installed `pnpm exec mantle generate`.
|
|
133
134
|
3. Import `manifest` from `.mantle/generated/site.js` into the conventional
|
|
134
135
|
Worker, or pass it as `CmsConfig.manifests` in low-level composition.
|
|
@@ -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,8 +1,8 @@
|
|
|
1
1
|
# ADR-0011: Adapter port spec
|
|
2
2
|
|
|
3
|
-
**Status:** Accepted for v0.1.0. Amended 2026-08-
|
|
3
|
+
**Status:** Accepted for v0.1.0. Amended 2026-08-13 to remove the unimplemented adapter stub.
|
|
4
4
|
|
|
5
|
-
**Date:** 2026-05-04 (revised 2026-05-09, 2026-05-10, and 2026-08-
|
|
5
|
+
**Date:** 2026-05-04 (revised 2026-05-09, 2026-05-10, 2026-08-11, and 2026-08-13).
|
|
6
6
|
|
|
7
7
|
## Context
|
|
8
8
|
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
|
|
11
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 + MCP bearer validation. OAuth grant KV remains adapter-owned infrastructure and is not a runtime port.
|
|
12
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
|
-
|
|
15
13
|
This ADR fixes the contract so:
|
|
16
14
|
- Future adapter authors have a stable target.
|
|
17
15
|
- PR reviewers can mechanically check "does this commit add a CF-specific type to runtime?"
|
|
@@ -25,7 +23,7 @@ The POC accumulated multiple half-decisions about this seam (POC ADR-0015 docume
|
|
|
25
23
|
|
|
26
24
|
| Port | Surface |
|
|
27
25
|
|---|---|
|
|
28
|
-
| `DatabaseDriver` | All persistent state — `entries`, `site_config`, `staff`, `users`,
|
|
26
|
+
| `DatabaseDriver` | All persistent state — `entries`, `site_config`, `staff`, `users`, plus migrations. |
|
|
29
27
|
| `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
28
|
|
|
31
29
|
Rendered public artifacts are not a second storage model. D1 stays canonical;
|
|
@@ -123,7 +121,7 @@ export interface DatabaseDriver {
|
|
|
123
121
|
|
|
124
122
|
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
123
|
|
|
126
|
-
The CF adapter's impl is a thin proxy over `env.DB` (D1). A future Postgres
|
|
124
|
+
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
125
|
|
|
128
126
|
### `AssetServer`
|
|
129
127
|
|
|
@@ -136,7 +134,7 @@ export interface AssetServer {
|
|
|
136
134
|
}
|
|
137
135
|
```
|
|
138
136
|
|
|
139
|
-
CF adapter: wraps `env.ASSETS.fetch(req)`.
|
|
137
|
+
CF adapter: wraps `env.ASSETS.fetch(req)`. Other adapters can use a filesystem or object storage.
|
|
140
138
|
|
|
141
139
|
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
140
|
|
|
@@ -198,7 +196,7 @@ export default createOAuthProvider({
|
|
|
198
196
|
});
|
|
199
197
|
```
|
|
200
198
|
|
|
201
|
-
The runtime gets
|
|
199
|
+
The runtime gets two required adapter ports (`db`, `assets`)
|
|
202
200
|
alongside manifests, handlers, templates, and site defaults. Auth is
|
|
203
201
|
owned by the adapter layer that mounts HTTP/MCP surfaces; the runtime
|
|
204
202
|
receives authenticated context when the adapter dispatches requests.
|
|
@@ -208,19 +206,16 @@ There's no module-global state holding adapter-specific bindings.
|
|
|
208
206
|
|
|
209
207
|
**Hard-enforced boundaries**:
|
|
210
208
|
- `@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
|
|
209
|
+
- 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
210
|
- Removing a port is also possible (if a port is found to overlap or be unnecessary), again by amending this ADR.
|
|
213
211
|
|
|
214
212
|
**Discoverability for adapter authors**:
|
|
215
|
-
- A future
|
|
213
|
+
- 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
214
|
|
|
217
215
|
**Test ergonomics**:
|
|
218
216
|
- Each port is small and isolated. Tests can mock individual ports without spinning up D1 or an OAuth provider.
|
|
219
217
|
- 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
218
|
|
|
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
219
|
## Alternatives considered
|
|
225
220
|
|
|
226
221
|
**(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 +224,7 @@ There's no module-global state holding adapter-specific bindings.
|
|
|
229
224
|
|
|
230
225
|
**(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
226
|
|
|
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.
|
|
227
|
+
**(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
228
|
|
|
234
229
|
**(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
230
|
|
|
@@ -241,27 +236,26 @@ When you're authoring `@aotter/mantle-runtime` code:
|
|
|
241
236
|
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
237
|
3. Tests must use port mocks (in-memory implementations) — never reach into a real D1 from runtime tests.
|
|
243
238
|
|
|
244
|
-
When you're authoring an adapter
|
|
239
|
+
When you're authoring an adapter:
|
|
245
240
|
|
|
246
241
|
1. Read `mantle-runtime/src/domain/port/`. Implement each required port against your runtime's primitives.
|
|
247
242
|
2. Compose the runtime via `createCmsRuntime({ db, assets, manifests, handlers, templates, siteDefaults, ... })`.
|
|
248
243
|
3. Call `runtime.bootInit()` once before serving CMS traffic.
|
|
249
|
-
4. Bind to your HTTP framework
|
|
244
|
+
4. Bind to your HTTP framework.
|
|
250
245
|
5. Provide adapter-owned auth and map sessions/scopes/roles into runtime handler context.
|
|
251
246
|
6. Bundle `@aotter/mantle-admin-ui`'s `dist/` via your runtime's static-asset surface and bind `AssetServer` to it.
|
|
252
247
|
|
|
253
248
|
When you're reviewing a PR:
|
|
254
249
|
|
|
255
250
|
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,
|
|
251
|
+
2. If a new port method shows up, check it is also reflected in this ADR.
|
|
252
|
+
3. If a port shape changed, every shipping adapter gets updated in the same PR.
|
|
258
253
|
|
|
259
254
|
## Implementation status
|
|
260
255
|
|
|
261
256
|
- [x] Required port interface files live in `packages/mantle-runtime/src/domain/port/*.ts`.
|
|
262
257
|
- [x] Cloudflare required port implementations live in `packages/adapters/cloudflare/src/bindings/*.ts`.
|
|
263
258
|
- [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
259
|
- [ ] CI lint: forbid `@cloudflare/*` / `D1Database` / `KVNamespace` imports in `mantle-runtime/` (post-v0.1.0; manual review until then)
|
|
266
260
|
|
|
267
261
|
## 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.
|
|
@@ -44,7 +44,7 @@ A 2026 Workers-friendly auth library — [Better Auth](https://better-auth.com)
|
|
|
44
44
|
- **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
45
|
- Account linking with policies (verified-email match + reauth requirement)
|
|
46
46
|
|
|
47
|
-
Better Auth depends on a Kysely / Drizzle / Prisma adapter for the database, not on any Cloudflare-specific service. The auth machinery
|
|
47
|
+
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
48
|
|
|
49
49
|
## Decision (historical baseline; amended below)
|
|
50
50
|
|
|
@@ -64,7 +64,7 @@ Adopt Better Auth as the SDK's full auth surface. It owns:
|
|
|
64
64
|
|
|
65
65
|
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
66
|
|
|
67
|
-
This makes the runtime more platform-agnostic, not less: Better Auth runs on Workers (D1 via Kysely), Bun (sqlite), Node (postgres) without
|
|
67
|
+
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
68
|
|
|
69
69
|
### 2. `staff` table → `user.role` via Better Auth admin plugin
|
|
70
70
|
|
|
@@ -81,23 +81,18 @@ admin({
|
|
|
81
81
|
|
|
82
82
|
The manifest grammar predicate `requires.auth.all: [{ "ctx.staff": ["editor"] }]` evaluates against `session.user.role` at runtime. Closed enum membership unchanged.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
### 3. Two explicit MCP surfaces
|
|
85
85
|
|
|
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
|
-
```
|
|
86
|
+
`/mcp` and `/staff/mcp` are mounted side-by-side from boot. `/staff/mcp`
|
|
87
|
+
exposes staff authoring/lifecycle tools and `/mcp` exposes declared public
|
|
88
|
+
Views and Procedures. An MCP Trigger explicitly chooses `surface: public |
|
|
89
|
+
staff`; the target's `requires.auth` still gates every call.
|
|
94
90
|
|
|
95
91
|
Tool partition rules:
|
|
96
92
|
|
|
97
93
|
- 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
94
|
- `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`
|
|
95
|
+
- `query_view_<name>` follows `View.spec.surface`.
|
|
101
96
|
|
|
102
97
|
### 4. Scope-aware DCR via Better Auth `oauthProvider`
|
|
103
98
|
|
|
@@ -140,7 +135,7 @@ The token can carry `role` via `customAccessTokenClaims` for caller convenience,
|
|
|
140
135
|
|
|
141
136
|
### 6. Single auth surface, no port indirection
|
|
142
137
|
|
|
143
|
-
Auth is no longer an adapter port. `mantle-runtime` does NOT define an auth port and `createCmsRuntime()` does not accept auth. Adapter packages
|
|
138
|
+
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
139
|
|
|
145
140
|
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
141
|
|
|
@@ -188,7 +183,7 @@ This makes the implicit explicit. The SDK's auth surface is committee-curated; u
|
|
|
188
183
|
|
|
189
184
|
### 8. Path to `@aotter/mantle-better-auth` separate package (deferred)
|
|
190
185
|
|
|
191
|
-
|
|
186
|
+
Each adapter owns its Better Auth wiring. Today the seam is:
|
|
192
187
|
|
|
193
188
|
- `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
189
|
- `createAuth.ts` is the only file with `import { betterAuth }` (~290 LOC, no Cloudflare-binding-specific code outside `config.database: D1Database`).
|
|
@@ -201,10 +196,10 @@ The future split looks like:
|
|
|
201
196
|
@aotter/mantle-runtime ← ports + use cases (today)
|
|
202
197
|
@aotter/mantle-better-auth ← createAuth + EmailSender impls + appleClientSecret (new, when needed)
|
|
203
198
|
@aotter/mantle-cloudflare ← Workers adapter; depends on (or accepts) Auth-shape (today)
|
|
204
|
-
|
|
199
|
+
future adapter ← same contract, implemented when needed
|
|
205
200
|
```
|
|
206
201
|
|
|
207
|
-
The pivot point — when to extract — is when
|
|
202
|
+
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
203
|
|
|
209
204
|
## Consequences
|
|
210
205
|
|
|
@@ -241,7 +236,6 @@ The pivot point — when to extract — is when the second adapter (`mantle-netl
|
|
|
241
236
|
- `databaseHooks.user.create.after` for `ensureBootstrapOwner` semantics
|
|
242
237
|
- Two `/.well-known/oauth-protected-resource/*` metadata endpoints (Better Auth helpers)
|
|
243
238
|
- 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
239
|
- Skills + docs updates for the dual MCP URL handoff
|
|
246
240
|
|
|
247
241
|
### Backward compatibility
|
|
@@ -263,20 +257,9 @@ User MCP URL: https://<worker>.workers.dev/mcp (give to visitors / t
|
|
|
263
257
|
The publication starter repo's production smoke recipe uses `/mcp/staff`
|
|
264
258
|
for the MCP operator smoke step.
|
|
265
259
|
|
|
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
260
|
### Platform agnosticism
|
|
278
261
|
|
|
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
|
|
262
|
+
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
263
|
|
|
281
264
|
## Alternatives considered
|
|
282
265
|
|
|
@@ -346,13 +329,6 @@ Phase 2 (v0.1.x):
|
|
|
346
329
|
- Magic-link + email-OTP plugins enabled (need `ResendEmailSender` wired)
|
|
347
330
|
- Account-linking with reauth UI in publication starter
|
|
348
331
|
|
|
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
332
|
## How to apply
|
|
357
333
|
|
|
358
334
|
When reviewing or implementing a change that touches auth, MCP routing, or roles:
|
package/docs/adr/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Records of *why* mantle ended up shaped this way. The numbering preserves POC AD
|
|
|
12
12
|
| [0008](0008-structured-diagnostic-shape.md) | Diagnostic shape for validate/boot/runtime failures, with a reserved consumer-test phase; measured harnesses keep purpose-shaped reports. | Accepted + amended |
|
|
13
13
|
| [0009](0009-consumer-supplied-manifests.md) | Consumers own manifest YAML; the installed CLI emits the parser-free runtime module and handler types. Core ships no application manifests. | Accepted + amended |
|
|
14
14
|
| [0010](0010-locale-and-translates.md) | Locale 3-layer (manifest / D1 site_config / data field) + translates pattern. Boot decoupled from `site_config` (issue #60 fix). | Accepted (refreshed) |
|
|
15
|
-
| [0011](0011-adapter-port-spec.md) | Adapter port spec. Required runtime ports plus optional feature ports.
|
|
15
|
+
| [0011](0011-adapter-port-spec.md) | Adapter port spec. Required runtime ports plus optional feature ports. | Accepted (new) |
|
|
16
16
|
| [0012](0012-views-as-public-rest.md) | Views auto-expose matching REST and `query_view_*` MCP reads on their declared `public` or `staff` surface. Schemas never get a public REST endpoint. | Accepted + amended |
|
|
17
17
|
| [0013](0013-agent-provisioned-consumer-projects.md) | Historical agent-provisioned consumer projects path. Superseded for first launch by landing provision bundles. | Superseded |
|
|
18
18
|
| [0014](0014-auth-better-auth-and-multi-tenant-mcp.md) | The Cloudflare adapter owns the curated Better Auth identity/session facade and top-level `@cloudflare/workers-oauth-provider` MCP transport. Both normalize verified callers into runtime context; mutable staff role and target authorization are re-evaluated per call. | Accepted + amended |
|
|
@@ -39,12 +39,12 @@ for new v0.1.0 boundaries, and folds / drops the rest:
|
|
|
39
39
|
|
|
40
40
|
- **POC ADR-0003** OpenAPI emission → folded into `mantle-spec` README (the *what* is implementation; the *why* was already captured by ADR-0001's grammar lock).
|
|
41
41
|
- **POC ADR-0004** D1 today, Hyperdrive PG tomorrow → folded into `mantle-cloudflare` README (now a v0.2 roadmap item, not an architectural decision).
|
|
42
|
-
- **POC ADR-0005** v0.1 minimum
|
|
42
|
+
- **POC ADR-0005** v0.1 minimum grammar → folded into ADR-0001's fail-closed grammar policy.
|
|
43
43
|
- **POC ADR-0006** multi-doc YAML → folded into ADR-0001 §"Authoring shape: multi-doc YAML."
|
|
44
|
-
- **POC ADR-0011** lifecycle binary opt-in → distilled to a §"Lifecycle" subsection in `docs/design-atoms.md`. v0.1.0 ships `
|
|
44
|
+
- **POC ADR-0011** lifecycle binary opt-in → distilled to a §"Lifecycle" subsection in `docs/design-atoms.md`. v0.1.0 ships `publishing` and `operational`.
|
|
45
45
|
- **POC ADR-0012** strategic posture vs adjacent CMS designs → strategic / marketing material, lives in `README.md` if anywhere.
|
|
46
46
|
- **POC ADR-0013** role-split surfaces (coder agent vs operator agent) → folded into ADR-0007 (Part B).
|
|
47
|
-
- **POC ADR-0014** builtin handlers and lifecycle Triggers → promoted to v0.1.0 and implemented in the rebuild via `LifecycleHookingEntryRepository` and `InvokeBuiltinUseCase`.
|
|
47
|
+
- **POC ADR-0014** builtin handlers and lifecycle Triggers → promoted to v0.1.0 and implemented in the rebuild via `LifecycleHookingEntryRepository` and `InvokeBuiltinUseCase`. Full shape spec lives in `docs/design-atoms.md`.
|
|
48
48
|
- **POC ADR-0015** cms-astro internal seam discipline → POC-specific to a package that no longer exists; replaced by ADR-0011 (adapter port spec).
|
|
49
49
|
- **POC ADR-0029** drop Astro from cms-cloudflare → POC-specific historical record; the rebuild starts post-Astro.
|
|
50
50
|
|
|
@@ -128,6 +128,13 @@ Mantle Platform user id, Hosted Auth upstream subject, email, or provider id.
|
|
|
128
128
|
Hosted Auth may establish the site session, but Platform is not part of the
|
|
129
129
|
View query path.
|
|
130
130
|
|
|
131
|
+
## Site OAuth symmetry
|
|
132
|
+
|
|
133
|
+
A site-issued OAuth access token represents the same caller on public MCP and
|
|
134
|
+
manifest HTTP routes. Both surfaces populate `ctx.user` and `ctx.auth` from the
|
|
135
|
+
same token grant; expiry, revocation, scope, client, and resource audience are
|
|
136
|
+
enforced before the Procedure or View runs.
|
|
137
|
+
|
|
131
138
|
## Cloudflare consumer wiring
|
|
132
139
|
|
|
133
140
|
Pass one site-owned resolver to `createCmsRef`. Return `not-handled` when the
|