@aotter/mantle 0.1.3-alpha.4 → 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 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.4`):
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-21.
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-21)
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`.
@@ -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.4`. Pin the package, record the tested version, and
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.4",
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.4",
87
- "@aotter/mantle-spec": "0.1.3-alpha.4"
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-ui": "0.1.3-alpha.4",
96
- "@aotter/mantle-admin": "0.1.3-alpha.4",
97
- "@aotter/mantle-auth": "0.1.3-alpha.4",
98
- "@aotter/mantle-bun": "0.1.3-alpha.4",
99
- "@aotter/mantle-vercel": "0.1.3-alpha.4",
100
- "@aotter/mantle-web": "0.1.3-alpha.4",
101
- "@aotter/mantle-cloudflare": "0.1.3-alpha.4"
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.4",
148
- "@aotter/mantle-admin-ui": "0.1.3-alpha.4",
149
- "@aotter/mantle-auth": "0.1.3-alpha.4",
150
- "@aotter/mantle-bun": "0.1.3-alpha.4",
151
- "@aotter/mantle-vercel": "0.1.3-alpha.4",
152
- "@aotter/mantle-cloudflare": "0.1.3-alpha.4",
153
- "@aotter/mantle-web": "0.1.3-alpha.4"
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"