@aotter/mantle 0.1.3-alpha.3 → 0.1.3-alpha.5
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 +1 -1
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +37 -2
- package/docs/handbook/cloudflare/authentication.md +45 -0
- package/docs/handbook/cloudflare/bindings.md +58 -0
- package/docs/handbook/cloudflare/conventional-worker.md +1 -1
- package/docs/handbook/reference/schema.md +1 -1
- package/docs/spec-only-host-adoption.md +1 -1
- package/package.json +17 -17
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx skills add aotter/mantle --skill install
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
To depend on this package in an existing project, pin the exact version
|
|
19
|
-
from `package.json` (currently `0.1.3-alpha.
|
|
19
|
+
from `package.json` (currently `0.1.3-alpha.5`):
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
npm install @aotter/mantle
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
5
|
Accepted. Amended 2026-05-14, 2026-05-15, 2026-06-30, 2026-07-15,
|
|
6
|
-
2026-08-03, 2026-08-22, 2026-09-18, and 2026-09-
|
|
6
|
+
2026-08-03, 2026-08-22, 2026-09-18, 2026-09-21, and 2026-09-22.
|
|
7
7
|
|
|
8
8
|
## Date
|
|
9
9
|
|
|
10
|
-
2026-05-09 (last amended 2026-09-
|
|
10
|
+
2026-05-09 (last amended 2026-09-22)
|
|
11
11
|
|
|
12
12
|
> **Current authority:** the original decision below records the rejected
|
|
13
13
|
> Better-Auth-for-MCP design. The 2026-08-22 Better Auth 1.7 amendment is
|
|
@@ -168,6 +168,8 @@ The internal consumers of `Auth` (post-#193: `mountServerEndpoints`, `createMcpA
|
|
|
168
168
|
|
|
169
169
|
**Anti-pattern to refuse in review: Better Auth-field pass-through.**
|
|
170
170
|
|
|
171
|
+
> **Narrowed by the 2026-09-22 amendment** for `OAuthProviderConfig.extensions`, Better Auth's declared OAuth-provider composition point. See § "2026-09-22 amendment" below.
|
|
172
|
+
|
|
171
173
|
If a future PR's only effect is to rename a Better Auth field into our `CreateAuthConfig` and forward it verbatim, refuse it. **Picking a different literal default for an existing Better Auth field does NOT, by itself, justify a new field on `CreateAuthConfig` — that's the same pass-through dressed up.** The SDK adds load-bearing surface area only when the new field exists for at least one of these concrete reasons:
|
|
172
174
|
|
|
173
175
|
- **Workers-aware behavior** that Better Auth doesn't supply (e.g. `rateLimit` flipped on by default when an email-shaped method registers — Better Auth's per-route limits gate on `process.env.NODE_ENV === "production"` which is unset on Workers; `advanced.backgroundTasks.handler` wired to fire-and-forget — closes the timing-oracle on OTP send).
|
|
@@ -862,3 +864,36 @@ in a later refactor:
|
|
|
862
864
|
`415` to anything else, so an HTML form (whose enctypes cannot produce that
|
|
863
865
|
header) can never drive a cookie session on `/mcp`; the same-origin guard
|
|
864
866
|
remains the second layer.
|
|
867
|
+
|
|
868
|
+
## 2026-09-22 amendment — OAuth provider extensions are an adopter seam
|
|
869
|
+
|
|
870
|
+
Issue #1016 supersedes the 2026-05-14 "Better Auth-field pass-through"
|
|
871
|
+
prohibition for one field: `OAuthProviderConfig.extensions`, forwarded to
|
|
872
|
+
`@better-auth/oauth-provider`'s `extensions` option. It follows the 2026-09-18
|
|
873
|
+
line that already exposes official Better Auth options and plugin instances
|
|
874
|
+
rather than copying them.
|
|
875
|
+
|
|
876
|
+
Why this is not the refused pattern: an `OAuthProviderExtension` is Better
|
|
877
|
+
Auth's declared composition point for token grants, client-authentication
|
|
878
|
+
strategies, discovery metadata and additional claims. MCP Enterprise-Managed
|
|
879
|
+
Authorization needs a `jwt-bearer` (ID-JAG) grant whose issuer and JWKS belong
|
|
880
|
+
to one enterprise IdP; that is adopter configuration, not an SDK default, and
|
|
881
|
+
Core has no business knowing the issuer. Curating a first-class
|
|
882
|
+
`enterpriseIdp` field would put an IdP contract inside Core and still forward
|
|
883
|
+
it verbatim underneath. The reference implementation lives outside Core
|
|
884
|
+
(`@aotterclam/id-jag`).
|
|
885
|
+
|
|
886
|
+
Boundaries that stay:
|
|
887
|
+
|
|
888
|
+
- Core's own claims extension (`mantle_consent_id`) is always first on the
|
|
889
|
+
`mcpResource` branch; adopter extensions append after it and cannot replace
|
|
890
|
+
it. Better Auth's contract makes claims contributors additive, so an
|
|
891
|
+
extension cannot overwrite identity or AS-owned claims either way.
|
|
892
|
+
- `createMantleAuth` still owns login and consent pages, CIMD/DCR policy,
|
|
893
|
+
`mcpResource`, scopes and resources. `extensions` adds grants and strategies
|
|
894
|
+
beside those; it is not a `Partial<OAuthOptions>` merge and cannot change
|
|
895
|
+
them.
|
|
896
|
+
- Tokens an extension issues go through the provider's shared token path, so
|
|
897
|
+
`verifyOAuthAccessToken`, DPoP binding and the MCP challenge are unchanged.
|
|
898
|
+
- No other Better Auth field gains a passthrough by this amendment. The
|
|
899
|
+
2026-05-14 test still applies to the next proposal.
|
|
@@ -262,6 +262,51 @@ request—DPoP proof binding with database-backed replay protection. It returns
|
|
|
262
262
|
only `userId`, `clientId`, `credentialId`, and scopes. Opaque tokens are
|
|
263
263
|
rejected; there is no introspection fallback.
|
|
264
264
|
|
|
265
|
+
## Enterprise-Managed Authorization
|
|
266
|
+
|
|
267
|
+
MCP's [Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization)
|
|
268
|
+
extension lets an enterprise IdP decide which employees may reach an MCP
|
|
269
|
+
server. The MCP client exchanges the user's IdP login for an ID-JAG (identity
|
|
270
|
+
assertion authorization grant) and presents it to the server's token endpoint
|
|
271
|
+
as `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`; no consent page
|
|
272
|
+
is shown. Everything after that is an ordinary access token.
|
|
273
|
+
|
|
274
|
+
Core does not know any issuer or JWKS. The token grant is a
|
|
275
|
+
`@better-auth/oauth-provider` extension supplied by the adopter through
|
|
276
|
+
`oauthProvider.extensions`, appended after Core's own claims extension:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { identityAssertionAuthorizationGrant } from "@aotterclam/id-jag";
|
|
280
|
+
|
|
281
|
+
const auth = createAuth({
|
|
282
|
+
// database, baseURL, secret, methods...
|
|
283
|
+
oauthProvider: {
|
|
284
|
+
loginPage: "/admin/sign-in",
|
|
285
|
+
consentPage: "/oauth/consent",
|
|
286
|
+
scopes: ["mcp", "offline_access"],
|
|
287
|
+
mcpResource: env.PUBLIC_ORIGIN + "/mcp",
|
|
288
|
+
extensions: [
|
|
289
|
+
identityAssertionAuthorizationGrant({
|
|
290
|
+
issuer: env.ENTERPRISE_IDP_ISSUER,
|
|
291
|
+
jwksUrl: env.ENTERPRISE_IDP_JWKS_URL,
|
|
292
|
+
authorizationServer: env.PUBLIC_ORIGIN,
|
|
293
|
+
resource: env.PUBLIC_ORIGIN + "/mcp",
|
|
294
|
+
scopes: ["mcp"],
|
|
295
|
+
fetchJwks: (input, init) => fetch(input, { ...init, redirect: "manual" }),
|
|
296
|
+
}),
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
The extension validates the assertion's signature, issuer, audience and
|
|
303
|
+
lifetime, maps its subject to a user, and issues tokens through the provider's
|
|
304
|
+
shared token path, so `verifyOAuthAccessToken`, DPoP and the MCP challenge
|
|
305
|
+
behave exactly as for interactive grants. `@aotterclam/id-jag` is a reference
|
|
306
|
+
implementation, not a Core dependency; any `OAuthProviderExtension` works.
|
|
307
|
+
Extensions may also add client-authentication strategies, discovery metadata
|
|
308
|
+
and additional claims. The same passthrough applies without `mcpResource`.
|
|
309
|
+
|
|
265
310
|
## Source
|
|
266
311
|
- [`packages/adapters/cloudflare/README.md`](../../../packages/adapters/cloudflare/README.md)
|
|
267
312
|
- [`packages/adapters/cloudflare/src/auth/conventionalAuth.ts`](../../../packages/adapters/cloudflare/src/auth/conventionalAuth.ts)
|
|
@@ -40,6 +40,7 @@ export interface Env extends MantleCloudflareEnv {
|
|
|
40
40
|
readonly R2_ACCESS_KEY_ID?: string;
|
|
41
41
|
readonly R2_SECRET_ACCESS_KEY?: string;
|
|
42
42
|
readonly MEDIA_PUBLIC_URL_BASE?: string;
|
|
43
|
+
readonly AUDIT?: AnalyticsEngineDataset; // optional MCP tools/call audit trail
|
|
43
44
|
readonly MANTLE_INTERNAL_QUEUE?: Queue<DeferredHookEnvelope>; // optional deferred hooks
|
|
44
45
|
readonly ORDER_EXPIRY_QUEUE?: Queue<{ type: "expire-order"; orderToken: string }>; // application queue
|
|
45
46
|
readonly INVENTORY_COORDINATOR?: DurableObjectNamespace; // application-owned
|
|
@@ -101,6 +102,63 @@ A deployment-owned namespace that caches the caller-independent MCP catalog proj
|
|
|
101
102
|
|
|
102
103
|
Staff media uploads through Staff MCP presigned PUT. The binding alone cannot sign URLs; you also need `R2_ACCOUNT_ID`, `MEDIA_PUBLIC_URL_BASE` and the two S3 credential secrets. See [Media uploads with R2](./media-r2.md).
|
|
103
104
|
|
|
105
|
+
## Analytics Engine: `AUDIT` (optional)
|
|
106
|
+
|
|
107
|
+
```jsonc
|
|
108
|
+
"analytics_engine_datasets": [{ "binding": "AUDIT", "dataset": "mantle_mcp_audit" }]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
An MCP `tools/call` audit trail: who called which tool on which surface, with
|
|
112
|
+
what outcome and how long it took. The runtime's `McpJsonRpcDispatcher` is the
|
|
113
|
+
single write point; it records nothing unless the composition root passes an
|
|
114
|
+
`AuditSink`, and the write happens through `waitUntil`, off the response path.
|
|
115
|
+
Request and response payloads are never recorded.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { analyticsEngineAuditSink, createMantleWorker } from "@aotter/mantle/cloudflare";
|
|
119
|
+
|
|
120
|
+
export default createMantleWorker<Env>({
|
|
121
|
+
plan,
|
|
122
|
+
audit: (env) => env.AUDIT
|
|
123
|
+
? analyticsEngineAuditSink(env.AUDIT, { index: env.PUBLIC_ORIGIN })
|
|
124
|
+
: undefined,
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
One dataset may hold many deployments: `index` is the deployment's public
|
|
129
|
+
origin, the only column Analytics Engine filters cheaply. Columns, in order:
|
|
130
|
+
|
|
131
|
+
| Column | Field |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `index1` | the configured `index` |
|
|
134
|
+
| `blob1` … `blob7` | `surface`, `callerId`, `clientId`, `credential`, `tool`, `outcome`, `operationId` |
|
|
135
|
+
| `double1`, `double2` | `at` (epoch ms), `durationMs` |
|
|
136
|
+
|
|
137
|
+
`outcome` is `ok`, a runtime Diagnostic code such as `UNAUTHENTICATED` or
|
|
138
|
+
`AUTH_DENIED`, `UNKNOWN_TOOL`, `INVALID_PARAMS`, `INTERNAL`, or a transport gate denial
|
|
139
|
+
(`INVALID_TOKEN`, `INSUFFICIENT_SCOPE`, `INSUFFICIENT_ROLE`, `CROSS_ORIGIN`): a
|
|
140
|
+
`tools/call` refused before it reaches the dispatcher is recorded too, with
|
|
141
|
+
whatever identity the gate established. Its body goes through the same 1 MiB
|
|
142
|
+
bounded reader as an admitted call; a denied request whose body is oversized
|
|
143
|
+
or not JSON is still recorded, with `tool` set to `(unreadable)`, so padding a
|
|
144
|
+
body cannot hide a credential probe. `operationId` is the call's `operationId`
|
|
145
|
+
argument when present, else empty; it correlates retries of one mutation and
|
|
146
|
+
is read the same way on admitted and denied calls. Read it with the
|
|
147
|
+
[SQL API](https://developers.cloudflare.com/analytics/analytics-engine/sql-api/):
|
|
148
|
+
|
|
149
|
+
```sql
|
|
150
|
+
SELECT timestamp, blob2 AS caller, blob5 AS tool, blob6 AS outcome, double2 AS ms
|
|
151
|
+
FROM mantle_mcp_audit
|
|
152
|
+
WHERE index1 IN ('https://site-a.example', 'https://site-b.example')
|
|
153
|
+
AND timestamp > NOW() - INTERVAL '7' DAY
|
|
154
|
+
ORDER BY timestamp DESC
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Analytics Engine keeps data for a bounded window and samples under very high
|
|
158
|
+
write rates. Longer retention or a tamper-evident trail is an export job on top
|
|
159
|
+
of this dataset. Self-hosted runtimes may implement `AuditSink` over any store;
|
|
160
|
+
the interface is one `record(event)` method.
|
|
161
|
+
|
|
104
162
|
## Queues
|
|
105
163
|
|
|
106
164
|
```jsonc
|
|
@@ -51,7 +51,7 @@ Once per isolate, `createMantleWorker` assembles and memoizes:
|
|
|
51
51
|
- Conventional bindings: `DB` becomes the D1 driver, `ASSETS` serves the Admin bundle, `MANTLE_KV` (when bound) becomes the MCP catalog projection. See [Bindings](./bindings.md).
|
|
52
52
|
- Conventional Auth chosen by `MANTLE_AUTH_MODE`, or your `auth` factory. See [Authentication](./authentication.md).
|
|
53
53
|
- Runtime endpoints: manifest HTTP Triggers, `GET /api/views` and `GET /api/views/<name>` for public Views.
|
|
54
|
-
- Admin at `/admin` when Admin assets are present, OAuth consent and discovery, and MCP at `/mcp` and `/mcp/staff`.
|
|
54
|
+
- Admin at `/admin` when Admin assets are present, OAuth consent and discovery, and MCP at `/mcp` and `/mcp/staff`. The MCP transport is Streamable HTTP, POST-only: no session header, no server-initiated stream (`GET` answers `405`), and `tools/call` answers `401` with an OAuth challenge so a client can authenticate mid-session and retry. Verified against the official TypeScript client SDK `@modelcontextprotocol/client` 2.0.0 in the adapter's conformance test; that pin is the gate when the SDK is upgraded.
|
|
55
55
|
- A `/favicon.ico` route derived from `siteDefaults.icons`. This is a convention, not a reserved path; an existing host route wins.
|
|
56
56
|
- The final cache policy on every response, and best-effort purge of the deployment-scoped public tag after publishing-content and site-setting writes.
|
|
57
57
|
- A redacted error boundary: an unexpected failure returns `500` with `{ "ok": false, "error": "internal_error" }` and `private, no-store`.
|
|
@@ -22,7 +22,7 @@ A Schema declares one collection: the JSON Schema for each entry's `data`, its i
|
|
|
22
22
|
|
|
23
23
|
### Reserved entry columns
|
|
24
24
|
|
|
25
|
-
Every entry carries `id`, `status`, `version`, `createdAt`, `updatedAt` and `authorId` as native columns outside `data`. They are valid in View `fields`, `filter`, `orderBy` and `uiSchema.list`, and `indexes` may include them (`uniqueIndexes` may not). A data property may not reuse one of these names: validate fails closed with `INVALID_MANIFEST_ENVELOPE` at `/spec/schema/properties/<name>`, because SQLite-family and IndexedDB storage would otherwise resolve the name differently and an index declared today could change meaning when a same-named property is added later. Use a domain name instead (`submittedAt`, `orderStatus`, `submittedBy`); the native column is still there and still readable (ADR-0025). `locale` is a reserved data field: only a localized Schema may declare it, and the runtime requires it on writes to a localized Schema. SQL Views project the native column. Do not declare `expectedVersion` under `spec.schema.properties` — that name is the reserved Procedure OCC token; validate fails closed with `INVALID_MANIFEST_ENVELOPE` (ADR-0022). New reserved Procedure input names need an ADR.
|
|
25
|
+
Every entry carries `id`, `status`, `version`, `createdAt`, `updatedAt` and `authorId` as native columns outside `data`. They are valid in View `fields`, `filter`, `orderBy` and `uiSchema.list.columns`, and `indexes` may include them (`uniqueIndexes` may not). `uiSchema.list.primaryField` is the entry title and must be a declared data property. A data property may not reuse one of these names: validate fails closed with `INVALID_MANIFEST_ENVELOPE` at `/spec/schema/properties/<name>`, because SQLite-family and IndexedDB storage would otherwise resolve the name differently and an index declared today could change meaning when a same-named property is added later. Use a domain name instead (`submittedAt`, `orderStatus`, `submittedBy`); the native column is still there and still readable (ADR-0025). `locale` is a reserved data field: only a localized Schema may declare it, and the runtime requires it on writes to a localized Schema. SQL Views project the native column. Do not declare `expectedVersion` under `spec.schema.properties` — that name is the reserved Procedure OCC token; validate fails closed with `INVALID_MANIFEST_ENVELOPE` (ADR-0022). New reserved Procedure input names need an ADR.
|
|
26
26
|
|
|
27
27
|
## Example
|
|
28
28
|
|
|
@@ -6,7 +6,7 @@ Runtime. This Spec-only path is allowed by
|
|
|
6
6
|
[ADR-0019](adr/0019-sealed-manifest-runtime-pipeline.md), not a new adapter,
|
|
7
7
|
manifest grammar, or fork of Core.
|
|
8
8
|
|
|
9
|
-
This recipe targets `0.1.3-alpha.
|
|
9
|
+
This recipe targets `0.1.3-alpha.5`. Pin the package, record the tested version, and
|
|
10
10
|
rerun compatibility checks when upgrading.
|
|
11
11
|
|
|
12
12
|
## What stays with the host
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aotter/mantle",
|
|
3
|
-
"version": "0.1.3-alpha.
|
|
3
|
+
"version": "0.1.3-alpha.5",
|
|
4
4
|
"description": "Embeddable Mantle Core umbrella with Spec and Runtime; Web, Admin, Auth, Bun, Vercel, Cloudflare, and Admin UI are optional peer packages.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://mantle.tools/",
|
|
@@ -83,8 +83,8 @@
|
|
|
83
83
|
"README.md"
|
|
84
84
|
],
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@aotter/mantle-runtime": "0.1.3-alpha.
|
|
87
|
-
"@aotter/mantle-spec": "0.1.3-alpha.
|
|
86
|
+
"@aotter/mantle-runtime": "0.1.3-alpha.5",
|
|
87
|
+
"@aotter/mantle-spec": "0.1.3-alpha.5"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
90
|
"aws4fetch": "^1.0.20",
|
|
@@ -92,13 +92,13 @@
|
|
|
92
92
|
"hono": "^4.12.0",
|
|
93
93
|
"@libsql/client": "^0.17.4",
|
|
94
94
|
"zod": "^4.5.0",
|
|
95
|
-
"@aotter/mantle-admin": "0.1.3-alpha.
|
|
96
|
-
"@aotter/mantle-
|
|
97
|
-
"@aotter/mantle-
|
|
98
|
-
"@aotter/mantle-
|
|
99
|
-
"@aotter/mantle-
|
|
100
|
-
"@aotter/mantle-
|
|
101
|
-
"@aotter/mantle-
|
|
95
|
+
"@aotter/mantle-admin": "0.1.3-alpha.5",
|
|
96
|
+
"@aotter/mantle-auth": "0.1.3-alpha.5",
|
|
97
|
+
"@aotter/mantle-bun": "0.1.3-alpha.5",
|
|
98
|
+
"@aotter/mantle-admin-ui": "0.1.3-alpha.5",
|
|
99
|
+
"@aotter/mantle-vercel": "0.1.3-alpha.5",
|
|
100
|
+
"@aotter/mantle-cloudflare": "0.1.3-alpha.5",
|
|
101
|
+
"@aotter/mantle-web": "0.1.3-alpha.5"
|
|
102
102
|
},
|
|
103
103
|
"peerDependenciesMeta": {
|
|
104
104
|
"@aotter/mantle-admin": {
|
|
@@ -144,13 +144,13 @@
|
|
|
144
144
|
"typescript": "^6.0.3",
|
|
145
145
|
"vitest": "^4.1.11",
|
|
146
146
|
"zod": "^4.5.4",
|
|
147
|
-
"@aotter/mantle-admin": "0.1.3-alpha.
|
|
148
|
-
"@aotter/mantle-auth": "0.1.3-alpha.
|
|
149
|
-
"@aotter/mantle-
|
|
150
|
-
"@aotter/mantle-
|
|
151
|
-
"@aotter/mantle-cloudflare": "0.1.3-alpha.
|
|
152
|
-
"@aotter/mantle-
|
|
153
|
-
"@aotter/mantle-
|
|
147
|
+
"@aotter/mantle-admin-ui": "0.1.3-alpha.5",
|
|
148
|
+
"@aotter/mantle-auth": "0.1.3-alpha.5",
|
|
149
|
+
"@aotter/mantle-bun": "0.1.3-alpha.5",
|
|
150
|
+
"@aotter/mantle-admin": "0.1.3-alpha.5",
|
|
151
|
+
"@aotter/mantle-cloudflare": "0.1.3-alpha.5",
|
|
152
|
+
"@aotter/mantle-web": "0.1.3-alpha.5",
|
|
153
|
+
"@aotter/mantle-vercel": "0.1.3-alpha.5"
|
|
154
154
|
},
|
|
155
155
|
"engines": {
|
|
156
156
|
"node": ">=22"
|