@aotter/mantle-cloudflare 0.1.0-alpha.9 → 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.
- package/README.md +87 -13
- package/dist/auth/conventionalAuth.d.ts +1 -0
- package/dist/auth/conventionalAuth.d.ts.map +1 -1
- package/dist/auth/conventionalAuth.js +2 -0
- package/dist/auth/conventionalAuth.js.map +1 -1
- package/dist/auth/createAuth.d.ts +24 -11
- package/dist/auth/createAuth.d.ts.map +1 -1
- package/dist/auth/createAuth.js +274 -23
- package/dist/auth/createAuth.js.map +1 -1
- package/dist/auth/emailTemplates.d.ts +7 -0
- package/dist/auth/emailTemplates.d.ts.map +1 -0
- package/dist/auth/emailTemplates.js +27 -0
- package/dist/auth/emailTemplates.js.map +1 -0
- package/dist/bindings/KvSiteConfigRepository.d.ts +44 -0
- package/dist/bindings/KvSiteConfigRepository.d.ts.map +1 -0
- package/dist/bindings/KvSiteConfigRepository.js +286 -0
- package/dist/bindings/KvSiteConfigRepository.js.map +1 -0
- package/dist/bindings/R2MediaStorage.d.ts +2 -3
- package/dist/bindings/R2MediaStorage.d.ts.map +1 -1
- package/dist/bindings/R2MediaStorage.js +59 -42
- package/dist/bindings/R2MediaStorage.js.map +1 -1
- package/dist/bindings/WorkersQueueHookDispatcher.d.ts.map +1 -1
- package/dist/bindings/WorkersQueueHookDispatcher.js +10 -8
- package/dist/bindings/WorkersQueueHookDispatcher.js.map +1 -1
- package/dist/bindings/conventionalBindings.d.ts +3 -1
- package/dist/bindings/conventionalBindings.d.ts.map +1 -1
- package/dist/bindings/conventionalBindings.js +4 -1
- package/dist/bindings/conventionalBindings.js.map +1 -1
- package/dist/bindings/index.d.ts +1 -0
- package/dist/bindings/index.d.ts.map +1 -1
- package/dist/bindings/index.js +1 -0
- package/dist/bindings/index.js.map +1 -1
- package/dist/mount/bootRuntimeOnce.d.ts +4 -0
- package/dist/mount/bootRuntimeOnce.d.ts.map +1 -1
- package/dist/mount/bootRuntimeOnce.js +53 -27
- package/dist/mount/bootRuntimeOnce.js.map +1 -1
- package/dist/mount/cmsConfig.d.ts +12 -1
- package/dist/mount/cmsConfig.d.ts.map +1 -1
- package/dist/mount/mountMcp.d.ts.map +1 -1
- package/dist/mount/mountMcp.js +76 -92
- package/dist/mount/mountMcp.js.map +1 -1
- package/dist/mount/mountPublicRoutes.d.ts +1 -0
- package/dist/mount/mountPublicRoutes.d.ts.map +1 -1
- package/dist/mount/mountPublicRoutes.js +71 -58
- package/dist/mount/mountPublicRoutes.js.map +1 -1
- package/dist/mount/mountRuntimeEndpoints.d.ts.map +1 -1
- package/dist/mount/mountRuntimeEndpoints.js +20 -3
- package/dist/mount/mountRuntimeEndpoints.js.map +1 -1
- package/dist/mount/resolveCaller.d.ts +1 -1
- package/dist/mount/resolveCaller.d.ts.map +1 -1
- package/dist/mount/resolveCaller.js +5 -3
- package/dist/mount/resolveCaller.js.map +1 -1
- package/dist/oauth/cachePolicy.d.ts +3 -1
- package/dist/oauth/cachePolicy.d.ts.map +1 -1
- package/dist/oauth/cachePolicy.js +10 -2
- package/dist/oauth/cachePolicy.js.map +1 -1
- package/dist/oauth/index.d.ts +2 -2
- package/dist/oauth/index.d.ts.map +1 -1
- package/dist/oauth/index.js +2 -2
- package/dist/oauth/index.js.map +1 -1
- package/dist/requestDiagnostics.d.ts +53 -0
- package/dist/requestDiagnostics.d.ts.map +1 -0
- package/dist/requestDiagnostics.js +32 -0
- package/dist/requestDiagnostics.js.map +1 -0
- package/dist/testing.d.ts +19 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +263 -0
- package/dist/testing.js.map +1 -0
- package/dist/worker/createMantleWorker.d.ts +5 -0
- package/dist/worker/createMantleWorker.d.ts.map +1 -1
- package/dist/worker/createMantleWorker.js +58 -19
- package/dist/worker/createMantleWorker.js.map +1 -1
- package/package.json +24 -17
- package/dist/auth/rejectCrossOriginMutation.d.ts +0 -2
- package/dist/auth/rejectCrossOriginMutation.d.ts.map +0 -1
- package/dist/auth/rejectCrossOriginMutation.js +0 -15
- package/dist/auth/rejectCrossOriginMutation.js.map +0 -1
- package/dist/oauth/consentHtml.d.ts +0 -15
- package/dist/oauth/consentHtml.d.ts.map +0 -1
- package/dist/oauth/consentHtml.js +0 -91
- package/dist/oauth/consentHtml.js.map +0 -1
- package/dist/oauth/mountOAuth.d.ts +0 -9
- package/dist/oauth/mountOAuth.d.ts.map +0 -1
- package/dist/oauth/mountOAuth.js +0 -37
- package/dist/oauth/mountOAuth.js.map +0 -1
package/dist/auth/createAuth.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { betterAuth } from "better-auth";
|
|
3
|
+
import { getMigrations } from "better-auth/db/migration";
|
|
4
|
+
import { D1DatabaseDriver } from "../bindings/D1DatabaseDriver.js";
|
|
2
5
|
import { createDpopReplayStore, enforceDpopBinding, isDpopBindingError, parseAccessTokenAuthorization, verifyJwsAccessToken, } from "better-auth/oauth2";
|
|
3
6
|
import { admin, emailOTP, jwt, magicLink } from "better-auth/plugins";
|
|
4
7
|
import { createAccessControl } from "better-auth/plugins/access";
|
|
@@ -8,8 +11,22 @@ import { splitSetCookieHeader } from "better-auth/cookies";
|
|
|
8
11
|
import { oauthProvider } from "@better-auth/oauth-provider";
|
|
9
12
|
import { mcp } from "@better-auth/mcp";
|
|
10
13
|
import { cimd } from "@better-auth/cimd";
|
|
11
|
-
import { decodeMemberCursor, encodeMemberCursor } from "@aotter/mantle-admin";
|
|
14
|
+
import { decodeMemberCursor, encodeMemberCursor, } from "@aotter/mantle-admin";
|
|
15
|
+
import { signInCodeEmail, signInLinkEmail, staffInvitationEmail } from "./emailTemplates.js";
|
|
12
16
|
import { STAFF_ROLES } from "@aotter/mantle-spec";
|
|
17
|
+
// Better Auth 1.7.2 initializes its shared stores asynchronously. Seed them
|
|
18
|
+
// before any request can be canceled; the accessor-identity regression test
|
|
19
|
+
// pins this version-specific integration to the stores Better Auth uses.
|
|
20
|
+
const betterAuthGlobalKey = Symbol.for("better-auth:global");
|
|
21
|
+
const betterAuthGlobals = globalThis;
|
|
22
|
+
const betterAuthGlobal = betterAuthGlobals[betterAuthGlobalKey] ??= {
|
|
23
|
+
version: "",
|
|
24
|
+
epoch: 0,
|
|
25
|
+
context: {},
|
|
26
|
+
};
|
|
27
|
+
betterAuthGlobal.context.requestStateAsyncStorage ??= new AsyncLocalStorage();
|
|
28
|
+
betterAuthGlobal.context.endpointContextAsyncStorage ??= new AsyncLocalStorage();
|
|
29
|
+
betterAuthGlobal.context.adapterAsyncStorage ??= new AsyncLocalStorage();
|
|
13
30
|
export { decodeMemberCursor, encodeMemberCursor };
|
|
14
31
|
export { STAFF_ROLES };
|
|
15
32
|
/**
|
|
@@ -231,8 +248,7 @@ function buildMagicLinkPlugin(method) {
|
|
|
231
248
|
const locale = pickLocale(ctx?.request, fallback);
|
|
232
249
|
return method.sender.send({
|
|
233
250
|
to: data.email,
|
|
234
|
-
|
|
235
|
-
text: `Click to sign in: ${data.url}\nThe link expires shortly. If you didn't request this, ignore this email.`,
|
|
251
|
+
...signInLinkEmail(data.url),
|
|
236
252
|
locale,
|
|
237
253
|
category: "auth.magic-link.sign-in",
|
|
238
254
|
});
|
|
@@ -259,8 +275,7 @@ function buildEmailOTPPlugin(method) {
|
|
|
259
275
|
const locale = pickLocale(ctx?.request, fallback);
|
|
260
276
|
return method.sender.send({
|
|
261
277
|
to: data.email,
|
|
262
|
-
|
|
263
|
-
text: `Your one-time code is ${data.otp}. It expires shortly. If you didn't request this, ignore this email.`,
|
|
278
|
+
...signInCodeEmail(data.otp),
|
|
264
279
|
locale,
|
|
265
280
|
category: `auth.email-otp.${data.type}`,
|
|
266
281
|
});
|
|
@@ -398,6 +413,9 @@ export function buildOAuthProviderOptions(config) {
|
|
|
398
413
|
};
|
|
399
414
|
}
|
|
400
415
|
function buildAuth(config) {
|
|
416
|
+
if (config.hostOnlyCookies && (new URL(config.baseURL).protocol !== "https:" || config.crossSubDomainCookies?.enabled)) {
|
|
417
|
+
throw new Error("createAuth: hostOnlyCookies requires HTTPS and cannot share cookies across subdomains.");
|
|
418
|
+
}
|
|
401
419
|
if (config.methods.length === 0) {
|
|
402
420
|
throw new Error("createAuth: methods[] is empty — register at least one AuthMethodConfig so staff can sign in.");
|
|
403
421
|
}
|
|
@@ -412,19 +430,18 @@ function buildAuth(config) {
|
|
|
412
430
|
const providerOptions = config.oauthProvider
|
|
413
431
|
? buildOAuthProviderOptions(config.oauthProvider)
|
|
414
432
|
: null;
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
// surface) we ALWAYS turn limits on; adopter can override
|
|
420
|
-
// window/max via `config.rateLimit`.
|
|
433
|
+
// Workers may not set NODE_ENV. Explicitly enable the provider's route
|
|
434
|
+
// limits too (notably anonymous DCR: 5/minute).
|
|
435
|
+
// ponytail: memory limits are per isolate; use an ingress rate-limit rule
|
|
436
|
+
// when a deployment needs a distributed abuse quota.
|
|
421
437
|
const hasEmailMethod = !!(emailOtpMethod || magicLinkMethod);
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
:
|
|
438
|
+
const rateLimit = {
|
|
439
|
+
window: 60,
|
|
440
|
+
max: hasEmailMethod ? 10 : 100,
|
|
441
|
+
...config.rateLimit,
|
|
442
|
+
enabled: true,
|
|
443
|
+
storage: "memory",
|
|
444
|
+
};
|
|
428
445
|
// `trustedOrigins`: per-provider auto-origins (Apple needs
|
|
429
446
|
// `https://appleid.apple.com`) plus adopter-owned first-party
|
|
430
447
|
// origins for flows such as hosted auth across trusted subdomains.
|
|
@@ -457,6 +474,11 @@ function buildAuth(config) {
|
|
|
457
474
|
? mcp({
|
|
458
475
|
...providerOptions,
|
|
459
476
|
resource: config.oauthProvider.mcpResource,
|
|
477
|
+
extensions: [{
|
|
478
|
+
claims: {
|
|
479
|
+
accessToken: ({ referenceId }) => ({ mantle_consent_id: referenceId ?? null }),
|
|
480
|
+
},
|
|
481
|
+
}],
|
|
460
482
|
})
|
|
461
483
|
: oauthProvider(providerOptions),
|
|
462
484
|
...(config.oauthProvider.mcpResource
|
|
@@ -466,7 +488,9 @@ function buildAuth(config) {
|
|
|
466
488
|
// runtime network boundary: resolution and connection stay
|
|
467
489
|
// on the public Internet. Better Auth owns timeout, limits,
|
|
468
490
|
// validation, caching, and redirect rejection above it.
|
|
469
|
-
|
|
491
|
+
// Workers does not implement `redirect: "error"`; `manual`
|
|
492
|
+
// exposes 3xx responses so Better Auth can reject them.
|
|
493
|
+
fetchClientMetadataResource: (input, init) => fetch(input, { ...init, redirect: "manual" }),
|
|
470
494
|
metadataProfile: "mcp-2026-07-28",
|
|
471
495
|
}),
|
|
472
496
|
]
|
|
@@ -492,6 +516,8 @@ function buildAuth(config) {
|
|
|
492
516
|
// it (Better Auth's default raises a state-mismatch).
|
|
493
517
|
const appleNeedsCrossSite = methodsRequireSameSiteNone(config.methods);
|
|
494
518
|
const advancedConfig = {
|
|
519
|
+
// Cloudflare overwrites this at ingress. Never trust client-controlled XFF.
|
|
520
|
+
ipAddress: { ipAddressHeaders: ["cf-connecting-ip"] },
|
|
495
521
|
...(appleNeedsCrossSite
|
|
496
522
|
? {
|
|
497
523
|
// Browsers require `secure: true` whenever `sameSite: "none"`.
|
|
@@ -502,6 +528,13 @@ function buildAuth(config) {
|
|
|
502
528
|
? { crossSubDomainCookies: config.crossSubDomainCookies }
|
|
503
529
|
: {}),
|
|
504
530
|
...(config.cookiePrefix ? { cookiePrefix: config.cookiePrefix } : {}),
|
|
531
|
+
...(config.hostOnlyCookies ? {
|
|
532
|
+
// Better Auth prepends __Secure- otherwise. Secure is set explicitly below.
|
|
533
|
+
useSecureCookies: false,
|
|
534
|
+
cookiePrefix: `__Host-${config.cookiePrefix || "better-auth"}`,
|
|
535
|
+
crossSubDomainCookies: { enabled: false },
|
|
536
|
+
defaultCookieAttributes: { secure: true, path: "/", ...(appleNeedsCrossSite ? { sameSite: "none" } : {}) },
|
|
537
|
+
} : {}),
|
|
505
538
|
// Fire-and-forget hook closes the user-existence timing oracle
|
|
506
539
|
// on OTP send — see § "Auth as contract" notes in ADR-0014.
|
|
507
540
|
backgroundTasks: {
|
|
@@ -549,16 +582,59 @@ function buildAuth(config) {
|
|
|
549
582
|
before: async (user, context) => guardGithubLoginProfile(user, context, config.methods),
|
|
550
583
|
},
|
|
551
584
|
},
|
|
585
|
+
verification: {
|
|
586
|
+
create: {
|
|
587
|
+
before: async (verification) => {
|
|
588
|
+
if (!config.oauthProvider?.mcpResource)
|
|
589
|
+
return;
|
|
590
|
+
let value;
|
|
591
|
+
try {
|
|
592
|
+
value = JSON.parse(verification.value);
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return; // OTPs and other verification values are not OAuth grants.
|
|
596
|
+
}
|
|
597
|
+
if (value?.type !== "authorization_code" || typeof value.userId !== "string" ||
|
|
598
|
+
typeof value.query?.client_id !== "string")
|
|
599
|
+
return;
|
|
600
|
+
const consent = await config.database
|
|
601
|
+
.prepare("SELECT id FROM oauthConsent WHERE userId = ? AND clientId = ? LIMIT 1")
|
|
602
|
+
.bind(value.userId, value.query.client_id)
|
|
603
|
+
.first();
|
|
604
|
+
// Better Auth carries referenceId from this code through every
|
|
605
|
+
// refresh rotation. Never rebind an old lineage to a new consent.
|
|
606
|
+
return { data: { value: JSON.stringify({ ...value, referenceId: consent?.id ?? "" }) } };
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
},
|
|
552
610
|
};
|
|
611
|
+
const secondaryStorage = config.sessionCacheKv ? {
|
|
612
|
+
get: (key) => key.startsWith("verification:")
|
|
613
|
+
? Promise.resolve(null)
|
|
614
|
+
: config.sessionCacheKv.get(`better-auth:${key}`),
|
|
615
|
+
set: (key, value, ttl) => key.startsWith("verification:")
|
|
616
|
+
? Promise.resolve()
|
|
617
|
+
: config.sessionCacheKv.put(`better-auth:${key}`, value, ttl ? { expirationTtl: Math.max(60, Math.ceil(ttl)) } : undefined),
|
|
618
|
+
delete: (key) => key.startsWith("verification:")
|
|
619
|
+
? Promise.resolve()
|
|
620
|
+
: config.sessionCacheKv.delete(`better-auth:${key}`),
|
|
621
|
+
// Verification and rate limiting stay in D1/memory below. Fail loudly if
|
|
622
|
+
// Better Auth starts routing either atomic operation through this adapter.
|
|
623
|
+
getAndDelete: async () => { throw new Error("Better Auth KV getAndDelete is disabled"); },
|
|
624
|
+
increment: async () => { throw new Error("Better Auth KV increment is disabled"); },
|
|
625
|
+
} : undefined;
|
|
553
626
|
return betterAuth({
|
|
554
627
|
database: config.database,
|
|
628
|
+
secondaryStorage,
|
|
629
|
+
session: { storeSessionInDatabase: true },
|
|
630
|
+
verification: { storeInDatabase: true },
|
|
555
631
|
secret: config.secret,
|
|
556
632
|
baseURL: config.baseURL,
|
|
557
633
|
basePath: normalizeAuthBasePath(config.basePath),
|
|
558
634
|
onAPIError: { errorURL: normalizeAuthErrorURL(config.errorURL, config.baseURL) },
|
|
559
635
|
socialProviders,
|
|
560
636
|
user: userConfig,
|
|
561
|
-
|
|
637
|
+
rateLimit,
|
|
562
638
|
trustedOrigins,
|
|
563
639
|
advanced: advancedConfig,
|
|
564
640
|
plugins: sdkPlugins,
|
|
@@ -574,6 +650,34 @@ const LEGACY_DCR_TTL_MS = 90 * 24 * 60 * 60 * 1_000;
|
|
|
574
650
|
const LEGACY_DCR_CLEANUP_INTERVAL_MS = 60 * 60 * 1_000;
|
|
575
651
|
export function createAuth(config) {
|
|
576
652
|
const auth = buildAuth(config);
|
|
653
|
+
const ready = auth.$context.then(() => undefined);
|
|
654
|
+
// Observe eager initialization even for low-level callers; keep the original
|
|
655
|
+
// rejection available to callers awaiting ready and Better Auth's handlers.
|
|
656
|
+
void ready.catch(() => { });
|
|
657
|
+
// Auth owns its schema even when content uses a different semantic store.
|
|
658
|
+
// Keep schema work lazy: static/plan-only routes must not prepare tables.
|
|
659
|
+
let schemaReady = null;
|
|
660
|
+
const prepareAuth = () => schemaReady ??= (async () => {
|
|
661
|
+
const context = await auth.$context;
|
|
662
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(context.tables)));
|
|
663
|
+
const id = `auth-schema:1:${Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
664
|
+
try {
|
|
665
|
+
const applied = await config.database.prepare("SELECT id FROM _migrations WHERE id = ?").bind(id).first();
|
|
666
|
+
if (applied?.id === id)
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
// A new, auth-only database has no legacy Runtime ledger yet.
|
|
671
|
+
if (!/no such table: _migrations/i.test(String(error)))
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
const { compileMigrations } = await getMigrations(context.options);
|
|
675
|
+
await new D1DatabaseDriver(config.database).migrations.runAll([{
|
|
676
|
+
id,
|
|
677
|
+
description: "Selected Better Auth schema and staff-role access path",
|
|
678
|
+
sql: `${await compileMigrations()}\nCREATE INDEX IF NOT EXISTS user_role_idx ON user (role) WHERE role IS NOT NULL;`,
|
|
679
|
+
}]);
|
|
680
|
+
})().catch(error => { schemaReady = null; throw error; });
|
|
577
681
|
const basePath = normalizeAuthBasePath(config.basePath);
|
|
578
682
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
579
683
|
const api = auth.api;
|
|
@@ -588,8 +692,13 @@ export function createAuth(config) {
|
|
|
588
692
|
};
|
|
589
693
|
const verifyAccessToken = config.oauthProvider
|
|
590
694
|
? async (token, audience) => {
|
|
695
|
+
await prepareAuth();
|
|
591
696
|
const context = await auth.$context;
|
|
592
|
-
|
|
697
|
+
const claims = await verifyOAuthJwtWithLocalJwks(token, audience, context.baseURL, async () => api.getJwks(), localJwksCacheKey);
|
|
698
|
+
if (audience === config.oauthProvider?.mcpResource) {
|
|
699
|
+
await assertActiveMcpGrant(config.database, claims, audience);
|
|
700
|
+
}
|
|
701
|
+
return claims;
|
|
593
702
|
}
|
|
594
703
|
: null;
|
|
595
704
|
let nextDcrCleanupAt = 0;
|
|
@@ -612,18 +721,41 @@ export function createAuth(config) {
|
|
|
612
721
|
};
|
|
613
722
|
return {
|
|
614
723
|
basePath,
|
|
724
|
+
ready,
|
|
615
725
|
...(config.oauthProvider?.mcpResource
|
|
616
726
|
? { mcpResource: config.oauthProvider.mcpResource }
|
|
617
727
|
: {}),
|
|
618
728
|
handler: async (request) => {
|
|
729
|
+
await prepareAuth();
|
|
619
730
|
const pathname = new URL(request.url).pathname;
|
|
620
731
|
if (pathname.startsWith(`${basePath}/oauth2/`)) {
|
|
621
732
|
await pruneExpiredDynamicClients();
|
|
622
733
|
}
|
|
623
734
|
return normalizeAuthResponseCookies(await auth.handler(request));
|
|
624
735
|
},
|
|
625
|
-
getSession: (request) =>
|
|
736
|
+
getSession: async (request) => {
|
|
737
|
+
let session;
|
|
738
|
+
try {
|
|
739
|
+
session = await api.getSession({ headers: request.headers });
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
// Existing sessions resolve from KV without paying the schema-ledger read.
|
|
743
|
+
// A fresh database with a stale cookie prepares once, then retries safely.
|
|
744
|
+
await prepareAuth();
|
|
745
|
+
session = await api.getSession({ headers: request.headers });
|
|
746
|
+
}
|
|
747
|
+
return session
|
|
748
|
+
? {
|
|
749
|
+
...session,
|
|
750
|
+
user: {
|
|
751
|
+
...session.user,
|
|
752
|
+
...(Object.hasOwn(session.user, "role") ? { roleCurrent: true } : {}),
|
|
753
|
+
},
|
|
754
|
+
}
|
|
755
|
+
: null;
|
|
756
|
+
},
|
|
626
757
|
getUserRole: async (userId) => {
|
|
758
|
+
await prepareAuth();
|
|
627
759
|
const row = await config.database
|
|
628
760
|
.prepare("SELECT role FROM user WHERE id = ? LIMIT 1")
|
|
629
761
|
.bind(userId)
|
|
@@ -631,6 +763,7 @@ export function createAuth(config) {
|
|
|
631
763
|
return row?.role ?? null;
|
|
632
764
|
},
|
|
633
765
|
getUser: async (userId) => {
|
|
766
|
+
await prepareAuth();
|
|
634
767
|
const row = await config.database
|
|
635
768
|
.prepare("SELECT id, email, name, image, role, githubLogin, emailVerified, createdAt FROM user WHERE id = ? LIMIT 1")
|
|
636
769
|
.bind(userId)
|
|
@@ -653,6 +786,7 @@ export function createAuth(config) {
|
|
|
653
786
|
};
|
|
654
787
|
},
|
|
655
788
|
getProviderAccessToken: async (request, providerId) => {
|
|
789
|
+
await prepareAuth();
|
|
656
790
|
const session = await api.getSession({ headers: request.headers });
|
|
657
791
|
const userId = session?.user?.id;
|
|
658
792
|
const account = userId
|
|
@@ -676,6 +810,7 @@ export function createAuth(config) {
|
|
|
676
810
|
const clientId = url.searchParams.get("client_id");
|
|
677
811
|
if (!clientId || !url.search)
|
|
678
812
|
return null;
|
|
813
|
+
await prepareAuth();
|
|
679
814
|
const oauthQuery = url.search.slice(1);
|
|
680
815
|
const client = await api.getOAuthClientPublicPrelogin({
|
|
681
816
|
headers: request.headers,
|
|
@@ -706,6 +841,7 @@ export function createAuth(config) {
|
|
|
706
841
|
if (typeof oauthQuery !== "string" || oauthQuery.length === 0) {
|
|
707
842
|
throw new Error("completeOAuthConsent: oauth_query is missing.");
|
|
708
843
|
}
|
|
844
|
+
await prepareAuth();
|
|
709
845
|
const headers = new Headers(request.headers);
|
|
710
846
|
headers.set("content-type", "application/json");
|
|
711
847
|
headers.delete("content-length");
|
|
@@ -723,6 +859,57 @@ export function createAuth(config) {
|
|
|
723
859
|
}
|
|
724
860
|
return result.url;
|
|
725
861
|
},
|
|
862
|
+
...(config.oauthProvider
|
|
863
|
+
? {
|
|
864
|
+
listOAuthConsents: async (userId) => {
|
|
865
|
+
await prepareAuth();
|
|
866
|
+
const result = await config.database
|
|
867
|
+
.prepare(`SELECT consent.id, consent.clientId,
|
|
868
|
+
COALESCE(client.name, consent.clientId) AS clientName,
|
|
869
|
+
consent.scopes
|
|
870
|
+
FROM oauthConsent AS consent
|
|
871
|
+
LEFT JOIN oauthClient AS client ON client.clientId = consent.clientId
|
|
872
|
+
WHERE consent.userId = ?
|
|
873
|
+
ORDER BY consent.updatedAt DESC, consent.id ASC`)
|
|
874
|
+
.bind(userId)
|
|
875
|
+
.all();
|
|
876
|
+
return (result.results ?? []).map((row) => ({
|
|
877
|
+
id: row.id,
|
|
878
|
+
clientId: row.clientId,
|
|
879
|
+
clientName: row.clientName,
|
|
880
|
+
scopes: parseStoredStringArray(row.scopes) ?? [],
|
|
881
|
+
}));
|
|
882
|
+
},
|
|
883
|
+
revokeOAuthConsent: async (userId, consentId) => {
|
|
884
|
+
await prepareAuth();
|
|
885
|
+
const consent = await config.database
|
|
886
|
+
.prepare("SELECT clientId FROM oauthConsent WHERE id = ? AND userId = ? LIMIT 1")
|
|
887
|
+
.bind(consentId, userId)
|
|
888
|
+
.first();
|
|
889
|
+
if (!consent)
|
|
890
|
+
return false;
|
|
891
|
+
const revokedAt = new Date().toISOString();
|
|
892
|
+
await config.database.batch([
|
|
893
|
+
config.database
|
|
894
|
+
.prepare("UPDATE oauthRefreshToken SET revoked = ? WHERE userId = ? AND clientId = ? AND revoked IS NULL")
|
|
895
|
+
.bind(revokedAt, userId, consent.clientId),
|
|
896
|
+
config.database
|
|
897
|
+
.prepare("UPDATE oauthAccessToken SET revoked = ? WHERE userId = ? AND clientId = ? AND revoked IS NULL")
|
|
898
|
+
.bind(revokedAt, userId, consent.clientId),
|
|
899
|
+
config.database
|
|
900
|
+
.prepare(`DELETE FROM verification
|
|
901
|
+
WHERE CASE WHEN json_valid(value) THEN json_extract(value, '$.type') END = 'authorization_code'
|
|
902
|
+
AND CASE WHEN json_valid(value) THEN json_extract(value, '$.userId') END = ?
|
|
903
|
+
AND CASE WHEN json_valid(value) THEN json_extract(value, '$.query.client_id') END = ?`)
|
|
904
|
+
.bind(userId, consent.clientId),
|
|
905
|
+
config.database
|
|
906
|
+
.prepare("DELETE FROM oauthConsent WHERE userId = ? AND clientId = ?")
|
|
907
|
+
.bind(userId, consent.clientId),
|
|
908
|
+
]);
|
|
909
|
+
return true;
|
|
910
|
+
},
|
|
911
|
+
}
|
|
912
|
+
: {}),
|
|
726
913
|
methods: config.methods.map((m) => {
|
|
727
914
|
switch (m.kind) {
|
|
728
915
|
case "social":
|
|
@@ -739,6 +926,7 @@ export function createAuth(config) {
|
|
|
739
926
|
}
|
|
740
927
|
}),
|
|
741
928
|
listLinkedAccounts: async (userId) => {
|
|
929
|
+
await prepareAuth();
|
|
742
930
|
const result = await config.database
|
|
743
931
|
.prepare("SELECT id, providerId, accountId, createdAt, updatedAt FROM account WHERE userId = ? ORDER BY createdAt ASC, id ASC")
|
|
744
932
|
.bind(userId)
|
|
@@ -752,6 +940,7 @@ export function createAuth(config) {
|
|
|
752
940
|
}));
|
|
753
941
|
},
|
|
754
942
|
unlinkAccount: async (userId, providerId) => {
|
|
943
|
+
await prepareAuth();
|
|
755
944
|
const result = await config.database
|
|
756
945
|
.prepare("DELETE FROM account WHERE userId = ? AND providerId = ?")
|
|
757
946
|
.bind(userId, providerId)
|
|
@@ -759,6 +948,7 @@ export function createAuth(config) {
|
|
|
759
948
|
return (result.meta?.changes ?? 0) > 0;
|
|
760
949
|
},
|
|
761
950
|
listUsers: async () => {
|
|
951
|
+
await prepareAuth();
|
|
762
952
|
const placeholders = STAFF_ROLES.map(() => "?").join(",");
|
|
763
953
|
const result = await config.database
|
|
764
954
|
.prepare(`SELECT id, email, name, role, githubLogin, emailVerified, createdAt FROM user WHERE role IN (${placeholders}) ORDER BY createdAt ASC, id ASC`)
|
|
@@ -775,6 +965,7 @@ export function createAuth(config) {
|
|
|
775
965
|
}));
|
|
776
966
|
},
|
|
777
967
|
listMembers: async ({ search, cursor, cursorDirection = "forward", limit }) => {
|
|
968
|
+
await prepareAuth();
|
|
778
969
|
const parsedCursor = cursor ? decodeMemberCursor(cursor) : null;
|
|
779
970
|
const backward = cursorDirection === "backward";
|
|
780
971
|
const conditions = [
|
|
@@ -783,9 +974,9 @@ export function createAuth(config) {
|
|
|
783
974
|
const bindings = [...STAFF_ROLES];
|
|
784
975
|
const term = search?.trim().toLowerCase();
|
|
785
976
|
if (term) {
|
|
786
|
-
conditions.push("(LOWER(name) LIKE ? ESCAPE '\\' OR LOWER(email) LIKE ? ESCAPE '\\')");
|
|
977
|
+
conditions.push("(LOWER(id) LIKE ? ESCAPE '\\' OR LOWER(name) LIKE ? ESCAPE '\\' OR LOWER(email) LIKE ? ESCAPE '\\')");
|
|
787
978
|
const like = `%${term.replace(/[\\%_]/g, (character) => `\\${character}`)}%`;
|
|
788
|
-
bindings.push(like, like);
|
|
979
|
+
bindings.push(like, like, like);
|
|
789
980
|
}
|
|
790
981
|
if (parsedCursor) {
|
|
791
982
|
const operator = backward ? "<" : ">";
|
|
@@ -822,6 +1013,14 @@ export function createAuth(config) {
|
|
|
822
1013
|
if (role !== null && !STAFF_ROLE_SET.has(role)) {
|
|
823
1014
|
throw new Error(`setUserRole: '${role}' is not a staff role — expected one of [${STAFF_ROLES.join(", ")}] or null.`);
|
|
824
1015
|
}
|
|
1016
|
+
await prepareAuth();
|
|
1017
|
+
if (config.sessionCacheKv) {
|
|
1018
|
+
const context = await auth.$context;
|
|
1019
|
+
return !!await context.internalAdapter.updateUser(userId, {
|
|
1020
|
+
role,
|
|
1021
|
+
updatedAt: new Date(),
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
825
1024
|
const result = await config.database
|
|
826
1025
|
.prepare("UPDATE user SET role = ?, updatedAt = ? WHERE id = ?")
|
|
827
1026
|
.bind(role, new Date().toISOString(), userId)
|
|
@@ -832,6 +1031,7 @@ export function createAuth(config) {
|
|
|
832
1031
|
if (!STAFF_ROLE_SET.has(role)) {
|
|
833
1032
|
throw new Error(`inviteUser: '${role}' is not a staff role — expected one of [${STAFF_ROLES.join(", ")}].`);
|
|
834
1033
|
}
|
|
1034
|
+
await prepareAuth();
|
|
835
1035
|
const normalized = email.trim().toLowerCase();
|
|
836
1036
|
const existing = await config.database
|
|
837
1037
|
.prepare("SELECT id FROM user WHERE email = ? LIMIT 1")
|
|
@@ -850,7 +1050,19 @@ export function createAuth(config) {
|
|
|
850
1050
|
.run();
|
|
851
1051
|
return { kind: "created", id };
|
|
852
1052
|
},
|
|
1053
|
+
...(config.staffInvitationSender ? {
|
|
1054
|
+
sendStaffInvitation: async (email, role) => {
|
|
1055
|
+
const normalized = email.trim().toLowerCase();
|
|
1056
|
+
await config.staffInvitationSender.send({
|
|
1057
|
+
to: normalized,
|
|
1058
|
+
...staffInvitationEmail(role, new URL("/admin/sign-in", config.baseURL).href),
|
|
1059
|
+
locale: "en",
|
|
1060
|
+
category: "auth.staff-invitation",
|
|
1061
|
+
});
|
|
1062
|
+
},
|
|
1063
|
+
} : {}),
|
|
853
1064
|
revokeInvite: async (userId) => {
|
|
1065
|
+
await prepareAuth();
|
|
854
1066
|
const result = await config.database
|
|
855
1067
|
.prepare("DELETE FROM user WHERE id = ? AND emailVerified = 0 AND NOT EXISTS (SELECT 1 FROM account WHERE account.userId = user.id)")
|
|
856
1068
|
.bind(userId)
|
|
@@ -861,6 +1073,7 @@ export function createAuth(config) {
|
|
|
861
1073
|
if (!config.oauthProvider) {
|
|
862
1074
|
throw new Error("registerOAuthClient: oauthProvider is not configured.");
|
|
863
1075
|
}
|
|
1076
|
+
await prepareAuth();
|
|
864
1077
|
const created = await api.adminCreateOAuthClient({
|
|
865
1078
|
headers: new Headers(input.requestHeaders),
|
|
866
1079
|
body: {
|
|
@@ -968,6 +1181,44 @@ function scopesFromClaim(value) {
|
|
|
968
1181
|
}
|
|
969
1182
|
return [];
|
|
970
1183
|
}
|
|
1184
|
+
function parseStoredStringArray(value) {
|
|
1185
|
+
if (typeof value !== "string")
|
|
1186
|
+
return null;
|
|
1187
|
+
try {
|
|
1188
|
+
const parsed = JSON.parse(value);
|
|
1189
|
+
return Array.isArray(parsed) && parsed.every((item) => typeof item === "string")
|
|
1190
|
+
? parsed
|
|
1191
|
+
: null;
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
async function assertActiveMcpGrant(database, claims, audience) {
|
|
1198
|
+
const userId = claims["sub"];
|
|
1199
|
+
const clientId = claims["azp"];
|
|
1200
|
+
const sessionId = claims["sid"];
|
|
1201
|
+
const consentId = claims["mantle_consent_id"];
|
|
1202
|
+
if (typeof userId !== "string" ||
|
|
1203
|
+
typeof clientId !== "string" ||
|
|
1204
|
+
typeof sessionId !== "string" ||
|
|
1205
|
+
typeof consentId !== "string" || !consentId) {
|
|
1206
|
+
throw new Error("MCP token is not bound to a user session.");
|
|
1207
|
+
}
|
|
1208
|
+
const result = await database
|
|
1209
|
+
.prepare("SELECT c.resources, c.scopes FROM oauthConsent AS c " +
|
|
1210
|
+
"JOIN session AS s ON s.id = ? AND s.userId = c.userId AND s.expiresAt > ? " +
|
|
1211
|
+
"WHERE c.id = ? AND c.userId = ? AND c.clientId = ?")
|
|
1212
|
+
.bind(sessionId, new Date().toISOString(), consentId, userId, clientId)
|
|
1213
|
+
.first();
|
|
1214
|
+
const tokenScopes = scopesFromClaim(claims["scope"]);
|
|
1215
|
+
const resources = parseStoredStringArray(result?.resources);
|
|
1216
|
+
const scopes = parseStoredStringArray(result?.scopes);
|
|
1217
|
+
const active = resources?.includes(audience) === true && scopes !== null &&
|
|
1218
|
+
tokenScopes.every((scope) => scopes.includes(scope));
|
|
1219
|
+
if (!active)
|
|
1220
|
+
throw new Error("MCP authorization grant is no longer active.");
|
|
1221
|
+
}
|
|
971
1222
|
/** @internal exported to keep same-Worker OAuth verification off the network. */
|
|
972
1223
|
export function verifyOAuthJwtWithLocalJwks(token, audience, issuer, jwksFetch, jwksCacheKey) {
|
|
973
1224
|
return verifyJwsAccessToken(token, {
|