84 lines
3.7 KiB
TypeScript
84 lines
3.7 KiB
TypeScript
import { env } from "cloudflare:workers";
|
|
import { genericOAuth, organization } from "better-auth/plugins";
|
|
import { baseAuthOptions } from "@/lib/auth-options";
|
|
import { GA4_OAUTH_PROVIDER_ID, GA4_OAUTH_SCOPES } from "@/shared/ga4";
|
|
import { GSC_OAUTH_PROVIDER_ID, GSC_OAUTH_SCOPES } from "@/shared/gsc";
|
|
|
|
export function createBaseAuthConfig() {
|
|
return {
|
|
...baseAuthOptions,
|
|
advanced: {
|
|
ipAddress: {
|
|
// On Cloudflare Workers the client IP arrives in CF-Connecting-IP;
|
|
// x-forwarded-for (better-auth's default) is absent, so without this
|
|
// getIp() returns null and rate limiting is silently skipped on every
|
|
// /api/auth endpoint. Header lookup is case-insensitive.
|
|
ipAddressHeaders: ["cf-connecting-ip"],
|
|
},
|
|
},
|
|
account: {
|
|
// Encrypt OAuth access/refresh tokens at rest in D1. Also covers the
|
|
// google social-login tokens; the key derives from BETTER_AUTH_SECRET.
|
|
encryptOAuthTokens: true,
|
|
accountLinking: {
|
|
// Allow connecting a Google account whose email differs from the
|
|
// logged-in user's (agency/freelancer managing a client's property).
|
|
allowDifferentEmails: true,
|
|
},
|
|
},
|
|
plugins: [
|
|
// Block user-initiated org creation: each org is its own Autumn customer
|
|
// with its own onboarding-plan credit grant, so an authenticated user
|
|
// hitting POST /api/auth/organization/create could mint unlimited fresh
|
|
// grants. The app gives every user exactly one workspace, created
|
|
// server-side at signup via `auth.api.createOrganization({ body: { userId }})`
|
|
// — that's a "system action" (no session + userId in body) which better-auth
|
|
// exempts from this flag, so the bootstrap keeps working.
|
|
//
|
|
// invitationLimit: 0 closes the other path to multi-org membership.
|
|
// "One user, one workspace" is a billing invariant: MCP API keys bill the
|
|
// user's first org, sessions bill the active org — identical only while
|
|
// users can't be invited into a second workspace. Remove this when teams
|
|
// ship, in the same change that moves API-key requests to project-level
|
|
// authz (org derived per tool call from the project; keys stay
|
|
// user-scoped, no key→workspace binding).
|
|
//
|
|
// disableOrganizationDeletion closes the delete side of the same loop:
|
|
// POST /api/auth/organization/delete (owner-callable by default) would
|
|
// cascade-delete the workspace, and the next request auto-creates a fresh
|
|
// org id — a fresh Autumn customer with a fresh credit grant.
|
|
organization({
|
|
allowUserToCreateOrganization: false,
|
|
invitationLimit: 0,
|
|
disableOrganizationDeletion: true,
|
|
}),
|
|
genericOAuth({
|
|
config: [
|
|
{
|
|
providerId: GSC_OAUTH_PROVIDER_ID,
|
|
clientId: env.GOOGLE_CLIENT_ID?.trim() ?? "",
|
|
clientSecret: env.GOOGLE_CLIENT_SECRET?.trim() ?? "",
|
|
discoveryUrl:
|
|
"https://accounts.google.com/.well-known/openid-configuration",
|
|
scopes: [...GSC_OAUTH_SCOPES],
|
|
accessType: "offline", // request a refresh token
|
|
prompt: "select_account consent",
|
|
pkce: true,
|
|
},
|
|
{
|
|
providerId: GA4_OAUTH_PROVIDER_ID,
|
|
clientId: env.GOOGLE_CLIENT_ID?.trim() ?? "",
|
|
clientSecret: env.GOOGLE_CLIENT_SECRET?.trim() ?? "",
|
|
discoveryUrl:
|
|
"https://accounts.google.com/.well-known/openid-configuration",
|
|
scopes: [...GA4_OAUTH_SCOPES],
|
|
accessType: "offline",
|
|
prompt: "select_account consent",
|
|
pkce: true,
|
|
},
|
|
],
|
|
}),
|
|
],
|
|
};
|
|
}
|