import crypto from "node:crypto"; import { optionalEnv } from "../config/env.server"; /** * Resolves the GiroCode URL signing key lazily (per call, not at module load) * so the process can boot even when only the fallback secret is present. * * Prefers the dedicated `GIROCODE_SIGNING_KEY`; falls back to * `SHOPIFY_API_SECRET` ONLY when the dedicated key is unset, so existing * signed URLs and deployments keep working. Throws if neither is set * (fail closed) — an empty key would make signatures forgeable. */ function getSigningKey(): string { const key = optionalEnv("GIROCODE_SIGNING_KEY") ?? optionalEnv("SHOPIFY_API_SECRET"); if (!key) { throw new Error( "GiroCode signing key missing: set GIROCODE_SIGNING_KEY (preferred) " + "or SHOPIFY_API_SECRET.", ); } return key; } function hmac(payload: string): string { return crypto.createHmac("sha256", getSigningKey()).update(payload).digest("hex"); } export interface GiroCodeUrlParams { shop: string; orderId: string; exp: number; // unix seconds } export function signGiroCodeUrl(params: GiroCodeUrlParams): string { const base = `shop=${params.shop}&orderId=${params.orderId}&exp=${params.exp}`; const sig = hmac(base); return `${base}&sig=${sig}`; } export function verifyGiroCodeUrl(query: URLSearchParams): { ok: boolean; shop?: string; orderId?: string; reason?: string } { const shop = query.get("shop") || ""; const orderId = query.get("orderId") || ""; const exp = parseInt(query.get("exp") || "0", 10); const sig = query.get("sig") || ""; if (!shop || !orderId || !exp || !sig) return { ok: false, reason: "missing-params" }; if (Date.now() / 1000 > exp) return { ok: false, reason: "expired" }; const expected = hmac(`shop=${shop}&orderId=${orderId}&exp=${exp}`); // timing-safe compare const a = Buffer.from(sig); const b = Buffer.from(expected); if (a.length !== b.length) return { ok: false, reason: "bad-sig" }; if (!crypto.timingSafeEqual(a, b)) return { ok: false, reason: "bad-sig" }; return { ok: true, shop, orderId }; }