56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
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 };
|
|
}
|