@aotter/mantle 0.1.0-alpha.8 → 0.1.2-alpha.1

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 (104) hide show
  1. package/README.md +38 -38
  2. package/dist/cli/generate.d.ts +10 -1
  3. package/dist/cli/generate.d.ts.map +1 -1
  4. package/dist/cli/generate.js +49 -4
  5. package/dist/cli/generate.js.map +1 -1
  6. package/dist/cli/main.js +0 -8
  7. package/dist/cli/main.js.map +1 -1
  8. package/docs/adapter-guide.md +20 -24
  9. package/docs/adr/0008-structured-diagnostic-shape.md +7 -2
  10. package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +100 -0
  11. package/docs/adr/0018-core-starters-repository-boundary.md +3 -0
  12. package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +37 -1
  13. package/docs/adr/0020-builtin-handler-contracts-and-matched-upsert.md +199 -0
  14. package/docs/adr/0021-retire-starter-scaffolding.md +30 -0
  15. package/docs/adr/0022-caller-observed-version-occ.md +212 -0
  16. package/docs/adr/0023-port-failure-contract.md +69 -0
  17. package/docs/adr/README.md +18 -3
  18. package/docs/adr/adr-lite-803-request-diagnostics.md +38 -0
  19. package/docs/adr/adr-lite-808-route-readiness.md +47 -0
  20. package/docs/adr/adr-lite-809-bounded-public-content.md +71 -0
  21. package/docs/adr/adr-lite-812-native-parity.md +149 -0
  22. package/docs/adr/adr-lite-823-home-statistics.md +63 -0
  23. package/docs/adr/adr-lite-842-mcp-authoring.md +30 -0
  24. package/docs/adr/adr-lite-861-admin-webmcp.md +42 -0
  25. package/docs/adr/adr-lite-909-admin-ui-kit.md +37 -0
  26. package/docs/api-mcp-authorization.md +3 -664
  27. package/docs/auth-hosting-model.md +4 -4
  28. package/docs/cloudflare-low-level-composition.md +3 -104
  29. package/docs/deferred-lifecycle-queues.md +2 -243
  30. package/docs/design-atoms.md +16 -796
  31. package/docs/direct-authoring.md +5 -0
  32. package/docs/examples/minimal-worker/README.md +37 -0
  33. package/docs/examples/minimal-worker/manifests/site.yaml +25 -0
  34. package/docs/examples/minimal-worker/package.json +26 -0
  35. package/docs/examples/minimal-worker/smoke.mjs +23 -0
  36. package/docs/examples/minimal-worker/src/index.ts +4 -0
  37. package/docs/examples/minimal-worker/tsconfig.json +17 -0
  38. package/docs/examples/minimal-worker/wrangler.jsonc +22 -0
  39. package/docs/handbook/cloudflare/authentication.md +167 -0
  40. package/docs/handbook/cloudflare/bindings.md +233 -0
  41. package/docs/handbook/cloudflare/conventional-worker.md +162 -0
  42. package/docs/handbook/cloudflare/deferred-hooks-queues.md +171 -0
  43. package/docs/handbook/cloudflare/deploy-and-operate.md +98 -0
  44. package/docs/handbook/cloudflare/low-level-composition.md +169 -0
  45. package/docs/handbook/cloudflare/media-r2.md +216 -0
  46. package/docs/handbook/cloudflare/public-web.md +144 -0
  47. package/docs/handbook/concepts/authorization.md +116 -0
  48. package/docs/handbook/concepts/four-atoms.md +173 -0
  49. package/docs/handbook/concepts/lifecycle-and-locales.md +142 -0
  50. package/docs/handbook/concepts/mcp-and-agents.md +109 -0
  51. package/docs/handbook/concepts/procedures-and-triggers.md +221 -0
  52. package/docs/handbook/concepts/runtime-and-adapters.md +177 -0
  53. package/docs/handbook/concepts/views.md +183 -0
  54. package/docs/handbook/examples/commerce-transaction.md +813 -0
  55. package/docs/handbook/examples/guarded-api.md +427 -0
  56. package/docs/handbook/examples/intake-form.md +319 -0
  57. package/docs/handbook/examples/legal-documents.md +218 -0
  58. package/docs/handbook/examples/procurement-approvals.md +240 -0
  59. package/docs/handbook/examples/publication.md +240 -0
  60. package/docs/handbook/examples/reservation.md +220 -0
  61. package/docs/handbook/navigation.json +63 -0
  62. package/docs/handbook/reference/authorization.md +295 -0
  63. package/docs/handbook/reference/diagnostics.md +183 -0
  64. package/docs/handbook/reference/manifest.md +132 -0
  65. package/docs/handbook/reference/procedure.md +263 -0
  66. package/docs/handbook/reference/schema.md +247 -0
  67. package/docs/handbook/reference/site-config.md +233 -0
  68. package/docs/handbook/reference/surface.md +207 -0
  69. package/docs/handbook/reference/trigger.md +193 -0
  70. package/docs/handbook/reference/view.md +256 -0
  71. package/docs/handbook/start/project-and-cli.md +122 -0
  72. package/docs/handbook/start/quickstart-worker.md +193 -0
  73. package/docs/labels.md +3 -1
  74. package/docs/media-uploads.md +3 -184
  75. package/docs/migration-0.1.2.md +34 -1
  76. package/docs/performance-harness.md +125 -8
  77. package/docs/release-process.md +85 -231
  78. package/docs/schema-indexes.md +3 -180
  79. package/docs/sealed-pipeline-ownership.md +5 -4
  80. package/docs/spec-only-host-adoption.md +158 -0
  81. package/docs/transaction-patterns.md +5 -0
  82. package/package.json +21 -25
  83. package/skills/README.md +4 -4
  84. package/skills/develop/SKILL.md +23 -28
  85. package/skills/install/SKILL.md +55 -147
  86. package/skills/provision/SKILL.md +4 -3
  87. package/skills/theme/SKILL.md +6 -6
  88. package/skills/update/SKILL.md +32 -68
  89. package/dist/cli/create.d.ts +0 -2
  90. package/dist/cli/create.d.ts.map +0 -1
  91. package/dist/cli/create.js +0 -243
  92. package/dist/cli/create.js.map +0 -1
  93. package/dist/cli/update.d.ts +0 -2
  94. package/dist/cli/update.d.ts.map +0 -1
  95. package/dist/cli/update.js +0 -413
  96. package/dist/cli/update.js.map +0 -1
  97. package/dist/provision/renderProvisionBundle.d.ts +0 -70
  98. package/dist/provision/renderProvisionBundle.d.ts.map +0 -1
  99. package/dist/provision/renderProvisionBundle.js +0 -367
  100. package/dist/provision/renderProvisionBundle.js.map +0 -1
  101. package/dist/provision.d.ts +0 -2
  102. package/dist/provision.d.ts.map +0 -1
  103. package/dist/provision.js +0 -2
  104. package/dist/provision.js.map +0 -1
