@aotter/mantle 0.1.0-alpha.1 → 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +105 -26
  2. package/dist/admin.d.ts +2 -0
  3. package/dist/admin.d.ts.map +1 -0
  4. package/dist/admin.js +2 -0
  5. package/dist/admin.js.map +1 -0
  6. package/dist/bun.d.ts +2 -0
  7. package/dist/bun.d.ts.map +1 -0
  8. package/dist/bun.js +2 -0
  9. package/dist/bun.js.map +1 -0
  10. package/dist/cli/create.d.ts +2 -0
  11. package/dist/cli/create.d.ts.map +1 -0
  12. package/dist/cli/create.js +243 -0
  13. package/dist/cli/create.js.map +1 -0
  14. package/dist/cli/generate.d.ts.map +1 -0
  15. package/dist/cli/generate.js +101 -0
  16. package/dist/cli/generate.js.map +1 -0
  17. package/dist/cli/harness.d.ts +3 -0
  18. package/dist/cli/harness.d.ts.map +1 -0
  19. package/dist/{harness-cli.js → cli/harness.js} +13 -8
  20. package/dist/cli/harness.js.map +1 -0
  21. package/dist/cli/main.d.ts +3 -0
  22. package/dist/cli/main.d.ts.map +1 -0
  23. package/dist/{cli.js → cli/main.js} +6 -8
  24. package/dist/cli/main.js.map +1 -0
  25. package/dist/cli/skills.d.ts +3 -0
  26. package/dist/cli/skills.d.ts.map +1 -0
  27. package/dist/{skills.js → cli/skills.js} +51 -6
  28. package/dist/cli/skills.js.map +1 -0
  29. package/dist/cli/update.d.ts.map +1 -0
  30. package/dist/{update.js → cli/update.js} +72 -46
  31. package/dist/cli/update.js.map +1 -0
  32. package/dist/codegen/emitMantleModule.d.ts +16 -0
  33. package/dist/codegen/emitMantleModule.d.ts.map +1 -0
  34. package/dist/codegen/emitMantleModule.js +217 -0
  35. package/dist/codegen/emitMantleModule.js.map +1 -0
  36. package/dist/codegen.d.ts +2 -0
  37. package/dist/codegen.d.ts.map +1 -0
  38. package/dist/codegen.js +2 -0
  39. package/dist/codegen.js.map +1 -0
  40. package/dist/provision/renderProvisionBundle.d.ts +70 -0
  41. package/dist/provision/renderProvisionBundle.d.ts.map +1 -0
  42. package/dist/provision/renderProvisionBundle.js +367 -0
  43. package/dist/provision/renderProvisionBundle.js.map +1 -0
  44. package/dist/provision.d.ts +2 -0
  45. package/dist/provision.d.ts.map +1 -0
  46. package/dist/provision.js +2 -0
  47. package/dist/provision.js.map +1 -0
  48. package/dist/vercel-libsql.d.ts +2 -0
  49. package/dist/vercel-libsql.d.ts.map +1 -0
  50. package/dist/vercel-libsql.js +2 -0
  51. package/dist/vercel-libsql.js.map +1 -0
  52. package/dist/vercel.d.ts +2 -0
  53. package/dist/vercel.d.ts.map +1 -0
  54. package/dist/vercel.js +2 -0
  55. package/dist/vercel.js.map +1 -0
  56. package/dist/web.d.ts +2 -0
  57. package/dist/web.d.ts.map +1 -0
  58. package/dist/web.js +2 -0
  59. package/dist/web.js.map +1 -0
  60. package/docs/adapter-guide.md +94 -47
  61. package/docs/adr/0001-four-atom-manifest-model.md +48 -307
  62. package/docs/adr/0002-closed-enums-for-bindings.md +6 -15
  63. package/docs/adr/0007-ai-as-primary-author.md +6 -4
  64. package/docs/adr/0008-structured-diagnostic-shape.md +2 -2
  65. package/docs/adr/0009-consumer-supplied-manifests.md +15 -9
  66. package/docs/adr/0010-locale-and-translates.md +13 -8
  67. package/docs/adr/0011-adapter-port-spec.md +29 -21
  68. package/docs/adr/0012-views-as-public-rest.md +5 -9
  69. package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +124 -43
  70. package/docs/adr/0018-core-starters-repository-boundary.md +22 -1
  71. package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +204 -0
  72. package/docs/adr/README.md +16 -14
  73. package/docs/api-mcp-authorization.md +28 -42
  74. package/docs/assets/mantle-admin-operations.png +0 -0
  75. package/docs/assets/mantle-hero.jpg +0 -0
  76. package/docs/auth-hosting-model.md +15 -11
  77. package/docs/cloudflare-low-level-composition.md +30 -18
  78. package/docs/deferred-lifecycle-queues.md +20 -38
  79. package/docs/design-atoms.md +155 -401
  80. package/docs/labels.md +5 -3
  81. package/docs/media-uploads.md +9 -2
  82. package/docs/migration-0.1.2.md +45 -0
  83. package/docs/performance-harness.md +7 -6
  84. package/docs/release-process.md +86 -25
  85. package/docs/schema-indexes.md +1 -1
  86. package/docs/sealed-pipeline-ownership.md +100 -0
  87. package/package.json +84 -14
  88. package/skills/README.md +39 -7
  89. package/skills/develop/SKILL.md +31 -17
  90. package/skills/install/SKILL.md +26 -25
  91. package/skills/media-gc/SKILL.md +85 -0
  92. package/skills/plugin/SKILL.md +2 -1
  93. package/skills/provision/SKILL.md +24 -2
  94. package/skills/theme/SKILL.md +11 -1
  95. package/skills/update/SKILL.md +1 -0
  96. package/dist/cli.d.ts +0 -3
  97. package/dist/cli.d.ts.map +0 -1
  98. package/dist/cli.js.map +0 -1
  99. package/dist/generate.d.ts.map +0 -1
  100. package/dist/generate.js +0 -181
  101. package/dist/generate.js.map +0 -1
  102. package/dist/harness-cli.d.ts +0 -3
  103. package/dist/harness-cli.d.ts.map +0 -1
  104. package/dist/harness-cli.js.map +0 -1
  105. package/dist/skills.d.ts +0 -2
  106. package/dist/skills.d.ts.map +0 -1
  107. package/dist/skills.js.map +0 -1
  108. package/dist/update.d.ts.map +0 -1
  109. package/dist/update.js.map +0 -1
  110. /package/dist/{generate.d.ts → cli/generate.d.ts} +0 -0
  111. /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; §"Future grammar discipline" covers the v0.1-vs-DRAFT window this lands in).
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 DRAFT keys; it does **not**
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
- unique: [slug]
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
- unique: [[slug, locale]]
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:** Accepted for v0.1.0. Amended 2026-08-11 to remove the rendered-artifact `KvCache` port.
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-11).
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 + MCP bearer validation. OAuth grant KV remains adapter-owned infrastructure and is not a runtime port.
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
- **Two required adapter ports**, defined as TypeScript interfaces in `@aotter/mantle-runtime/src/domain/port/`. Concrete adapters provide implementations and inject them into `createCmsRuntime`.
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`, `approvals`, plus migrations. |
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-via-Hyperdrive adapter wraps `pg` to the same shape; a Netlify adapter could wrap Neon, Supabase, or PlanetScale.
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)`. Future: filesystem read, S3+CDN, Netlify static-publish dir.
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 three required adapter ports (`db`, `kv`, `assets`)
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 ALL adapters (CF + Netlify stub) in the same change. Optional feature ports must be documented here and must state when adapters are required to implement them.
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 Bun/Deno/Vercel/Netlify port 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.
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. The current 5-package structure (spec / runtime / admin-ui / cloudflare / netlify) is already at the boundary of "too many"; splitting further increases the maintenance tax without useful benefit. Ports are TS interfaces in `mantle-runtime`'s `src/domain/port/` directory — that's enough.
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 (`@aotter/mantle-cloudflare` for v0.1.0; future `mantle-netlify`, `mantle-bun`, …):
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 — Hono on CF, Netlify Functions handler, raw `fetch` Worker, …
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's also reflected in this ADR + the Netlify stub README.
257
- 3. If a port shape changed, all 2 adapters (CF real, Netlify stub) get updated in the same PR.
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
- ## Out of scope (deferred)
131
+ ## Current limits
134
132
 
