From 72c103c1892d8e96dc2864a87df2becd8f56e13c Mon Sep 17 00:00:00 2001 From: Nadeesha Cabral Date: Mon, 30 Dec 2024 21:09:59 +1100 Subject: [PATCH] fix: Implement HTTP signing for Val.town API requests (#424) * feat: Implement HTTP signing for Val.town API requests * chore: Remove redundant secret parameter from Val.town integration functions --- .../src/modules/integrations/valtown.test.ts | 20 +++++++++ .../src/modules/integrations/valtown.ts | 43 +++++++++++++++++-- control-plane/src/utilities/env.ts | 7 ++- 3 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 control-plane/src/modules/integrations/valtown.test.ts diff --git a/control-plane/src/modules/integrations/valtown.test.ts b/control-plane/src/modules/integrations/valtown.test.ts new file mode 100644 index 00000000..01e03a28 --- /dev/null +++ b/control-plane/src/modules/integrations/valtown.test.ts @@ -0,0 +1,20 @@ +import { signedHeaders } from "./valtown"; + +describe("Val.town Integration", () => { + describe("signedHeaders", () => { + it("should sign headers", async () => { + const result = signedHeaders({ + body: JSON.stringify({ test: "value" }), + method: "GET", + path: "/meta", + secret: "secret", + timestamp: "819118800000", + }); + + expect(result).toStrictEqual({ + "X-Signature": "126f621ef1898cba0c6b0c0bd74d443c6004e7c3291a6432f90a233db903c8d8", + "X-Timestamp": "819118800000", + }); + }); + }); +}); diff --git a/control-plane/src/modules/integrations/valtown.ts b/control-plane/src/modules/integrations/valtown.ts index 9f13d7c8..d2c466ee 100644 --- a/control-plane/src/modules/integrations/valtown.ts +++ b/control-plane/src/modules/integrations/valtown.ts @@ -8,6 +8,8 @@ import { deleteServiceDefinition, upsertServiceDefinition } from "../service-def import { integrationSchema } from "./schema"; import { InstallableIntegration } from "./types"; import { valtownIntegration } from "./constants"; +import { env } from "../../utilities/env"; +import { createHmac } from "crypto"; // Schema for the /meta endpoint response const valtownMetaSchema = z.object({ @@ -28,12 +30,42 @@ const valtownMetaSchema = z.object({ type ValTownMeta = z.infer; +export const signedHeaders = ({ + body, + method, + path, + secret = env.VALTOWN_HTTP_SIGNING_SECRET, + timestamp = Date.now().toString(), +}: { + body: string; + method: string; + path: string; + secret?: string; + timestamp?: string; +}): Record => { + if (!secret) { + logger.error("Missing Val.town HTTP signing secret"); + return {}; + } + + const hmac = createHmac("sha256", secret); + hmac.update(`${timestamp}${method}${path}${body}`); + const xSignature = hmac.digest("hex"); + + return { + "X-Signature": xSignature, + "X-Timestamp": timestamp, + }; +}; + /** * Fetch metadata from Val.town endpoint */ -async function fetchValTownMeta({ endpoint }: { endpoint: string }): Promise { +export async function fetchValTownMeta({ endpoint }: { endpoint: string }): Promise { const metaUrl = new URL("/meta", endpoint).toString(); - const response = await fetch(metaUrl); + const response = await fetch(metaUrl, { + headers: signedHeaders({ body: "", method: "GET", path: "/meta" }), + }); if (!response.ok) { logger.error("Failed to fetch Val.town metadata", { @@ -52,7 +84,7 @@ async function fetchValTownMeta({ endpoint }: { endpoint: string }): Promise value == "true" || value == "1"); + .transform(value => value == "true" || value == "1"); const envSchema = z .object({ NODE_ENV: z .enum(["test", "development", "production"]) .default("development") - .transform((value) => { + .transform(value => { if (process.env.CI) { return "test"; } @@ -75,6 +75,9 @@ const envSchema = z POSTHOG_API_KEY: z.string().optional(), POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), ANALYTICS_BUCKET_NAME: z.string().optional(), + + // Integrations + VALTOWN_HTTP_SIGNING_SECRET: z.string().optional(), }) .superRefine((value, ctx) => { if (!value.MANAGEMENT_API_SECRET && !value.JWKS_URL) {