metatron-open-seo/src/lib/auth-config.ts
2026-08-11 19:20:44 -04:00

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,
},
],
}),
],
};
}