Files
Claude 301c477e25 Merge main: Guidad Felsökning flyttar ur roten till felsokning/
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
2026-08-04 12:22:49 +00:00

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