135
- - **`Trigger.target.view`** (lifecycle/projection triggers fired by Views). Tracked separately as a v0.2 grammar move.
136
- - **`spec.output.kind`** (declaring scalar / tree / tabular result shape per View). Lands with join + group-by support in v0.1.x.
137
- - **Optional param-ref drop semantics in the parser.** Runtime is already implemented; parser promotes when v0.1.x lands.
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
- - A View per filter combination (until DRAFT operators land). `posts-by-locale` plus `posts-by-tag` plus `posts-by-locale-and-tag` would be three Views in v0.1.0.
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, and
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-03)
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-05-15 carve-out and 2026-07-15 unified
14
- > authorization amendment are authoritative where they conflict. Operational
15
- > guidance in "How to apply" is maintained against the current adapter/runtime.
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 becomes platform-agnostic — porting to Netlify / Bun / Deno is config-only.
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 code changes. Future Netlify / partner adapters get the auth surface for free.
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
- What we lose: `grantedBy` / `grantedAt` audit trail. v0.1.0 doesn't need this; v0.1.x can re-add via `additionalFields` on user, or via a separate append-only `staff_audit_log` table.
85
+ ### 3. Two explicit MCP surfaces
85
86
 
86
- ### 3. Two MCP routes, surface-derived from manifest predicate
87
-
88
- `/mcp` and `/staff/mcp` are mounted side-by-side from boot. v0.1.0 ships the conservative partition: `/staff/mcp` exposes all staff authoring/lifecycle tools and requires `mcp:staff` plus an admin role; `/mcp` exposes only read-only `query_view_<name>` tools and requires `mcp:read`. The v0.2+ extension point is **automatic** surface partition derived from each Procedure's `requires.auth.all` predicate:
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>` (auto-emitted from each parsed View, mirroring the existing `/api/views/<name>` REST shape) — public; `/mcp` only
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 (`mantle-cloudflare`, future `mantle-netlify`) construct the Better Auth instance with the right database adapter for their platform and keep it in the adapter-owned HTTP/MCP mount layer.
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
- When `mantle-netlify` lands, the Better Auth wiring moves to its own package. Today the seam is in place:
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
- @aotter/mantle-netlify ← Netlify adapter; same shape (v0.2)
200
+ future adapter ← same contract, implemented when needed
205
201
  ```
206
202
 
207
- The pivot point — when to extract — is when the second adapter (`mantle-netlify`) needs the same wiring. Until then, in-place co-location is cheaper than a new package boundary.
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 Netlify adapter constructs a Better Auth instance backed by a Netlify-compatible D1 / postgres / sqlite database; the rest of the runtime + dispatcher + skills + prompts work unchanged. ADR-0011 (adapter port spec) is amended: the `OAuthVerifier` port disappears; auth becomes a direct constructor argument with platform-agnostic Better Auth as the type.
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,