301c477e25
Main är sedan den här grenen skapades en helt annan produkt — Semantika,
en mobilapp med egen CDK-infrastruktur. Den äger nu repots rot: en
npm-workspaces-monorepo med apps/mobile, services/api och infra.
För att båda ska rymmas i samma repo flyttar Guidad Felsökning in i en
egen katalog i stället för att göra anspråk på roten:
felsokning/app webbklienten (Vite, egen package.json och
eslint-/vitest-konfiguration)
felsokning/services plattformstjänsten och AI-orkestern
felsokning/infra Terraform och databasschemat
felsokning/docs vision, moduler, drift
felsokning/supabase edge-funktion och migrationer
Merge:n hade tagit bort 128 filer som Guidad Felsökning bygger på —
värdapplikationens komponenter, Supabase-klienten, tillgångar — eftersom
main raderat dem och den här grenen inte råkat ändra just dem. De är
återställda på sin nya plats. Utan dem gick varken bygget eller
testerna: ai.ts och synk.ts importerar Supabase-klienten.
Semantikas rotfiler är orörda: package.json, eslint.config.js och
.github/workflows/ är deras. Guidad Felsökning har egna motsvarigheter i
sin katalog.
CI flyttar samtidigt från GitHub Actions till .gitea/workflows — samma
syntax, egna runners. .github/workflows/ tillhör Semantika härefter.
Verifierat på den nya platsen: 96 vitest-tester, typkontroll, eslint på
både klient och tjänster, bygge, och integrationstest mot riktig Postgres.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
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);
|
|
}
|