@@ -1,666 +1,5 @@
1
1
  # API and MCP authorization
2
2
 
3
- Mantle gives generated sites one authorization pipeline for manifest HTTP
4
- Triggers, Views, Procedures, and MCP tools. Core verifies or normalizes a
5
- caller, evaluates the closed manifest predicates, invokes an optional dynamic
6
- guard Procedure, and only then reaches the target.
7
-
8
- Mantle does **not** issue or store API keys or personal tokens, define a scope
9
- catalog, read payment-provider state, or decide who is entitled to a product.
10
- Those are site-owned concerns. The Cloudflare adapter supplies a narrow
11
- resolver seam and the runtime supplies the common enforcement machinery.
12
-
13
- ## Ownership boundary
14
-
15
- | Mantle Core SDK | Generated site / Mantle Site |
16
- | --- | --- |
17
- | Curated OAuth resource and audience options | API-key and personal-token generation, hashing, storage, rotation, and revocation |
18
- | JWT verification and linked-provider token facade | Scope names and grant rules |
19
- | `ConsumerCredentialResolver` normalization seam | Account, transaction, subscription, and entitlement tables |
20
- | `HandlerContext.auth`, closed predicates, and guard orchestration | Guard handlers and payment-state freshness rules |
21
- | Consistent REST/MCP diagnostics and reflection | CORS policy and business response fields |
22
-
23
- Authentication and entitlement are deliberately separate. A resolver answers
24
- “is this credential valid, and who/what does it represent?” A guard answers
25
- “is that currently verified caller allowed to perform this business action?”
26
-
27
- ## Core contracts
28
-
29
- After the adapter verifies a caller, runtime handlers see only normalized,
30
- non-secret metadata:
31
-
32
- ```ts
33
- interface HandlerContext {
34
- readonly user: { readonly id: string } | null;
35
- readonly staff: { readonly id: string; readonly role: StaffRole } | null;
36
- readonly auth?: {
37
- readonly credential: "session" | "oauth" | "api-key" | "personal-token";
38
- readonly credentialId: string | null;
39
- readonly clientId: string | null;
40
- readonly scopes: readonly string[];
41
- };
42
- // env, waitUntil, and event omitted here
43
- }
44
- ```
45
-
46
- Raw credentials and refresh tokens never enter this context. The manifest
47
- vocabulary stays closed:
48
-
49
- ```yaml
50
- requires:
51
- auth:
52
- all:
53
- - ctx.auth
54
- - ctx.user
55
- - { "ctx.auth.scope": "orders:read" }
56
- guard:
57
- procedure: require-active-api-access
58
- ```
59
-
60
- - `ctx.auth` requires any verified credential.
61
- - `ctx.user` requires a verified user subject. Service API keys may have no
62
- user.
63
- - each `ctx.auth.scope` entry requires that opaque, site-defined scope;
64
- repeat the predicate to require multiple scopes.
65
- - `ctx.staff` continues to use the closed staff-role list.
66
- - `guard.procedure` names one ordinary, unguarded `handler.kind: ref`
67
- Procedure. It is not a fifth Policy atom.
68
-
69
- The runtime order is fixed:
70
-
71
- 1. verify and normalize the transport credential;
72
- 2. evaluate static predicates before exposing input-schema details;
73
- 3. validate/coerce target input or View params;
74
- 4. invoke the guard with that validated value and the same context;
75
- 5. invoke the target only after the guard succeeds.
76
-
77
- Missing/invalid credentials return `401`; a verified caller missing a required
78
- role or scope returns `403`; a site guard may return
79
- `ENTITLEMENT_REQUIRED`/`402`. Guards run on every call and are not cached.
80
-
81
- ### Identity-bound Views
82
-
83
- Use the closed `{ "$ctx.user": "id" }` filter sentinel for rows owned by the
84
- current site-local Better Auth user. The caller never supplies this value, so
85
- the same View is safe on both REST and public MCP:
86
-
87
- ```yaml
88
- apiVersion: cms.mantle.aotter.net/v1
89
- kind: Schema
90
- metadata: { name: orders }
91
- spec:
92
- schema:
93
- type: object
94
- properties:
95
- userId: { type: string, x-mantle-bind: ctx.user }
96
- orderNumber: { type: string }
97
- orderStatus: { type: string }
98
- totalMinor: { type: integer }
99
- placedAt: { type: integer }
100
- indexes: [[userId, placedAt]]
101
- ---
102
- apiVersion: cms.mantle.aotter.net/v1
103
- kind: View
104
- metadata: { name: my-orders }
105
- spec:
106
- surface: public
107
- from: orders
108
- requires:
109
- auth:
110
- all: [ctx.user]
111
- filter:
112
- and:
113
- - { eq: { field: status, value: published } }
114
- - { eq: { field: userId, value: { "$ctx.user": id } } }
115
- fields: [orderNumber, orderStatus, totalMinor, placedAt]
116
- orderBy: [{ field: placedAt, direction: desc }]
117
- limit: 50
118
- ```
119
-
120
- Core rejects this sentinel unless the View requires `ctx.user` and the bound
121
- field is the leftmost field of a declared Schema index. Missing identity fails
122
- with `401`; it never drops the filter or falls back to all rows. REST exposes
123
- `GET /api/views/my-orders`; public MCP exposes `query_view_my_orders`. Both
124
- call `ExecuteViewUseCase` and bind the same `ctx.user.id`.
125
-
126
- The id belongs to the customer site's Better Auth user row. It is not a
127
- Mantle Platform user id, Hosted Auth upstream subject, email, or provider id.
128
- Hosted Auth may establish the site session, but Platform is not part of the
129
- View query path.
130
-
131
- ## Site OAuth symmetry
132
-
133
- A site-issued OAuth access token represents the same caller on public MCP and
134
- manifest HTTP routes. Both surfaces populate `ctx.user` and `ctx.auth` from the
135
- same token grant; expiry, revocation, scope, client, and resource audience are
136
- enforced before the Procedure or View runs.
137
-
138
- ## Cloudflare consumer wiring
139
-
140
- Pass one site-owned resolver to `createMantleRuntimeRef`. Return `not-handled` when the
141
- request is not one of the site's credential formats, `invalid` when it is a
142
- recognized but bad/revoked credential, and `verified` only after checking the
143
- authoritative site record.
144
-
145
- This example table and query are consumer code, not a Mantle migration:
146
-
147
- ```ts
148
- import type { ConsumerCredentialResolver } from "@aotter/mantle/cloudflare";
149
-
150
- type CredentialRow = {
151
- id: string;
152
- kind: "api-key" | "personal-token";
153
- user_id: string | null;
154
- scopes_json: string;
155
- revoked_at: string | null;
156
- };
157
-
158
- export function siteCredentialResolver(db: D1Database): ConsumerCredentialResolver {
159
- return async (request) => {
160
- const apiKey = request.headers.get("x-api-key");
161
- const authorization = request.headers.get("authorization");
162
-
163
- let kind: CredentialRow["kind"];
164
- let raw: string;
165
- if (apiKey !== null) {
166
- kind = "api-key";
167
- raw = apiKey;
168
- } else if (authorization?.startsWith("Bearer site_pat_")) {
169
- kind = "personal-token";
170
- raw = authorization.slice("Bearer ".length);
171
- } else {
172
- // Lets configured OAuth bearer or cookie-session auth try next.
173
- return { kind: "not-handled" };
174
- }
175
-
176
- const digest = await sha256(raw);
177
- const row = await db
178
- .prepare(
179
- "SELECT id, kind, user_id, scopes_json, revoked_at " +
180
- "FROM site_credentials WHERE token_sha256 = ? AND kind = ? LIMIT 1",
181
- )
182
- .bind(digest, kind)
183
- .first<CredentialRow>();
184
-
185
- if (!row || row.revoked_at !== null) return { kind: "invalid" };
186
- const scopes = parseScopes(row.scopes_json);
187
- if (!scopes) return { kind: "invalid" };
188
-
189
- return {
190
- kind: "verified",
191
- credential: {
192
- credential: row.kind,
193
- credentialId: row.id, // opaque row id, never the raw key/token
194
- userId: row.user_id,
195
- scopes,
196
- },
197
- };
198
- };
199
- }
200
-
201
- async function sha256(value: string): Promise<string> {
202
- const bytes = await crypto.subtle.digest(
203
- "SHA-256",
204
- new TextEncoder().encode(value),
205
- );
206
- return [...new Uint8Array(bytes)]
207
- .map((byte) => byte.toString(16).padStart(2, "0"))
208
- .join("");
209
- }
210
-
211
- function parseScopes(json: string): string[] | null {
212
- try {
213
- const value: unknown = JSON.parse(json);
214
- return Array.isArray(value) && value.every((scope) => typeof scope === "string")
215
- ? value
216
- : null;
217
- } catch {
218
- return null;
219
- }
220
- }
221
- ```
222
-
223
- Wire it alongside the existing Auth facade. `jwtBearer` is optional and
224
- enables JWT bearer verification for manifest REST routes:
225
-
226
- ```ts
227
- import {
228
- createMantleWorker,
229
- } from "@aotter/mantle/cloudflare";
230
-
231
- export default createMantleWorker({
232
- plan,
233
- handlers,
234
- extend: ({ env }) => ({
235
- credentialResolver: siteCredentialResolver(env.DB),
236
- jwtBearer: {
237
- audience: "https://api.example.com",
238
- // Optional server-wide floor. Manifest scopes still run per target.
239
- scopes: ["api"],
240
- },
241
- }),
242
- });
243
- ```
244
-
245
- The facade mounts both MCP surfaces behind the same Better Auth 1.7 resource.
246
- Low-level composition must pass that canonical resource to each
247
- `createMcpApiHandler` explicitly.
248
-
249
- Resolution precedence is site resolver, configured OAuth bearer, then cookie
250
- session. A recognized invalid credential never falls back to a valid cookie.
251
- For each verified user, the adapter re-reads the current staff role rather than
252
- trusting a token or consent-time snapshot.
253
-
254
- ## 1. Anonymous public API
255
-
256
- Omit `requires` when the operation is intentionally anonymous:
257
-
258
- ```yaml
259
- apiVersion: cms.mantle.aotter.net/v1
260
- kind: Procedure
261
- metadata: { name: public-status }
262
- spec:
263
- input: { type: object }
264
- output:
265
- type: object
266
- required: [status]
267
- properties:
268
- status: { type: string }
269
- handler: { kind: ref, ref: publicStatus }
270
- ---
271
- apiVersion: cms.mantle.aotter.net/v1
272
- kind: Trigger
273
- metadata: { name: public-status-http }
274
- spec:
275
- source: { kind: http, method: POST, path: /api/status }
276
- target: { procedure: public-status }
277
- ```
278
-
279
- ```ts
280
- const handlers = {
281
- publicStatus: async () => ({ status: "ok" }),
282
- };
283
- ```
284
-
285
- ```bash
286
- curl -i -X POST https://site.example.com/api/status \
287
- -H 'content-type: application/json' \
288
- -d '{}'
289
- # HTTP/2 200
290
- ```
291
-
292
- OpenAPI emits no `security` requirement and no auth responses for this
293
- operation. No MCP tool is created unless a separate MCP Trigger targets the
294
- Procedure.
295
-
296
- ## 2. Public API requiring an API key
297
-
298
- The API remains publicly reachable, but its target requires a verified
299
- credential and the site-defined `catalog:read` scope:
300
-
301
- ```yaml
302
- apiVersion: cms.mantle.aotter.net/v1
303
- kind: Procedure
304
- metadata: { name: read-catalog }
305
- spec:
306
- requires:
307
- auth:
308
- all:
309
- - ctx.auth
310
- - { "ctx.auth.scope": "catalog:read" }
311
- input: { type: object }
312
- output: { type: object }
313
- handler: { kind: ref, ref: readCatalog }
314
- ---
315
- apiVersion: cms.mantle.aotter.net/v1
316
- kind: Trigger
317
- metadata: { name: read-catalog-http }
318
- spec:
319
- source: { kind: http, method: POST, path: /api/catalog/read }
320
- target: { procedure: read-catalog }
321
- ```
322
-
323
- ```ts
324
- import type { HandlerContext } from "@aotter/mantle/runtime";
325
-
326
- const handlers = {
327
- readCatalog: async (_input: unknown, ctx: HandlerContext) => ({
328
- credentialId: ctx.auth!.credentialId,
329
- items: [],
330
- }),
331
- };
332
- ```
333
-
334
- ```bash
335
- curl -i -X POST https://site.example.com/api/catalog/read \
336
- -H 'content-type: application/json' \
337
- -H "x-api-key: $SITE_API_KEY" \
338
- -d '{}'
339
- # valid key with catalog:read -> 200
340
- # missing or recognized-invalid key -> 401
341
- # verified key without catalog:read -> 403
342
- ```
343
-
344
- `ctx.auth` intentionally means any verified credential; there is no
345
- credential-kind predicate. Configure and document only the credential sources
346
- the site intends to accept, or put a kind-specific business rule in a guard.
347
- With `security.apiKey` configured during OpenAPI emission, the operation
348
- advertises the real header and carries `x-mantle-required-scopes`.
349
-
350
- ## 3. API key plus a mutable paid/transaction guard
351
-
352
- Keep key verification in the resolver. Put current paid state in an ordinary,
353
- site-owned guard Procedure:
354
-
355
- ```yaml
356
- apiVersion: cms.mantle.aotter.net/v1
357
- kind: Procedure
358
- metadata: { name: require-active-api-access }
359
- spec:
360
- input: { type: object }
361
- output: { type: object }
362
- handler: { kind: ref, ref: requireActiveApiAccess }
363
- ---
364
- apiVersion: cms.mantle.aotter.net/v1
365
- kind: Procedure
366
- metadata: { name: download-export }
367
- spec:
368
- requires:
369
- auth:
370
- all:
371
- - ctx.auth
372
- - { "ctx.auth.scope": "exports:read" }
373
- guard: { procedure: require-active-api-access }
374
- input:
375
- type: object
376
- required: [reportId]
377
- properties:
378
- reportId: { type: string }
379
- output: { type: object }
380
- handler: { kind: ref, ref: downloadExport }
381
- ---
382
- apiVersion: cms.mantle.aotter.net/v1
383
- kind: Trigger
384
- metadata: { name: download-export-http }
385
- spec:
386
- source: { kind: http, method: POST, path: /api/exports/download }
387
- target: { procedure: download-export }
388
- ```
389
-
390
- ```ts
391
- import {
392
- DiagnosticError,
393
- runtimeDiagnostic,
394
- } from "@aotter/mantle/spec";
395
- import type { HandlerContext } from "@aotter/mantle/runtime";
396
-
397
- const handlers = {
398
- requireActiveApiAccess: async (_input: unknown, ctx: HandlerContext) => {
399
- const credentialId = ctx.auth?.credentialId;
400
- const paid = credentialId
401
- ? await env.DB.prepare(
402
- "SELECT 1 FROM site_api_entitlements " +
403
- "WHERE credential_id = ? AND state = 'paid' LIMIT 1",
404
- )
405
- .bind(credentialId)
406
- .first()
407
- : null;
408
-
409
- if (!paid) {
410
- throw new DiagnosticError(
411
- runtimeDiagnostic({
412
- code: "ENTITLEMENT_REQUIRED",
413
- severity: "error",
414
- path: "site:api-entitlement",
415
- message: "Active paid API access is required.",
416
- }),
417
- );
418
- }
419
- return {};
420
- },
421
- downloadExport: async ({ reportId }: { reportId: string }) => ({ reportId }),
422
- };
423
- ```
424
-
425
- ```bash
426
- curl -i -X POST https://site.example.com/api/exports/download \
427
- -H 'content-type: application/json' \
428
- -H "x-api-key: $SITE_API_KEY" \
429
- -d '{"reportId":"report-1"}'
430
- # valid + entitled -> 200
431
- # invalid key -> 401
432
- # verified key missing exports:read -> 403
433
- # verified key whose current paid row is absent/revoked -> 402
434
- ```
435
-
436
- The guard receives the already validated target input and runs for every call.
437
- On `402`, the target handler is not invoked. OpenAPI reflects the guard as
438
- `x-mantle-guard-procedure` and includes a `402` response; Mantle does not infer
439
- or publish the site's billing model.
440
-
441
- ## 4. Personal token with user scope, shared by REST and MCP semantics
442
-
443
- This Procedure requires a user subject, a verified credential, a delegated
444
- scope, and current membership. Bind the same target to HTTP and public MCP:
445
-
446
- ```yaml
447
- apiVersion: cms.mantle.aotter.net/v1
448
- kind: Procedure
449
- metadata: { name: require-active-membership }
450
- spec:
451
- input: { type: object }
452
- output: { type: object }
453
- handler: { kind: ref, ref: requireActiveMembership }
454
- ---
455
- apiVersion: cms.mantle.aotter.net/v1
456
- kind: Procedure
457
- metadata: { name: read-account }
458
- spec:
459
- requires:
460
- auth:
461
- all:
462
- - ctx.user
463
- - ctx.auth
464
- - { "ctx.auth.scope": "accounts:read" }
465
- guard: { procedure: require-active-membership }
466
- input:
467
- type: object
468
- required: [accountId]
469
- properties:
470
- accountId: { type: string }
471
- output:
472
- type: object
473
- required: [accountId]
474
- properties:
475
- accountId: { type: string }
476
- handler: { kind: ref, ref: readAccount }
477
- ---
478
- apiVersion: cms.mantle.aotter.net/v1
479
- kind: Trigger
480
- metadata: { name: read-account-http }
481
- spec:
482
- source: { kind: http, method: POST, path: /api/accounts/read }
483
- target: { procedure: read-account }
484
- ---
485
- apiVersion: cms.mantle.aotter.net/v1
486
- kind: Trigger
487
- metadata: { name: read-account-mcp }
488
- spec:
489
- source: { kind: mcp, surface: public }
490
- target: { procedure: read-account }
491
- ```
492
-
493
- ```ts
494
- import {
495
- DiagnosticError,
496
- runtimeDiagnostic,
497
- } from "@aotter/mantle/spec";
498
- import type { HandlerContext } from "@aotter/mantle/runtime";
499
-
500
- const handlers = {
501
- requireActiveMembership: async (_input: unknown, ctx: HandlerContext) => {
502
- const active = await env.DB.prepare(
503
- "SELECT 1 FROM site_memberships " +
504
- "WHERE user_id = ? AND state = 'active' LIMIT 1",
505
- )
506
- .bind(ctx.user!.id)
507
- .first();
508
- if (!active) {
509
- throw new DiagnosticError(
510
- runtimeDiagnostic({
511
- code: "ENTITLEMENT_REQUIRED",
512
- severity: "error",
513
- path: `site:membership/${ctx.user!.id}`,
514
- message: "Active membership is required.",
515
- }),
516
- );
517
- }
518
- return {};
519
- },
520
- readAccount: async ({ accountId }: { accountId: string }) => ({ accountId }),
521
- };
522
- ```
523
-
524
- REST uses the site resolver's personal token:
525
-
526
- ```bash
527
- curl -i -X POST https://site.example.com/api/accounts/read \
528
- -H 'content-type: application/json' \
529
- -H "authorization: Bearer $SITE_PERSONAL_TOKEN" \
530
- -d '{"accountId":"acct-1"}'
531
- ```
532
-
533
- Standard remote MCP uses the MCP server's OAuth bearer, not the raw site PAT.
534
- After OAuth normalization, it reaches the same target and guard:
535
-
536
- ```bash
537
- curl -sS -X POST https://site.example.com/mcp \
538
- -H 'content-type: application/json' \
539
- -H "authorization: Bearer $MCP_OAUTH_ACCESS_TOKEN" \
540
- -d '{
541
- "jsonrpc":"2.0",
542
- "id":1,
543
- "method":"tools/call",
544
- "params":{
545
- "name":"read_account",
546
- "arguments":{"accountId":"acct-1"}
547
- }
548
- }'
549
- ```
550
-
551
- Expected behavior:
552
-
553
- | State | REST | MCP |
554
- | --- | --- | --- |
555
- | valid user credential, `accounts:read`, active membership | `200` target result | JSON-RPC `result` |
556
- | missing/invalid credential | `401` | OAuth layer rejects the request |
557
- | verified caller missing user or `accounts:read` | `403` | JSON-RPC error with `error.data.code = "AUTH_DENIED"` |
558
- | membership revoked while credential remains valid | `402` | JSON-RPC error with `error.data.code = "ENTITLEMENT_REQUIRED"` |
559
- | MCP bearer missing the resource-level `mcp` scope | n/a | HTTP `403` plus `WWW-Authenticate: ... insufficient_scope` |
560
-
561
- `tools/list` includes `read_account` only on the public surface selected by its
562
- MCP Trigger. The standard Tool schema remains standard: required scopes and
563
- guard metadata are described in text, while every `tools/call` re-evaluates
564
- the manifest predicates and guard. Staff Views are listed/callable only on the
565
- staff MCP surface; discovery is never the enforcement boundary.
566
-
567
- ## OAuth resource primitives
568
-
569
- When one Mantle site is an OAuth client of another, request a stable RFC 8707
570
- resource and use standard `offline_access` when refresh is needed:
571
-
572
- ```ts
573
- const clientAuth = createAuth({
574
- // database, baseURL, secret, other methods...
575
- methods: [{
576
- kind: "oauth",
577
- providerId: "mantle-platform",
578
- clientId: env.PLATFORM_CLIENT_ID,
579
- discoveryUrl: "https://platform.example.com/api/auth/.well-known/openid-configuration",
580
- scopes: ["openid", "offline_access", "accounts:read"],
581
- resource: "https://api.example.com",
582
- }],
583
- });
584
-
585
- const { accessToken, accessTokenExpiresAt, scopes } =
586
- await clientAuth.getProviderAccessToken(request, "mantle-platform");
587
- ```
588
-
589
- The server-side getter is bound to the current local session request and never
590
- returns a refresh token or account row. On the provider:
591
-
592
- ```ts
593
- const providerAuth = createAuth({
594
- // database, baseURL, secret, methods...
595
- oauthProvider: {
596
- loginPage: "/sign-in",
597
- consentPage: "/consent",
598
- scopes: ["openid", "offline_access", "accounts:read"],
599
- resources: ["https://api.example.com"],
600
- },
601
- });
602
-
603
- const verification = await providerAuth.verifyOAuthAccessToken(request, {
604
- audience: "https://api.example.com",
605
- scopes: ["accounts:read"],
606
- });
607
- ```
608
-
609
- The verifier accepts JWT access tokens only and checks the configured issuer,
610
- JWKS/signature, audience, time claims, required scopes, and—when passed the
611
- request—DPoP proof binding with database-backed replay protection. It returns
612
- only `userId`, `clientId`, `credentialId`, and scopes. Opaque tokens are
613
- rejected; there is no introspection fallback.
614
-
615
- ## OpenAPI reflection
616
-
617
- Emit only the schemes the deployed REST mount actually accepts:
618
-
619
- ```ts
620
- import { EmitOpenapiUseCase } from "@aotter/mantle/spec";
621
-
622
- const { document } = EmitOpenapiUseCase.run({
623
- manifests,
624
- title: "Site API",
625
- version: "1.0.0",
626
- security: {
627
- sessionCookie: false,
628
- oauthBearer: {
629
- openIdConnectUrl:
630
- "https://platform.example.com/api/auth/.well-known/openid-configuration",
631
- },
632
- apiKey: { in: "header", name: "X-API-Key" },
633
- personalToken: { bearerFormat: "PAT" },
634
- },
635
- });
636
- ```
637
-
638
- Anonymous operations have no security requirement. Protected operations use
639
- configured scheme alternatives, OAuth scopes derive from repeated
640
- `ctx.auth.scope` predicates, and guard-backed targets advertise `402`. Cookie
641
- sessions are represented as cookies, never mislabeled as bearer tokens.
642
-
643
- ## Runnable contract check
644
-
645
- The integration fixture uses mutable, consumer-owned credential and
646
- entitlement fakes. It proves this sequence for one Procedure over REST and MCP:
647
-
648
- ```text
649
- grant -> REST succeeds -> MCP succeeds
650
- revoke entitlement while credential remains valid
651
- -> next REST call is 402 -> next MCP call is ENTITLEMENT_REQUIRED
652
- ```
653
-
654
- Run the guide/contract and normalization checks from the Mantle repository:
655
-
656
- ```bash
657
- pnpm --filter @aotter/mantle-cloudflare exec vitest run \
658
- test/authorization-integration.test.ts \
659
- test/resolve-caller.test.ts \
660
- test/mount-http-trigger-auth.test.ts
661
- ```
662
-
663
- `authorization-integration.test.ts` also asserts that this shipped guide still
664
- contains all four scenarios and the exact public API names used by the fixture.
665
- The package typecheck catches changes to those APIs; the integration test
666
- catches changes to REST/MCP enforcement and mutable guard behavior.
3
+ See [Authorization](handbook/reference/authorization.md),
4
+ [Guarded API access](handbook/examples/guarded-api.md), and
5
+ [OAuth resource primitives](handbook/cloudflare/authentication.md#oauth-resource-primitives).
@@ -7,8 +7,8 @@ basic login. The split is:
7
7
  run its own auth.
8
8
  - **Mantle's conventional Cloudflare adapter** runs the generated site's
9
9
  selected self-hosted or Mantle Platform hosted client configuration.
10
- - **Mantle starters** declare the mode and provider placeholders that landing
11
- or the site owner completes.
10
+ - **The application owner/host** declares the explicit auth mode and provider
11
+ configuration; legacy Landing/Starters remain on alpha.17.
12
12
  - **Mantle Platform** can sell hosted identity, provider setup, email,
13
13
  and billing convenience for site owners who do not want to operate
14
14
  those pieces.
@@ -120,7 +120,7 @@ GitHub OAuth token is still Landing-owned unless a separate token
120
120
  handoff design is introduced.
121
121
 
122
122
  The conventional hosted-auth client wiring belongs in Core's Cloudflare
123
- adapter. Starters declare its environment bindings; landing supplies an
123
+ adapter. Applications declare its environment bindings; a provisioning host supplies an
124
124
  allocated client. A site can still replace Auth construction through
125
125
  `createMantleWorker({ auth })` when it needs a different curated identity
126
126
  design. Core continues to own the normalized manifest/runtime credential
@@ -138,7 +138,7 @@ handler that checks current business state.
138
138
  Mantle Platform may be the identity or OAuth token authority for a hosted
139
139
  flow. That does not make token claims the generated site's live membership or
140
140
  entitlement authority. The target site's guard reads its authoritative state
141
- on every call. See [API and MCP authorization](api-mcp-authorization.md) for
141
+ on every call. See [API and MCP authorization](handbook/examples/guarded-api.md) for
142
142
  the exact public API and four consumer examples.
143
143
 
144
144
  ## SDK Surface Rule