@aotter/mantle 0.0.11-alpha.73 → 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 +107 -28
  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 +95 -51
  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 +39 -43
  68. package/docs/adr/0012-views-as-public-rest.md +6 -10
  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 +78 -44
  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 -41
  79. package/docs/design-atoms.md +159 -400
  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 +23 -23
  84. package/docs/release-process.md +91 -28
  85. package/docs/schema-indexes.md +1 -1
  86. package/docs/sealed-pipeline-ownership.md +100 -0
  87. package/package.json +85 -15
  88. package/skills/README.md +39 -7
  89. package/skills/develop/SKILL.md +33 -19
  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. New ADR. Replaces the architectural concerns previously tracked in POC ADR-0015 (`cms-astro` internal seam discipline).
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 to absorb ADR-0014; revised 2026-05-10 to remove obsolete pre-ADR-0014 port text).
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, KV, ASSETS, and supplies a Better Auth instance (per ADR-0014) for sign-in + MCP bearer validation.
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,14 +22,25 @@ The POC accumulated multiple half-decisions about this seam (POC ADR-0015 docume
21
22
 
22
23
  ## Decision
23
24
 
24
- **Three 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. |
29
- | `KvCache` | Publish-pipeline cache — pre-rendered HTML, `.md` mirrors, `llms.txt` per locale. Read-mostly, written by the publish pipeline. |
36
+ | `DatabaseDriver` | All persistent state — `entries`, `site_config`, `staff`, `users`, plus migrations. |
30
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. |
31
38
 
39
+ Rendered public artifacts are not a second storage model. D1 stays canonical;
40
+ adapters render on origin misses and may use their native HTTP response cache.
41
+ The Cloudflare adapter uses version-local Workers Cache, which runs before the
42
+ Worker for eligible anonymous responses.
43
+
32
44
  Optional feature ports may also live in `domain/port/`, but they are
33
45
  not part of the first-run adapter contract until a feature is enabled.
34
46
  For v0.1.x media hosting and deferred lifecycle dispatch:
@@ -119,21 +131,7 @@ export interface DatabaseDriver {
119
131
 
120
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.
121
133
 
122
- 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.
123
-
124
- ### `KvCache`
125
-
126
- ```ts
127
- export interface KvCache {
128
- get(key: string): Promise<string | null>;
129
- put(key: string, value: string, opts?: { expirationTtl?: number }): Promise<void>;
130
- delete(key: string): Promise<void>;
131
- /** List keys with a prefix — used by sitemap / llms.txt aggregation. */
132
- list(prefix: string, cursor?: string | null): Promise<{ keys: string[]; cursor: string | null }>;
133
- }
134
- ```
135
-
136
- CF adapter: Workers KV. Future: Redis, FS, S3-compatible store.
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.
137
135
 
138
136
  ### `AssetServer`
139
137
 
@@ -146,12 +144,16 @@ export interface AssetServer {
146
144
  }
147
145
  ```
148
146
 
149
- 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.
150
148
 
151
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."
152
150
 
153
151
  ## How adapters wire ports
154
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
+
155
157
  ```ts
156
158
  // simplified Cloudflare adapter wiring (post-ADR-0014, amended
157
159
  // 2026-05-15 by PR #193's OAuth carve-out).
@@ -164,7 +166,6 @@ import {
164
166
  mountServerEndpoints,
165
167
  AssetsAssetServer,
166
168
  D1DatabaseDriver,
167
- KvCacheBinding,
168
169
  } from "@aotter/mantle-cloudflare";
169
170
 
170
171
  const auth = createAuth({
@@ -188,7 +189,6 @@ const cms = createCmsRef({
188
189
  handlers,
189
190
  bindings: {
190
191
  db: new D1DatabaseDriver(env.DB),
191
- kv: new KvCacheBinding(env.KV),
192
192
  assets: new AssetsAssetServer(env.ASSETS),
193
193
  },
194
194
  auth,
@@ -210,7 +210,7 @@ export default createOAuthProvider({
210
210
  });
211
211
  ```
212
212
 
213
- The runtime gets three required adapter ports (`db`, `kv`, `assets`)
213
+ The runtime gets two required adapter ports (`db`, `assets`)
214
214
  alongside manifests, handlers, templates, and site defaults. Auth is
215
215
  owned by the adapter layer that mounts HTTP/MCP surfaces; the runtime
216
216
  receives authenticated context when the adapter dispatches requests.
@@ -220,28 +220,25 @@ There's no module-global state holding adapter-specific bindings.
220
220
 
221
221
  **Hard-enforced boundaries**:
222
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.
223
- - 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.
224
224
  - Removing a port is also possible (if a port is found to overlap or be unnecessary), again by amending this ADR.
225
225
 
226
226
  **Discoverability for adapter authors**:
227
- - A future Bun/Deno/Vercel/Netlify port author reads this ADR + [`docs/adapter-guide.md`](../adapter-guide.md), implements the three 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.
228
228
 
229
229
  **Test ergonomics**:
230
- - Each port is small and isolated. Tests can mock individual ports without spinning up D1 / KV / OAuth provider.
230
+ - Each port is small and isolated. Tests can mock individual ports without spinning up D1 or an OAuth provider.
231
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.
232
232
 
233
- **The Netlify stub's job**:
234
- - 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.
235
-
236
233
  ## Alternatives considered
237
234
 
238
- **(a) Single mega-port** — One `RuntimePorts` interface containing every method (db.prepare, kv.get, assets.fetch, media.createUpload, …). **Rejected**: leaks the entire surface onto every adapter. Adapter authors who only want to swap KV would have to touch the mega-port impl. Discrete ports keep change blast radius per port.
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.
239
236
 
240
237
  **(b) Concrete CF types in runtime** — Just `import type { D1Database } from "@cloudflare/workers-types"` directly into `mantle-runtime`. Treat "CF-only" as a v0.1.0 reality, defer the abstraction. **Rejected**: this is what the POC did (via `cms-server` having implicit assumptions about D1 shape) and it's the trap the rebuild exists to escape. Once concrete CF types land in runtime, removing them is a multi-PR uplift later. Cheaper to do it right at v0.1.0.
241
238
 
242
- **(c) Function-injection (no interfaces, just functions)** — Runtime accepts a record of functions: `{ dbPrepare, kvGet, kvPut, sessionRead, … }`. **Rejected**: TypeScript interfaces are more discoverable (an adapter author IDE-jumps from `DatabaseDriver` to its surface; jumping from `dbPrepare` is harder). Interfaces also document grouping; functions don't.
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.
243
240
 
244
- **(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.
245
242
 
246
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.
247
244
 
@@ -251,29 +248,28 @@ When you're authoring `@aotter/mantle-runtime` code:
251
248
 
252
249
  1. If you reach for a CF-specific type, **stop**. Define a method on a port instead.
253
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.
254
- 3. Tests must use port mocks (in-memory implementations) — never reach into a real D1 / KV from runtime tests.
251
+ 3. Tests must use port mocks (in-memory implementations) — never reach into a real D1 from runtime tests.
255
252
 
256
- 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:
257
254
 
258
255
  1. Read `mantle-runtime/src/domain/port/`. Implement each required port against your runtime's primitives.
259
- 2. Compose the runtime via `createCmsRuntime({ db, kv, assets, manifests, handlers, templates, siteDefaults, ... })`.
256
+ 2. Compose the runtime via `createCmsRuntime({ db, assets, manifests, handlers, templates, siteDefaults, ... })`.
260
257
  3. Call `runtime.bootInit()` once before serving CMS traffic.
261
- 4. Bind to your HTTP framework — Hono on CF, Netlify Functions handler, raw `fetch` Worker, …
258
+ 4. Bind to your HTTP framework.
262
259
  5. Provide adapter-owned auth and map sessions/scopes/roles into runtime handler context.
263
260
  6. Bundle `@aotter/mantle-admin-ui`'s `dist/` via your runtime's static-asset surface and bind `AssetServer` to it.
264
261
 
265
262
  When you're reviewing a PR:
266
263
 
267
264
  1. Grep the diff for `@cloudflare`, `D1Database`, `KVNamespace`, `Fetcher` — flag any occurrence in `mantle-runtime/`.
268
- 2. If a new port method shows up, check it's also reflected in this ADR + the Netlify stub README.
269
- 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.
270
267
 
271
268
  ## Implementation status
272
269
 
273
270
  - [x] Required port interface files live in `packages/mantle-runtime/src/domain/port/*.ts`.
274
271
  - [x] Cloudflare required port implementations live in `packages/adapters/cloudflare/src/bindings/*.ts`.
275
272
  - [x] Optional feature port `MediaStorage` (public bucket) is declared but not required by first-run adapters. `PrivateMediaStorage` is v0.2.
276
- - [x] Netlify stub README references this ADR.
277
273
  - [ ] CI lint: forbid `@cloudflare/*` / `D1Database` / `KVNamespace` imports in `mantle-runtime/` (post-v0.1.0; manual review until then)
278
274
 
279
275
  ## See also
@@ -88,7 +88,7 @@ spec:
88
88
  eq: { field: locale, value: { $param: locale } }
89
89
  ```
90
90
 
91
- The `$param` discriminator key was chosen to match the JSON Schema `$ref` convention. Future sentinels (`$now`, `$ctx.user`) follow the same `$<name>` shape; this ADR adds none of them.
91
+ The `$param` discriminator key was chosen to match the JSON Schema `$ref` convention. The later closed `{ "$ctx.user": "id" }` sentinel follows the same `$<name>` shape; `$now` remains unimplemented.
92
92
 
93
93
  Boot validator gates:
94
94
  - `View.spec.params` MUST be `type: object` with `properties` declared (`VIEW_PARAMS_INVALID_SHAPE`).
@@ -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,