@aotter/mantle 0.0.11-alpha.63 → 0.0.11-alpha.64
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 +87 -12
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +52 -0
- package/dist/cli.js.map +1 -0
- package/dist/generate.d.ts +2 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +181 -0
- package/dist/generate.js.map +1 -0
- package/dist/skills.d.ts +2 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +80 -0
- package/dist/skills.js.map +1 -0
- package/dist/update.d.ts +2 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +387 -0
- package/dist/update.js.map +1 -0
- package/docs/adr/0001-four-atom-manifest-model.md +6 -7
- package/docs/adr/0007-ai-as-primary-author.md +100 -138
- package/docs/adr/0008-structured-diagnostic-shape.md +79 -99
- package/docs/adr/0009-consumer-supplied-manifests.md +101 -228
- package/docs/adr/0012-views-as-public-rest.md +43 -15
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +43 -17
- package/docs/adr/0018-core-starters-repository-boundary.md +155 -0
- package/docs/adr/README.md +8 -6
- package/docs/cloudflare-low-level-composition.md +94 -0
- package/docs/design-atoms.md +59 -57
- package/docs/design-references/editorial-blog-2026-05-05.md +7 -7
- package/docs/labels.md +1 -1
- package/docs/media-uploads.md +1 -1
- package/docs/release-process.md +156 -523
- package/package.json +9 -6
- package/skills/README.md +20 -16
- package/skills/develop/SKILL.md +4 -4
- package/skills/install/SKILL.md +16 -2
- package/skills/plugin/SKILL.md +1 -1
- package/skills/provision/SKILL.md +1 -1
- package/skills/theme/SKILL.md +12 -10
- package/skills/update/SKILL.md +31 -19
- package/skills/customize-design/SKILL.md +0 -215
- package/skills/extend/SKILL.md +0 -257
|
@@ -1,270 +1,143 @@
|
|
|
1
1
|
# ADR-0009: Consumer-supplied manifests at SDK boot
|
|
2
2
|
|
|
3
|
-
**Status:** Carried over from POC v0.0.x;
|
|
3
|
+
**Status:** Carried over from POC v0.0.x; amended for the parser-free v0.1
|
|
4
|
+
consumer boundary.
|
|
4
5
|
|
|
5
|
-
**Date
|
|
6
|
+
**Date:** 2026-05-01 (POC); last amended 2026-08-03
|
|
6
7
|
|
|
7
|
-
**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
---
|
|
8
|
+
**Related:** [ADR-0001](0001-four-atom-manifest-model.md),
|
|
9
|
+
[ADR-0007](0007-ai-as-primary-author.md),
|
|
10
|
+
[ADR-0018](0018-core-starters-repository-boundary.md)
|
|
12
11
|
|
|
13
12
|
## Context
|
|
14
13
|
|
|
15
|
-
The
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
manifest content from the consumer rather than baking any in itself.
|
|
19
|
-
|
|
20
|
-
The natural failure mode — and the one this ADR forecloses — is the
|
|
21
|
-
SDK shipping with a fixed manifest set hand-maintained inside the
|
|
22
|
-
package (e.g. a TS string mirror of starter YAML files compiled into
|
|
23
|
-
the SDK bundle). A consumer who wants a `comments` Schema, a
|
|
24
|
-
`like-post` Procedure, or a cron Trigger then has two bad options:
|
|
25
|
-
|
|
26
|
-
1. Edit the embedded set inside the SDK (forks the SDK; loses upgrades), or
|
|
27
|
-
2. Wait for a future SDK release that ships the manifest they want.
|
|
28
|
-
|
|
29
|
-
Either option is incompatible with the AI-as-primary-author contract
|
|
30
|
-
([ADR-0007](0007-ai-as-primary-author.md)): the AI is
|
|
31
|
-
working *inside the consumer's project*, not inside the SDK monorepo.
|
|
32
|
-
It cannot extend the SDK without a fork-and-publish loop that has none
|
|
33
|
-
of the three feedback loops the contract guarantees.
|
|
14
|
+
The four-atom model promises that domain shape is authored in the consumer's
|
|
15
|
+
project. Core therefore cannot ship a fixed manifest set: doing so would force
|
|
16
|
+
every new Schema, View, Procedure, or Trigger through an SDK fork/release.
|
|
34
17
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
18
|
+
The first v0.1 implementation imported YAML as Wrangler Text modules and parsed
|
|
19
|
+
it during Worker startup. That kept ownership correct but leaked authoring-only
|
|
20
|
+
YAML machinery into the runtime bundle and coupled consumers to Wrangler
|
|
21
|
+
`[[rules]]`. The public CLI now provides one build-time compilation boundary.
|
|
38
22
|
|
|
39
23
|
## Decision
|
|
40
24
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
registry feeds `createCmsRuntime(...).bootInit()`, the HTTP Trigger
|
|
45
|
-
mounts, the View executor, and the MCP tool catalog.
|
|
25
|
+
Consumers own YAML under `manifests/`. The installed `mantle` CLI parses and
|
|
26
|
+
validates that YAML, then writes the machine-owned runtime module and handler
|
|
27
|
+
types:
|
|
46
28
|
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
readonly handlers?: Readonly<Record<string, AnyHandler>>;
|
|
51
|
-
/** Parsed consumer-authored manifests. Starters usually import YAML
|
|
52
|
-
* as text via Wrangler rules and call `parseManifestsOrThrow()`
|
|
53
|
-
* before passing them to the adapter. */
|
|
54
|
-
readonly manifests: readonly Manifest[];
|
|
55
|
-
}
|
|
29
|
+
```bash
|
|
30
|
+
pnpm exec mantle generate
|
|
31
|
+
pnpm exec mantle generate --check
|
|
56
32
|
```
|
|
57
33
|
|
|
58
|
-
The
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
34
|
+
The outputs are:
|
|
35
|
+
|
|
36
|
+
- `.mantle/generated/site.ts` — a parser-free `readonly Manifest[]` export;
|
|
37
|
+
- `.mantle/generated/types.d.ts` — handler declarations derived from the same
|
|
38
|
+
validated manifest set.
|
|
39
|
+
|
|
40
|
+
The Worker imports the generated array rather than YAML text:
|
|
62
41
|
|
|
63
42
|
```ts
|
|
64
|
-
|
|
65
|
-
import
|
|
66
|
-
import contactYaml from "../manifests/contact.yaml";
|
|
67
|
-
import { sendContactMessage } from "./handlers/send-contact-message.js";
|
|
68
|
-
|
|
69
|
-
export const cmsConfig: CmsConfig = {
|
|
70
|
-
manifests: [postsYaml, contactYaml],
|
|
71
|
-
handlers: { "send-contact-message": sendContactMessage },
|
|
72
|
-
};
|
|
73
|
-
```
|
|
43
|
+
import { createMantleWorker } from "@aotter/mantle/cloudflare";
|
|
44
|
+
import { manifest } from "../.mantle/generated/site.js";
|
|
74
45
|
|
|
75
|
-
|
|
76
|
-
# wrangler.toml — bundle every manifests/*.yaml as text
|
|
77
|
-
[[rules]]
|
|
78
|
-
type = "Text"
|
|
79
|
-
globs = ["**/*.yaml"]
|
|
80
|
-
fallthrough = true
|
|
46
|
+
export default createMantleWorker({ manifest });
|
|
81
47
|
```
|
|
82
48
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
49
|
+
Low-level composition passes the same array as `CmsConfig.manifests`. Core and
|
|
50
|
+
adapters ship zero application manifests. The registry, boot validator, View
|
|
51
|
+
executor, HTTP/MCP mounts, and tool catalog are built only from the consumer's
|
|
52
|
+
generated array.
|
|
87
53
|
|
|
88
|
-
|
|
54
|
+
`mantle generate --check` must gate validation/deploy so authored YAML cannot
|
|
55
|
+
drift from checked-in generated output. Generation never edits YAML, handlers,
|
|
56
|
+
skills, package metadata, styles, or provider configuration.
|
|
89
57
|
|
|
90
|
-
|
|
91
|
-
only shipping adapter in v0.1.0, so the Text-import-via-`[[rules]]`
|
|
92
|
-
pattern is the canonical path. When other adapters (Netlify, etc.)
|
|
93
|
-
ship, they will need their own equivalent text-import mechanism, but
|
|
94
|
-
the SDK boundary remains unchanged: an array of YAML strings in,
|
|
95
|
-
parsed registry out.
|
|
58
|
+
### Adapter scope
|
|
96
59
|
|
|
97
|
-
|
|
60
|
+
Compilation is adapter-independent and uses Node at author/build time. The
|
|
61
|
+
Worker runtime receives plain typed objects, so Cloudflare needs no YAML Text
|
|
62
|
+
module rule and a future adapter needs no equivalent bundler hook.
|
|
98
63
|
|
|
99
|
-
|
|
100
|
-
internally. v0.1.0 lands consumer-supplied manifests as the only
|
|
101
|
-
path from day one — no embedded fallback, no deprecation window, no
|
|
102
|
-
override semantics. This is a fresh build with no existing v0.1.x
|
|
103
|
-
deployments to migrate.
|
|
64
|
+
### Single authority
|
|
104
65
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
the boot validator surfacing the empty set as a warning.
|
|
110
|
-
- No conflict policy is needed: the SDK ships no manifests, so there
|
|
111
|
-
is no merge surface where an override could disagree with a default.
|
|
66
|
+
YAML remains the authored source of truth. Generated TypeScript is a
|
|
67
|
+
deterministic transport artifact and must not be hand-edited. Parser
|
|
68
|
+
diagnostics point to the consumer manifest document/pointer before generation;
|
|
69
|
+
runtime boot validation covers cross-manifest and registered-handler facts.
|
|
112
70
|
|
|
113
71
|
## Worked example
|
|
114
72
|
|
|
115
|
-
The
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
the
|
|
119
|
-
`yaml.d.ts`, and the `src/mantleConfig.ts` file that wires manifests
|
|
120
|
-
+ handlers into the SDK. The blog `SKILL.md` walks AI install agents
|
|
121
|
-
through the sequence.
|
|
73
|
+
The external [`aotter/mantle-starters`](https://github.com/aotter/mantle-starters)
|
|
74
|
+
repository owns the blank project and typed overlays. Materialized projects
|
|
75
|
+
carry their own `manifests/`, generated module, handlers, and package scripts;
|
|
76
|
+
they consume the exact packed Core artifact rather than a workspace link.
|
|
122
77
|
|
|
123
78
|
## Consequences
|
|
124
79
|
|
|
125
|
-
###
|
|
126
|
-
|
|
127
|
-
- Consumers can compose all four atoms
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
manifests across all four feedback loops.
|
|
134
|
-
- The SDK bundle ships no manifest content. Consumers who don't need
|
|
135
|
-
a given starter's atoms (e.g. a docs site that only uses
|
|
136
|
-
`posts`-shaped content) don't pay for unrelated atoms.
|
|
137
|
-
- Text-imported YAML keeps manifests as the YAML source-of-truth on
|
|
138
|
-
disk — no codegen step in the build, no parser drift between
|
|
139
|
-
author-time YAML and runtime parse. Multi-doc YAML grouping
|
|
140
|
-
([ADR-0001](0001-four-atom-manifest-model.md) §"Authoring shape: multi-doc YAML")
|
|
141
|
-
is preserved end-to-end.
|
|
142
|
-
- The MCP tool catalog automatically picks up the consumer's atoms —
|
|
143
|
-
no separate registration call. The consumer adds a `comments` Schema
|
|
144
|
-
and the operator agent sees `create_draft_comments` /
|
|
145
|
-
`update_draft_comments` on the next deploy.
|
|
80
|
+
### Benefits
|
|
81
|
+
|
|
82
|
+
- Consumers can compose all four atoms without forking or publishing Core.
|
|
83
|
+
- One CLI parse/validation path drives runtime objects and handler types.
|
|
84
|
+
- Worker bundles do not include the YAML parser or consumer YAML text imports.
|
|
85
|
+
- Every REST/MCP catalog automatically reflects the generated consumer set.
|
|
86
|
+
- Other adapters consume the same `readonly Manifest[]` with no filesystem or
|
|
87
|
+
bundler-specific manifest loader.
|
|
146
88
|
|
|
147
89
|
### Costs
|
|
148
90
|
|
|
149
|
-
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
- **Build-step coupling.** Text imports depend on the consumer's
|
|
167
|
-
bundler. If a future Wrangler version changes the `[[rules]]`
|
|
168
|
-
surface the docs need updating. Mitigation: spec the import
|
|
169
|
-
contract loosely — "a string carrying the YAML text" — so any
|
|
170
|
-
other path (codegen, embedded literal, `fetch` from KV) works the
|
|
171
|
-
same way at the SDK boundary.
|
|
172
|
-
- **Manifest drift between dev and prod.** The consumer edits a YAML
|
|
173
|
-
file; until they re-run `wrangler deploy`, the runtime keeps
|
|
174
|
-
serving the previous bundle. This is the existing Worker dev-loop;
|
|
175
|
-
the validate CLI catches the static checks pre-deploy and the
|
|
176
|
-
boot validator catches handler-ref mismatches at deploy. No new
|
|
177
|
-
risk; just call out in the docs that "rebuild required after
|
|
178
|
-
manifest edit."
|
|
179
|
-
- **Multi-tenant deployments amplifying conflict.** A single Worker
|
|
180
|
-
running multiple tenants would want each tenant's manifests
|
|
181
|
-
isolated. The atom model has no `namespace` field
|
|
182
|
-
([ADR-0001](0001-four-atom-manifest-model.md)) on purpose —
|
|
183
|
-
multi-tenancy lives in the consumer app layer with `tenant_id`
|
|
184
|
-
columns. Consumer-supplied manifests don't change this; the SDK
|
|
185
|
-
still sees one flat manifest set per Worker. Multi-tenant
|
|
186
|
-
SaaS-on-this-CMS is a separate design question.
|
|
187
|
-
- **Manifest set growth.** A large consumer (50+ Procedures, 20+
|
|
188
|
-
Schemas) makes per-file imports verbose. Mitigation: a future
|
|
189
|
-
ergonomic helper (`loadManifestsFromGlob`) can be added without
|
|
190
|
-
changing the SDK contract — the contract is "pass an array of YAML
|
|
191
|
-
strings"; how the consumer assembles the array is their choice.
|
|
91
|
+
- Projects keep deterministic generated files and must run `generate` after
|
|
92
|
+
editing YAML.
|
|
93
|
+
- CI/deploy must run `generate --check`; otherwise an old generated module can
|
|
94
|
+
outlive its source YAML.
|
|
95
|
+
- Merge conflicts in generated output are resolved by re-running the installed
|
|
96
|
+
command, never by editing the artifact.
|
|
97
|
+
|
|
98
|
+
### Risks and controls
|
|
99
|
+
|
|
100
|
+
- **Version skew:** always run the generator from the installed package and
|
|
101
|
+
read its embedded docs. Do not generate a versioned project from `develop`.
|
|
102
|
+
- **Hidden stale output:** keep `mantle generate --check` in the normal project
|
|
103
|
+
validation gate.
|
|
104
|
+
- **Runtime/parser divergence:** generated objects come from the same parser
|
|
105
|
+
used by `validate`; focused packed-package tests compare the consumer output.
|
|
106
|
+
- **Multi-tenancy pressure:** Core still sees one flat manifest set per Worker.
|
|
107
|
+
Tenant isolation remains an application concern; this ADR adds no namespace.
|
|
192
108
|
|
|
193
109
|
## Alternatives considered
|
|
194
110
|
|
|
195
|
-
**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
**
|
|
205
|
-
|
|
206
|
-
not have a runtime filesystem. Even at build time, Wrangler doesn't
|
|
207
|
-
provide a hook for the SDK to read consumer files — that's the
|
|
208
|
-
consumer's bundler's job, which is exactly what Text-imported YAML
|
|
209
|
-
is for.
|
|
210
|
-
|
|
211
|
-
**(C) Separate `@aotter/mantle-manifests-<consumer>` package** —
|
|
212
|
-
each consumer ships their manifests as an npm package; the SDK
|
|
213
|
-
imports from `mantle-manifests-blog` etc. Rejected: 1:N package
|
|
214
|
-
overhead for what should be a directory of YAML files. Tooling pain
|
|
215
|
-
(versioning, publishing) for a layer that is fundamentally
|
|
216
|
-
consumer-internal. Useful if a community emerges around shared
|
|
217
|
-
manifest sets ("here's a forum schema as a package"), but YAGNI for
|
|
218
|
-
the v0.1 baseline — that ergonomic can be added later by a wrapper
|
|
219
|
-
that calls the mount factory with `pkg.manifests`.
|
|
220
|
-
|
|
221
|
-
**(D) Embed manifests in the SDK and ignore the question** — ship
|
|
222
|
-
the SDK with a fixed manifest set baked in. Rejected: fails the
|
|
223
|
-
ADR-0001 promise and the AI-author contract. The SDK is supposed to
|
|
224
|
-
be content-agnostic; making it content-prescriptive permanently is
|
|
225
|
-
a strategic mistake.
|
|
226
|
-
|
|
227
|
-
**(E) Override semantics for conflicts** — last-write-wins, with the
|
|
228
|
-
consumer's manifest beating the SDK's. Rejected: only relevant if
|
|
229
|
-
the SDK ships embedded manifests alongside consumer ones. With the
|
|
230
|
-
SDK shipping zero manifests there is no conflict surface.
|
|
111
|
+
**Runtime YAML Text imports.** Replaced. They require Wrangler-specific rules,
|
|
112
|
+
ambient YAML modules, and ship authoring-only parse machinery in the Worker.
|
|
113
|
+
|
|
114
|
+
**Runtime filesystem discovery.** Rejected. Workers have no runtime filesystem,
|
|
115
|
+
and hidden discovery would make inputs and diagnostics less deterministic.
|
|
116
|
+
|
|
117
|
+
**Application-specific manifest packages.** Rejected for the baseline. It adds
|
|
118
|
+
publish/version overhead to files that belong in one consumer repo.
|
|
119
|
+
|
|
120
|
+
**SDK-embedded manifests or override precedence.** Rejected. Core is
|
|
121
|
+
content-agnostic, so there is no default manifest set and no merge policy.
|
|
231
122
|
|
|
232
123
|
## How to apply
|
|
233
124
|
|
|
234
|
-
When
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
Wrangler's text rule covers plain imports.
|
|
248
|
-
3. Pass `manifests: [postsYaml, contactYaml, ...]` in the exported
|
|
249
|
-
`cmsConfig` object.
|
|
250
|
-
|
|
251
|
-
The blog `SKILL.md` walks this sequence.
|
|
125
|
+
When a Core feature needs the manifest set, accept the parsed consumer-owned
|
|
126
|
+
array; never hardcode starter collection names or read consumer files at
|
|
127
|
+
runtime.
|
|
128
|
+
|
|
129
|
+
When authoring or reviewing a generated project:
|
|
130
|
+
|
|
131
|
+
1. Edit YAML only under `manifests/`.
|
|
132
|
+
2. Run the installed `pnpm exec mantle generate`.
|
|
133
|
+
3. Import `manifest` from `.mantle/generated/site.js` into the conventional
|
|
134
|
+
Worker, or pass it as `CmsConfig.manifests` in low-level composition.
|
|
135
|
+
4. Run `pnpm exec mantle generate --check` in validation and before deploy.
|
|
136
|
+
5. Reject Wrangler YAML Text rules, runtime `parseManifests*` calls, or a second
|
|
137
|
+
manifest loader in a new generated project.
|
|
252
138
|
|
|
253
139
|
## Implementation status
|
|
254
140
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
- `
|
|
258
|
-
- The mount factory builds the registry from the consumer-supplied
|
|
259
|
-
YAML; no SDK fallback.
|
|
260
|
-
- The `@aotter/mantle-cloudflare` package ships zero embedded
|
|
261
|
-
manifests.
|
|
262
|
-
- The starter at `starters/blog/` is self-contained: `manifests/`,
|
|
263
|
-
`src/yaml.d.ts`, `wrangler.toml` `[[rules]]` block, and
|
|
264
|
-
`src/mantleConfig.ts` wiring it together.
|
|
265
|
-
- Boot validator's `path` field on `INVALID_MANIFEST_ENVELOPE`-class
|
|
266
|
-
errors uses the consumer-supplied YAML index
|
|
267
|
-
(e.g. `consumer-manifest:[2]#/spec/...`) so deploy logs point at
|
|
268
|
-
the right file.
|
|
269
|
-
|
|
270
|
-
Tracking: aotter/mantle.
|
|
141
|
+
Implemented by the umbrella `mantle generate` command and the conventional
|
|
142
|
+
Cloudflare Worker facade. Exact commands and output paths are documented in the
|
|
143
|
+
version-matched `@aotter/mantle` README and embedded Core skills.
|
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
# ADR-0012: Views as
|
|
1
|
+
# ADR-0012: Views as named REST and MCP read surfaces
|
|
2
2
|
|
|
3
|
-
**Status:** Accepted for v0.1.0.
|
|
3
|
+
**Status:** Accepted for v0.1.0. Amended to match the shipped public/staff
|
|
4
|
+
surface and authorization contract.
|
|
4
5
|
|
|
5
|
-
**Date:** 2026-05-05
|
|
6
|
+
**Date:** 2026-05-05; amended 2026-08-03
|
|
6
7
|
|
|
7
8
|
## Context
|
|
8
9
|
|
|
9
10
|
mantle ships two read-side surfaces and one write-side surface:
|
|
10
11
|
|
|
11
12
|
- **Templates** (rendered HTML / Markdown / `llms.txt`) — composed by the consumer's `TemplateRegistry` from runtime APIs. The starter blog uses these for `/{locale}/posts/{slug}` etc.
|
|
12
|
-
- **MCP tools** —
|
|
13
|
+
- **MCP tools** — the staff surface exposes entry authoring and lifecycle
|
|
14
|
+
tools; both public and staff surfaces expose `query_view_<name>` for Views
|
|
15
|
+
assigned to that surface.
|
|
13
16
|
- **HTTP Triggers** — write-side endpoints declared by the consumer (`Trigger.source.kind: http`, methods `POST | PUT | PATCH | DELETE` only). Per ADR-0001 grammar, **`GET` is intentionally absent** because read endpoints belong to Views, not Procedures.
|
|
14
17
|
|
|
15
18
|
What was missing: a stable, consumer-facing **public REST read surface**. The starter blog had no JSON API at all. The CMS needed an answer to "I'm a downstream service that wants `posts` filtered by locale — how do I read?" without forcing every consumer to hand-write a route handler that re-implements filtering.
|
|
16
19
|
|
|
17
|
-
This ADR answers that question: **every parsed View
|
|
20
|
+
This ADR answers that question: **every parsed View is a named read surface**.
|
|
21
|
+
Views default to public and auto-expose `GET /api/views/<view-name>` plus a
|
|
22
|
+
matching public MCP tool. `surface: staff` moves both transports to the guarded
|
|
23
|
+
staff surfaces. Schemas do not get a public REST surface at all.
|
|
18
24
|
|
|
19
25
|
(Schemas remain available on the **admin** REST surface — `/admin/api/*` — which lands with the admin UI commit and is auth-gated to staff. That's a separate cut and out of scope here.)
|
|
20
26
|
|
|
@@ -34,13 +40,30 @@ No version prefix. `apiVersion: cms.mantle.aotter.net/v1` is the manifest-gramma
|
|
|
34
40
|
|
|
35
41
|
`<view-name>` is `View.metadata.name` verbatim. Authors are free to pick kebab-case (`recent-posts`) or any URL-safe identifier; the runtime mounts the route as-is.
|
|
36
42
|
|
|
37
|
-
### 3. Views auto-expose
|
|
43
|
+
### 3. Views auto-expose on one declared surface
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
`View.spec.surface` uses the closed `public | staff` vocabulary and defaults to
|
|
46
|
+
`public`:
|
|
47
|
+
|
|
48
|
+
- public Views mount at `GET /api/views/<name>` and appear as
|
|
49
|
+
`query_view_<name>` on `/mcp`;
|
|
50
|
+
- staff Views mount at `GET /admin/api/views/<name>` behind the live staff-role
|
|
51
|
+
gate and appear only on `/mcp/staff`.
|
|
52
|
+
|
|
53
|
+
The adapter filters the View set before constructing each MCP dispatcher, so a
|
|
54
|
+
guessed public tool call cannot reach a staff View. `View.spec.requires` then
|
|
55
|
+
applies the same static predicates and optional guard Procedure on REST and MCP
|
|
56
|
+
calls. Surface selects transport visibility; authorization decides whether the
|
|
57
|
+
verified caller may execute the View.
|
|
58
|
+
|
|
59
|
+
We still reject a second `expose.rest` switch. A View is externally queryable
|
|
60
|
+
on exactly one surface; internal-only helpers remain TypeScript code.
|
|
40
61
|
|
|
41
62
|
### 4. Pagination knobs are reserved query-string names
|
|
42
63
|
|
|
43
|
-
|
|
64
|
+
REST callers pass `?page=<1-indexed>&show=<page-size>`. MCP callers pass the
|
|
65
|
+
same reserved names as tool arguments. Internally the runtime emits
|
|
66
|
+
`LIMIT show OFFSET (page-1)*show`.
|
|
44
67
|
|
|
45
68
|
`page` / `show` / `cursor` are reserved names. The parser rejects any `View.spec.params.properties.<name>` colliding with these (`VIEW_PARAMS_RESERVED_NAME`). The author owns the rest of the query-string namespace.
|
|
46
69
|
|
|
@@ -93,7 +116,7 @@ The required-only rule is a v0.1.0 simplification. v0.1.x will promote optional-
|
|
|
93
116
|
|
|
94
117
|
`hasMore = (rows.length === effectiveShow)` — the lazy semantics. We do **not** issue a separate `COUNT(*)` query, and we do not pull `LIMIT n+1` to probe. If the server returns exactly `show` rows, the caller may or may not have more; if fewer, we know definitively. The trade is one false-positive on the boundary case (caller asks for next page, gets empty) in exchange for no extra round-trip per request.
|
|
95
118
|
|
|
96
|
-
### 7. Param coercion happens at the
|
|
119
|
+
### 7. Param coercion happens at the transport boundary
|
|
97
120
|
|
|
98
121
|
Query strings arrive as strings; `View.spec.params` declares the JSON Schema type. The Cloudflare adapter (`coerceViewParams` in `mountServerEndpoints.ts`) coerces per-property:
|
|
99
122
|
|
|
@@ -109,29 +132,34 @@ Required params not present → `400 INPUT_VALIDATION_FAILED`. Coercion failure
|
|
|
109
132
|
|
|
110
133
|
## Out of scope (deferred)
|
|
111
134
|
|
|
112
|
-
- **MCP tools for Views.** MCP is for agents doing ops; readers don't need a separate MCP tool when they have a stable REST endpoint. Reconsider in v0.2 if downstream agent tooling demands it.
|
|
113
135
|
- **`Trigger.target.view`** (lifecycle/projection triggers fired by Views). Tracked separately as a v0.2 grammar move.
|
|
114
136
|
- **`spec.output.kind`** (declaring scalar / tree / tabular result shape per View). Lands with join + group-by support in v0.1.x.
|
|
115
137
|
- **Optional param-ref drop semantics in the parser.** Runtime is already implemented; parser promotes when v0.1.x lands.
|
|
116
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.
|
|
117
|
-
- **
|
|
139
|
+
- **Row-level policy rewriting.** `requires` authorizes the whole View; it does
|
|
140
|
+
not inject per-row visibility predicates. Consumer-specific membership,
|
|
141
|
+
payment, or entitlement checks belong in the optional guard Procedure.
|
|
118
142
|
|
|
119
143
|
## Consequences
|
|
120
144
|
|
|
121
145
|
**Authors gain:**
|
|
122
|
-
-
|
|
123
|
-
|
|
146
|
+
- REST and MCP read surfaces for free — declare a View and choose public or
|
|
147
|
+
staff visibility once.
|
|
148
|
+
- Single mental model for "how do consumers and agents read?": always Views.
|
|
124
149
|
- Cheap pagination + dynamic filters without hand-writing handlers.
|
|
125
150
|
|
|
126
151
|
**Authors lose:**
|
|
127
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.
|
|
128
|
-
- No
|
|
153
|
+
- No internal-only View surface; choose public/staff or keep the query in a TS
|
|
154
|
+
helper.
|
|
129
155
|
|
|
130
156
|
**Runtime gains:**
|
|
131
|
-
- One
|
|
157
|
+
- One executor and response shape cover public/staff REST and MCP reads.
|
|
132
158
|
- Forward-compat for join / group-by / aggregation: envelope generalises by Views declaring `output.kind` later.
|
|
133
159
|
|
|
134
160
|
**Reviewers / future contributors should:**
|
|
135
161
|
- Reject any PR adding `Schema.spec.expose.rest` or a similar Schema-level public-read flag.
|
|
136
162
|
- Reject any PR introducing a second public read surface (e.g. `/api/<collection>` shortcut).
|
|
163
|
+
- Require REST and MCP mounts to filter by the same `View.spec.surface` value,
|
|
164
|
+
and keep authorization in the shared `ExecuteViewUseCase` path.
|
|
137
165
|
- Reject any PR that lets `filter` reference state outside the declared `params` (e.g. `{ $env: ... }`, `{ $cookie: ... }`) without a matching ADR amendment.
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
# ADR-0014:
|
|
1
|
+
# ADR-0014: Adapter-owned identity and MCP authorization
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
Accepted
|
|
5
|
+
Accepted. Amended 2026-05-14, 2026-05-15, 2026-06-30, 2026-07-15, and
|
|
6
|
+
2026-08-03.
|
|
6
7
|
|
|
7
8
|
## Date
|
|
8
9
|
|
|
9
|
-
2026-05-09 (amended 2026-
|
|
10
|
+
2026-05-09 (last amended 2026-08-03)
|
|
11
|
+
|
|
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.
|
|
10
16
|
|
|
11
17
|
## Context
|
|
12
18
|
|
|
@@ -40,7 +46,7 @@ A 2026 Workers-friendly auth library — [Better Auth](https://better-auth.com)
|
|
|
40
46
|
|
|
41
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.
|
|
42
48
|
|
|
43
|
-
## Decision
|
|
49
|
+
## Decision (historical baseline; amended below)
|
|
44
50
|
|
|
45
51
|
### 1. Better Auth replaces both layers
|
|
46
52
|
|
|
@@ -310,7 +316,7 @@ Keep our `staff` overlay and `D1StaffRepository`. Use Better Auth only for ident
|
|
|
310
316
|
|
|
311
317
|
**Rejected** — duplicate role data (Better Auth `admin` plugin + our staff overlay) is worse than picking one. Audit trail is the only thing the standalone overlay buys, and v0.1.0 doesn't need it.
|
|
312
318
|
|
|
313
|
-
## Implementation status
|
|
319
|
+
## Implementation status (historical snapshot)
|
|
314
320
|
|
|
315
321
|
Phase 0 (spike, 0.5–1d) — pending:
|
|
316
322
|
|
|
@@ -351,13 +357,31 @@ Phase 3 (v0.2+, with community / fan-club):
|
|
|
351
357
|
|
|
352
358
|
When reviewing or implementing a change that touches auth, MCP routing, or roles:
|
|
353
359
|
|
|
354
|
-
1. **Identity
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
360
|
+
1. **Identity and local sessions** — depend on the adapter's public `Auth`
|
|
361
|
+
interface. `createAuth()` is the curated Better Auth-backed Cloudflare
|
|
362
|
+
default; do not import Better Auth internals outside that implementation or
|
|
363
|
+
add an un-curated passthrough.
|
|
364
|
+
2. **Mutable staff privilege** — call `auth.getUserRole(userId)` on every
|
|
365
|
+
protected REST/MCP invocation. Do not trust a role captured in an OAuth
|
|
366
|
+
grant or long-lived token.
|
|
367
|
+
3. **MCP transport** — export `createOAuthProvider(...)` at the Worker top
|
|
368
|
+
level. It owns DCR/PKCE/token verification and dispatches `/mcp` and
|
|
369
|
+
`/mcp/staff`; the consent handler uses the current local `Auth` session.
|
|
370
|
+
4. **MCP surface** — use explicit `View.spec.surface` and
|
|
371
|
+
`Trigger.source.surface`. The adapter pre-filters each catalog; the shared
|
|
372
|
+
runtime evaluator then enforces the target's `requires.auth.all` and
|
|
373
|
+
optional guard on every call.
|
|
374
|
+
5. **Scopes and props** — advertise the compatibility scope `mcp`. Store only
|
|
375
|
+
immutable grant identity (`userId`, `clientId`, scopes); never store mutable
|
|
376
|
+
staff role or raw/refresh tokens in runtime context.
|
|
377
|
+
6. **REST credentials** — normalize sessions, OAuth JWTs, and an optional
|
|
378
|
+
consumer `credentialResolver` into `HandlerContext`. A recognized invalid
|
|
379
|
+
API key/PAT fails closed and never falls back to a cookie. Standard remote
|
|
380
|
+
MCP does not promise raw REST key/PAT support.
|
|
381
|
+
7. **Adapter portability** — auth remains adapter-owned. A future adapter
|
|
382
|
+
verifies its platform's credentials and supplies the same normalized
|
|
383
|
+
runtime context; `mantle-runtime` does not gain a Better Auth dependency or
|
|
384
|
+
an auth storage port.
|
|
361
385
|
|
|
362
386
|
## Sources
|
|
363
387
|
|
|
@@ -408,17 +432,19 @@ The original ADR-0014 §"Auth as contract, Better Auth as default" framing stays
|
|
|
408
432
|
|
|
409
433
|
### What didn't change
|
|
410
434
|
|
|
411
|
-
- The auth port
|
|
435
|
+
- The auth storage port remains removed; the adapter owns `Auth` and passes
|
|
436
|
+
verified, normalized caller context into runtime dispatchers.
|
|
412
437
|
- Apple's `trustedOrigins` auto-append (`https://appleid.apple.com`) and `sameSite=none` cookie injection for cross-site `form_post` callback stay.
|
|
413
438
|
- `appleClientSecret()` helper (from PR #173) stays.
|
|
414
439
|
- All non-OAuth admin endpoints (`/api/auth/*`, `/api/auth/methods`, admin SPA mount) stay on Better Auth.
|
|
415
440
|
- The `bootstrapOwner` + email-OTP + magic-link + `methods[]` carve-out stay.
|
|
416
441
|
|
|
417
|
-
###
|
|
442
|
+
### Compatibility constraints to re-test before changing
|
|
418
443
|
|
|
419
|
-
-
|
|
420
|
-
|
|
421
|
-
-
|
|
444
|
+
- Anthropic clients required the `/mcp` resource-path prefix during the
|
|
445
|
+
carve-out verification.
|
|
446
|
+
- Colon-shaped advertised scopes broke the same clients; Mantle therefore
|
|
447
|
+
advertises one `mcp` scope and re-evaluates target authorization server-side.
|
|
422
448
|
|
|
423
449
|
## Amendment — 2026-06-30: Hosted-auth boundary and first-party cookie fields
|
|
424
450
|
|