ab608f50af
Co-authored-by: wolfoftyreso-debug <250630591+wolfoftyreso-debug@users.noreply.github.com>
88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
import { encode } from "https://deno.land/std@0.168.0/encoding/hex.ts";
|
|
import Stripe from "https://esm.sh/stripe@22.0.2";
|
|
|
|
const getEnv = (key: string): string => {
|
|
const value = Deno.env.get(key);
|
|
if (!value) throw new Error(`${key} is not configured`);
|
|
return value;
|
|
};
|
|
|
|
export type StripeEnv = "sandbox" | "live";
|
|
|
|
const GATEWAY_STRIPE_BASE = "https://connector-gateway.lovable.dev/stripe";
|
|
|
|
export function getConnectionApiKey(env: StripeEnv): string {
|
|
return env === "sandbox"
|
|
? getEnv("STRIPE_SANDBOX_API_KEY")
|
|
: getEnv("STRIPE_LIVE_API_KEY");
|
|
}
|
|
|
|
export function createStripeClient(env: StripeEnv): Stripe {
|
|
const connectionApiKey = getConnectionApiKey(env);
|
|
const lovableApiKey = getEnv("LOVABLE_API_KEY");
|
|
|
|
return new Stripe(connectionApiKey, {
|
|
apiVersion: "2026-03-25.dahlia" as any,
|
|
httpClient: Stripe.createFetchHttpClient((input, init) => {
|
|
const stripeUrl = input instanceof Request ? input.url : input.toString();
|
|
const gatewayUrl = stripeUrl.replace("https://api.stripe.com", GATEWAY_STRIPE_BASE);
|
|
return fetch(gatewayUrl, {
|
|
...init,
|
|
headers: {
|
|
...Object.fromEntries(
|
|
new Headers(
|
|
init?.headers ?? (input instanceof Request ? input.headers : undefined),
|
|
).entries(),
|
|
),
|
|
"X-Connection-Api-Key": connectionApiKey,
|
|
"Lovable-API-Key": lovableApiKey,
|
|
},
|
|
});
|
|
}),
|
|
});
|
|
}
|
|
|
|
export async function verifyWebhook(
|
|
req: Request,
|
|
env: StripeEnv,
|
|
): Promise<{ type: string; data: { object: any } }> {
|
|
const signature = req.headers.get("stripe-signature");
|
|
const body = await req.text();
|
|
const secret =
|
|
env === "sandbox"
|
|
? getEnv("PAYMENTS_SANDBOX_WEBHOOK_SECRET")
|
|
: getEnv("PAYMENTS_LIVE_WEBHOOK_SECRET");
|
|
|
|
if (!signature || !body) throw new Error("Missing signature or body");
|
|
|
|
let timestamp: string | undefined;
|
|
const v1Signatures: string[] = [];
|
|
for (const part of signature.split(",")) {
|
|
const [key, value] = part.split("=", 2);
|
|
if (key === "t") timestamp = value;
|
|
if (key === "v1") v1Signatures.push(value);
|
|
}
|
|
if (!timestamp || v1Signatures.length === 0) throw new Error("Invalid signature format");
|
|
|
|
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
|
|
if (age > 300) throw new Error("Webhook timestamp too old");
|
|
|
|
const key = await crypto.subtle.importKey(
|
|
"raw",
|
|
new TextEncoder().encode(secret),
|
|
{ name: "HMAC", hash: "SHA-256" },
|
|
false,
|
|
["sign"],
|
|
);
|
|
const signed = await crypto.subtle.sign(
|
|
"HMAC",
|
|
key,
|
|
new TextEncoder().encode(`${timestamp}.${body}`),
|
|
);
|
|
const expected = new TextDecoder().decode(encode(new Uint8Array(signed)));
|
|
|
|
if (!v1Signatures.includes(expected)) throw new Error("Invalid webhook signature");
|
|
|
|
return JSON.parse(body);
|
|
}
|