|
@@ -9,24 +9,60 @@ import type { CloudflareBindings } from "../env";
|
|
|
|
|
|
|
|
// Identity = phone number. OTP over SMS in prod; logged to the console in dev.
|
|
// Identity = phone number. OTP over SMS in prod; logged to the console in dev.
|
|
|
// Anonymous stays enabled as the zero-friction path for loop prototyping.
|
|
// Anonymous stays enabled as the zero-friction path for loop prototyping.
|
|
|
-async function sendOTP(env: CloudflareBindings | undefined, phone: string, code: string) {
|
|
|
|
|
- if (!env || env.OTP_MODE !== "twilio") {
|
|
|
|
|
|
|
+type TwilioConfig = {
|
|
|
|
|
+ accountSid: string;
|
|
|
|
|
+ authToken: string;
|
|
|
|
|
+ from: string;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+function getTwilioConfig(env: CloudflareBindings): TwilioConfig | undefined {
|
|
|
|
|
+ const mode: string = env.OTP_MODE;
|
|
|
|
|
+ if (mode === "console") return undefined;
|
|
|
|
|
+ if (mode !== "twilio") throw new Error(`Unsupported OTP_MODE: ${mode}`);
|
|
|
|
|
+
|
|
|
|
|
+ const missing = [
|
|
|
|
|
+ ["TWILIO_ACCOUNT_SID", env.TWILIO_ACCOUNT_SID],
|
|
|
|
|
+ ["TWILIO_AUTH_TOKEN", env.TWILIO_AUTH_TOKEN],
|
|
|
|
|
+ ["TWILIO_FROM", env.TWILIO_FROM],
|
|
|
|
|
+ ].filter(([, value]) => !value?.trim());
|
|
|
|
|
+
|
|
|
|
|
+ if (missing.length > 0) {
|
|
|
|
|
+ throw new Error(`OTP_MODE=twilio requires ${missing.map(([name]) => name).join(", ")}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ accountSid: env.TWILIO_ACCOUNT_SID!.trim(),
|
|
|
|
|
+ authToken: env.TWILIO_AUTH_TOKEN!.trim(),
|
|
|
|
|
+ from: env.TWILIO_FROM!.trim(),
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function sendOTP(config: TwilioConfig | undefined, phone: string, code: string) {
|
|
|
|
|
+ if (!config) {
|
|
|
console.log(`[OTP] ${phone} -> ${code}`);
|
|
console.log(`[OTP] ${phone} -> ${code}`);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- const body = new URLSearchParams({ To: phone, From: env.TWILIO_FROM!, Body: `Your Plan Maker code is ${code}` });
|
|
|
|
|
- await fetch(`https://api.twilio.com/2010-04-01/Accounts/${env.TWILIO_ACCOUNT_SID}/Messages.json`, {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const body = new URLSearchParams({ To: phone, From: config.from, Body: `Your Plan Maker code is ${code}` });
|
|
|
|
|
+ const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`, {
|
|
|
method: "POST",
|
|
method: "POST",
|
|
|
- headers: { Authorization: "Basic " + btoa(`${env.TWILIO_ACCOUNT_SID}:${env.TWILIO_AUTH_TOKEN}`) },
|
|
|
|
|
|
|
+ headers: { Authorization: "Basic " + btoa(`${config.accountSid}:${config.authToken}`) },
|
|
|
body,
|
|
body,
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`Twilio rejected the OTP message (HTTP ${response.status})`);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function createAuth(env?: CloudflareBindings, cf?: IncomingRequestCfProperties, baseURL?: string) {
|
|
function createAuth(env?: CloudflareBindings, cf?: IncomingRequestCfProperties, baseURL?: string) {
|
|
|
const db = env ? drizzle(env.DATABASE, { schema }) : ({} as any);
|
|
const db = env ? drizzle(env.DATABASE, { schema }) : ({} as any);
|
|
|
|
|
+ const authBaseURL = new URL(env?.BETTER_AUTH_URL?.trim() || baseURL || "http://localhost:8787").origin;
|
|
|
|
|
+ const twilio = env ? getTwilioConfig(env) : undefined;
|
|
|
|
|
|
|
|
return betterAuth({
|
|
return betterAuth({
|
|
|
- baseURL,
|
|
|
|
|
|
|
+ baseURL: authBaseURL,
|
|
|
|
|
+ trustedOrigins: [authBaseURL],
|
|
|
secret: env?.BETTER_AUTH_SECRET,
|
|
secret: env?.BETTER_AUTH_SECRET,
|
|
|
...withCloudflare(
|
|
...withCloudflare(
|
|
|
{
|
|
{
|
|
@@ -40,7 +76,7 @@ function createAuth(env?: CloudflareBindings, cf?: IncomingRequestCfProperties,
|
|
|
plugins: [
|
|
plugins: [
|
|
|
anonymous(),
|
|
anonymous(),
|
|
|
phoneNumber({
|
|
phoneNumber({
|
|
|
- sendOTP: ({ phoneNumber, code }) => sendOTP(env, phoneNumber, code),
|
|
|
|
|
|
|
+ sendOTP: ({ phoneNumber, code }) => sendOTP(twilio, phoneNumber, code),
|
|
|
otpLength: 6,
|
|
otpLength: 6,
|
|
|
expiresIn: 300,
|
|
expiresIn: 300,
|
|
|
signUpOnVerification: {
|
|
signUpOnVerification: {
|
|
@@ -52,7 +88,7 @@ function createAuth(env?: CloudflareBindings, cf?: IncomingRequestCfProperties,
|
|
|
rateLimit: { enabled: true, window: 60, max: 100 },
|
|
rateLimit: { enabled: true, window: 60, max: 100 },
|
|
|
}
|
|
}
|
|
|
),
|
|
),
|
|
|
- ...(env ? {} : { database: drizzleAdapter({} as any, { provider: "sqlite", usePlural: true }) }),
|
|
|
|
|
|
|
+ ...(env ? {} : { database: drizzleAdapter({} as any, { provider: "sqlite", schema, usePlural: true }) }),
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|