From be8b6448f9fa3fd58c4984f36c37ff415934fd3a Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Sat, 25 Jan 2025 11:47:24 +0800 Subject: [PATCH] feat: support mTLS certificate upload (#835) * feat: Add mTLS and custom HMAC support to webhoks * remove resetNonce * use env vars for client id/secret * update tests * debug steps * remove async * remove unneeded changes --- .env.test | 14 +-- package.json | 1 + .../migration.sql | 3 + src/prisma/schema.prisma | 3 + src/server/middleware/auth.ts | 10 +- src/server/routes/configuration/auth/get.ts | 10 +- .../routes/configuration/auth/update.ts | 55 ++++++++- src/server/routes/webhooks/create.ts | 2 +- .../db/configuration/get-configuration.ts | 6 + .../db/configuration/update-configuration.ts | 2 +- src/shared/db/webhooks/create-webhook.ts | 2 +- src/shared/schemas/config.ts | 4 + src/shared/utils/crypto.ts | 14 +-- src/shared/utils/custom-auth-header.ts | 63 +++++++++++ src/shared/utils/env.ts | 23 ++-- src/shared/utils/webhook.ts | 106 +++++++++++++----- tests/unit/webhook.test.ts | 57 ++++++++++ yarn.lock | 21 ++-- 18 files changed, 310 insertions(+), 86 deletions(-) create mode 100644 src/prisma/migrations/20241031010103_add_mtls_configuration/migration.sql create mode 100644 src/shared/utils/custom-auth-header.ts create mode 100644 tests/unit/webhook.test.ts diff --git a/.env.test b/.env.test index d2acedfb2..5ab9791db 100644 --- a/.env.test +++ b/.env.test @@ -7,11 +7,11 @@ ENABLE_HTTPS="true" REDIS_URL="redis://127.0.0.1:6379/0" THIRDWEB_API_SECRET_KEY="my-thirdweb-secret-key" -TEST_AWS_KMS_KEY_ID="" -TEST_AWS_KMS_ACCESS_KEY_ID="" -TEST_AWS_KMS_SECRET_ACCESS_KEY="" -TEST_AWS_KMS_REGION="" +TEST_AWS_KMS_KEY_ID="UNIMPLEMENTED" +TEST_AWS_KMS_ACCESS_KEY_ID="UNIMPLEMENTED" +TEST_AWS_KMS_SECRET_ACCESS_KEY="UNIMPLEMENTED" +TEST_AWS_KMS_REGION="UNIMPLEMENTED" -TEST_GCP_KMS_RESOURCE_PATH="" -TEST_GCP_KMS_EMAIL="" -TEST_GCP_KMS_PK="" \ No newline at end of file +TEST_GCP_KMS_RESOURCE_PATH="UNIMPLEMENTED" +TEST_GCP_KMS_EMAIL="UNIMPLEMENTED" +TEST_GCP_KMS_PK="UNIMPLEMENTED" \ No newline at end of file diff --git a/package.json b/package.json index 69359687f..5be6c5aa4 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "prom-client": "^15.1.3", "superjson": "^2.2.1", "thirdweb": "^5.83.0", + "undici": "^6.20.1", "uuid": "^9.0.1", "viem": "^2.21.54", "winston": "^3.14.1", diff --git a/src/prisma/migrations/20241031010103_add_mtls_configuration/migration.sql b/src/prisma/migrations/20241031010103_add_mtls_configuration/migration.sql new file mode 100644 index 000000000..9e7a54aac --- /dev/null +++ b/src/prisma/migrations/20241031010103_add_mtls_configuration/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "configuration" ADD COLUMN "mtlsCertificateEncrypted" TEXT, +ADD COLUMN "mtlsPrivateKeyEncrypted" TEXT; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 2e04b2d8b..44608303b 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -50,6 +50,9 @@ model Configuration { accessControlAllowOrigin String @default("https://thirdweb.com,https://embed.ipfscdn.io") @map("accessControlAllowOrigin") ipAllowlist String[] @default([]) @map("ipAllowlist") clearCacheCronSchedule String @default("*/30 * * * * *") @map("clearCacheCronSchedule") + // mTLS support + mtlsCertificateEncrypted String? + mtlsPrivateKeyEncrypted String? @@map("configuration") } diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts index f29804e70..fb21508da 100644 --- a/src/server/middleware/auth.ts +++ b/src/server/middleware/auth.ts @@ -5,10 +5,10 @@ import { type ThirdwebAuthUser, } from "@thirdweb-dev/auth/fastify"; import { AsyncWallet } from "@thirdweb-dev/wallets/evm/wallets/async"; -import { createHash } from "node:crypto"; import type { FastifyInstance } from "fastify"; import type { FastifyRequest } from "fastify/types/request"; import jsonwebtoken, { type JwtPayload } from "jsonwebtoken"; +import { createHash } from "node:crypto"; import { validate as uuidValidate } from "uuid"; import { getPermissions } from "../../shared/db/permissions/get-permissions"; import { createToken } from "../../shared/db/tokens/create-token"; @@ -123,7 +123,8 @@ export async function withAuth(server: FastifyInstance) { } // Allow this request to proceed. return; - }if (error) { + } + if (error) { message = error; } } catch (err: unknown) { @@ -172,10 +173,11 @@ export const onRequest = async ({ const authWallet = await getAuthWallet(); if (publicKey === (await authWallet.getAddress())) { return await handleAccessToken(jwt, req, getUser); - }if (publicKey === THIRDWEB_DASHBOARD_ISSUER) { + } + if (publicKey === THIRDWEB_DASHBOARD_ISSUER) { return await handleDashboardAuth(jwt); } - return await handleKeypairAuth({ jwt, req, publicKey }); + return await handleKeypairAuth({ jwt, req, publicKey }); } // Get the public key hash from the `kid` header. diff --git a/src/server/routes/configuration/auth/get.ts b/src/server/routes/configuration/auth/get.ts index 0b97d971e..115964dba 100644 --- a/src/server/routes/configuration/auth/get.ts +++ b/src/server/routes/configuration/auth/get.ts @@ -6,7 +6,9 @@ import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; export const responseBodySchema = Type.Object({ result: Type.Object({ - domain: Type.String(), + authDomain: Type.String(), + mtlsCertificate: Type.Union([Type.String(), Type.Null()]), + // Do not return mtlsPrivateKey. }), }); @@ -27,10 +29,12 @@ export async function getAuthConfiguration(fastify: FastifyInstance) { }, }, handler: async (_req, res) => { - const config = await getConfig(); + const { authDomain, mtlsCertificate } = await getConfig(); + res.status(StatusCodes.OK).send({ result: { - domain: config.authDomain, + authDomain, + mtlsCertificate, }, }); }, diff --git a/src/server/routes/configuration/auth/update.ts b/src/server/routes/configuration/auth/update.ts index 50d88a49d..3a1243679 100644 --- a/src/server/routes/configuration/auth/update.ts +++ b/src/server/routes/configuration/auth/update.ts @@ -5,10 +5,21 @@ import { updateConfiguration } from "../../../../shared/db/configuration/update- import { getConfig } from "../../../../shared/utils/cache/get-config"; import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; import { responseBodySchema } from "./get"; +import { createCustomError } from "../../../middleware/error"; +import { encrypt } from "../../../../shared/utils/crypto"; -export const requestBodySchema = Type.Object({ - domain: Type.String(), -}); +export const requestBodySchema = Type.Partial( + Type.Object({ + authDomain: Type.String(), + mtlsCertificate: Type.String({ + description: + "Engine certificate used for outbound mTLS requests. Must provide the full certificate chain.", + }), + mtlsPrivateKey: Type.String({ + description: "Engine private key used for outbound mTLS requests.", + }), + }), +); export async function updateAuthConfiguration(fastify: FastifyInstance) { fastify.route<{ @@ -29,15 +40,49 @@ export async function updateAuthConfiguration(fastify: FastifyInstance) { }, }, handler: async (req, res) => { + const { authDomain, mtlsCertificate, mtlsPrivateKey } = req.body; + + if (mtlsCertificate) { + if ( + !mtlsCertificate.includes("-----BEGIN CERTIFICATE-----\n") || + !mtlsCertificate.includes("\n-----END CERTIFICATE-----") + ) { + throw createCustomError( + "Invalid mtlsCertificate.", + StatusCodes.BAD_REQUEST, + "INVALID_MTLS_CERTIFICATE", + ); + } + } + if (mtlsPrivateKey) { + if ( + !mtlsPrivateKey.startsWith("-----BEGIN PRIVATE KEY-----\n") || + !mtlsPrivateKey.endsWith("\n-----END PRIVATE KEY-----") + ) { + throw createCustomError( + "Invalid mtlsPrivateKey.", + StatusCodes.BAD_REQUEST, + "INVALID_MTLS_PRIVATE_KEY", + ); + } + } + await updateConfiguration({ - authDomain: req.body.domain, + authDomain, + mtlsCertificateEncrypted: mtlsCertificate + ? encrypt(mtlsCertificate) + : undefined, + mtlsPrivateKeyEncrypted: mtlsPrivateKey + ? encrypt(mtlsPrivateKey) + : undefined, }); const config = await getConfig(false); res.status(StatusCodes.OK).send({ result: { - domain: config.authDomain, + authDomain: config.authDomain, + mtlsCertificate: config.mtlsCertificate, }, }); }, diff --git a/src/server/routes/webhooks/create.ts b/src/server/routes/webhooks/create.ts index 3d18768f8..a45e66a00 100644 --- a/src/server/routes/webhooks/create.ts +++ b/src/server/routes/webhooks/create.ts @@ -45,7 +45,7 @@ export async function createWebhookRoute(fastify: FastifyInstance) { method: "POST", url: "/webhooks/create", schema: { - summary: "Create a webhook", + summary: "Create webhook", description: "Create a webhook to call when a specific Engine event occurs.", tags: ["Webhooks"], diff --git a/src/shared/db/configuration/get-configuration.ts b/src/shared/db/configuration/get-configuration.ts index ee46e3a3b..71776b87b 100644 --- a/src/shared/db/configuration/get-configuration.ts +++ b/src/shared/db/configuration/get-configuration.ts @@ -172,6 +172,12 @@ const toParsedConfig = async (config: Configuration): Promise => { gcp: gcpWalletConfiguration, legacyWalletType_removeInNextBreakingChange, }, + mtlsCertificate: config.mtlsCertificateEncrypted + ? decrypt(config.mtlsCertificateEncrypted, env.ENCRYPTION_PASSWORD) + : null, + mtlsPrivateKey: config.mtlsPrivateKeyEncrypted + ? decrypt(config.mtlsPrivateKeyEncrypted, env.ENCRYPTION_PASSWORD) + : null, }; }; diff --git a/src/shared/db/configuration/update-configuration.ts b/src/shared/db/configuration/update-configuration.ts index f6d14fd47..4e153cde9 100644 --- a/src/shared/db/configuration/update-configuration.ts +++ b/src/shared/db/configuration/update-configuration.ts @@ -3,7 +3,7 @@ import { encrypt } from "../../utils/crypto"; import { prisma } from "../client"; export const updateConfiguration = async ( - data: Prisma.ConfigurationUpdateArgs["data"], + data: Prisma.ConfigurationUpdateInput, ) => { return prisma.configuration.update({ where: { diff --git a/src/shared/db/webhooks/create-webhook.ts b/src/shared/db/webhooks/create-webhook.ts index 0b3ebb151..ee07d770f 100644 --- a/src/shared/db/webhooks/create-webhook.ts +++ b/src/shared/db/webhooks/create-webhook.ts @@ -1,5 +1,5 @@ import type { Webhooks } from "@prisma/client"; -import { createHash, randomBytes } from "crypto"; +import { createHash, randomBytes } from "node:crypto"; import type { WebhooksEventTypes } from "../../schemas/webhooks"; import { prisma } from "../client"; diff --git a/src/shared/schemas/config.ts b/src/shared/schemas/config.ts index fc3ce2d62..6205c3816 100644 --- a/src/shared/schemas/config.ts +++ b/src/shared/schemas/config.ts @@ -33,6 +33,8 @@ export interface ParsedConfig | "gcpApplicationCredentialEmail" | "gcpApplicationCredentialPrivateKey" | "contractSubscriptionsRetryDelaySeconds" + | "mtlsCertificateEncrypted" + | "mtlsPrivateKeyEncrypted" > { walletConfiguration: { aws: AwsWalletConfiguration | null; @@ -41,4 +43,6 @@ export interface ParsedConfig }; contractSubscriptionsRequeryDelaySeconds: string; chainOverridesParsed: Chain[]; + mtlsCertificate: string | null; + mtlsPrivateKey: string | null; } diff --git a/src/shared/utils/crypto.ts b/src/shared/utils/crypto.ts index 4086d4a9e..a6e8c4e8a 100644 --- a/src/shared/utils/crypto.ts +++ b/src/shared/utils/crypto.ts @@ -1,20 +1,20 @@ -import crypto from "crypto"; import CryptoJS from "crypto-js"; +import crypto from "node:crypto"; import { env } from "./env"; -export const encrypt = (data: string): string => { +export function encrypt(data: string): string { return CryptoJS.AES.encrypt(data, env.ENCRYPTION_PASSWORD).toString(); -}; +} -export const decrypt = (data: string, password: string) => { +export function decrypt(data: string, password: string) { return CryptoJS.AES.decrypt(data, password).toString(CryptoJS.enc.Utf8); -}; +} -export const isWellFormedPublicKey = (key: string) => { +export function isWellFormedPublicKey(key: string) { try { crypto.createPublicKey(key); return true; } catch (_e) { return false; } -}; +} diff --git a/src/shared/utils/custom-auth-header.ts b/src/shared/utils/custom-auth-header.ts new file mode 100644 index 000000000..ac5fd54f5 --- /dev/null +++ b/src/shared/utils/custom-auth-header.ts @@ -0,0 +1,63 @@ +import { createHmac } from "node:crypto"; + +/** + * Generates an HMAC-256 secret to set in the "Authorization" header. + * + * @param webhookUrl - The URL to call. + * @param body - The request body. + * @param timestamp - The request timestamp. + * @param nonce - A unique string for this request. Should not be re-used. + * @param clientId - Your application's client id. + * @param clientSecret - Your application's client secret. + * @returns + */ +export const generateSecretHmac256 = (args: { + webhookUrl: string; + body: Record; + timestamp: Date; + nonce: string; + clientId: string; + clientSecret: string; +}): string => { + const { webhookUrl, body, timestamp, nonce, clientId, clientSecret } = args; + + // Create the body hash by hashing the payload. + const bodyHash = createHmac("sha256", clientSecret) + .update(JSON.stringify(body), "utf8") + .digest("base64"); + + // Create the signature hash by hashing the signature. + const ts = timestamp.getTime(); // timestamp expected in milliseconds + const httpMethod = "POST"; + const url = new URL(webhookUrl); + const resourcePath = url.pathname; + const host = url.hostname; + const port = url.port + ? Number.parseInt(url.port) + : url.protocol === "https:" + ? 443 + : 80; + + const signature = [ + ts, + nonce, + httpMethod, + resourcePath, + host, + port, + bodyHash, + "", // to insert a newline at the end + ].join("\n"); + + const signatureHash = createHmac("sha256", clientSecret) + .update(signature, "utf8") + .digest("base64"); + + return [ + `MAC id="${clientId}"`, + `ts="${ts}"`, + `nonce="${nonce}"`, + `bodyhash="${bodyHash}"`, + `mac="${signatureHash}"`, + ].join(","); +}; diff --git a/src/shared/utils/env.ts b/src/shared/utils/env.ts index 3fa84dcc2..c048630d1 100644 --- a/src/shared/utils/env.ts +++ b/src/shared/utils/env.ts @@ -6,18 +6,6 @@ import { z } from "zod"; const path = process.env.NODE_ENV === "test" ? ".env.test" : ".env"; dotenv.config({ path }); -export const JsonSchema = z.string().refine( - (value) => { - try { - JSON.parse(value); - return true; - } catch { - return false; - } - }, - { message: "Invalid JSON string" }, -); - const boolEnvSchema = (defaultBool: boolean) => z .string() @@ -68,7 +56,6 @@ export const env = createEnv({ .default("https://c.thirdweb.com/event"), SDK_BATCH_TIME_LIMIT: z.coerce.number().default(0), SDK_BATCH_SIZE_LIMIT: z.coerce.number().default(100), - ENABLE_KEYPAIR_AUTH: boolEnvSchema(false), REDIS_URL: z.string(), SEND_TRANSACTION_QUEUE_CONCURRENCY: z.coerce.number().default(200), CONFIRM_TRANSACTION_QUEUE_CONCURRENCY: z.coerce.number().default(200), @@ -99,6 +86,11 @@ export const env = createEnv({ // Sets the number of recent nonces to map to queue IDs. NONCE_MAP_COUNT: z.coerce.number().default(10_000), + ENABLE_KEYPAIR_AUTH: boolEnvSchema(false), + ENABLE_CUSTOM_HMAC_AUTH: boolEnvSchema(false), + CUSTOM_HMAC_AUTH_CLIENT_ID: z.string().optional(), + CUSTOM_HMAC_AUTH_CLIENT_SECRET: z.string().optional(), + /** * Experimental env vars. These may be renamed or removed in future non-major releases. */ @@ -130,7 +122,6 @@ export const env = createEnv({ CLIENT_ANALYTICS_URL: process.env.CLIENT_ANALYTICS_URL, SDK_BATCH_TIME_LIMIT: process.env.SDK_BATCH_TIME_LIMIT, SDK_BATCH_SIZE_LIMIT: process.env.SDK_BATCH_SIZE_LIMIT, - ENABLE_KEYPAIR_AUTH: process.env.ENABLE_KEYPAIR_AUTH, REDIS_URL: process.env.REDIS_URL, SEND_TRANSACTION_QUEUE_CONCURRENCY: process.env.SEND_TRANSACTION_QUEUE_CONCURRENCY, @@ -150,6 +141,10 @@ export const env = createEnv({ process.env.EXPERIMENTAL__MAX_GAS_PRICE_WEI, METRICS_PORT: process.env.METRICS_PORT, METRICS_ENABLED: process.env.METRICS_ENABLED, + ENABLE_KEYPAIR_AUTH: process.env.ENABLE_KEYPAIR_AUTH, + ENABLE_CUSTOM_HMAC_AUTH: process.env.ENABLE_CUSTOM_HMAC_AUTH, + CUSTOM_HMAC_AUTH_CLIENT_ID: process.env.CUSTOM_HMAC_AUTH_CLIENT_ID, + CUSTOM_HMAC_AUTH_CLIENT_SECRET: process.env.CUSTOM_HMAC_AUTH_CLIENT_SECRET, }, onValidationError: (error: ZodError) => { console.error( diff --git a/src/shared/utils/webhook.ts b/src/shared/utils/webhook.ts index c31b3e1af..1997502e6 100644 --- a/src/shared/utils/webhook.ts +++ b/src/shared/utils/webhook.ts @@ -1,43 +1,70 @@ import type { Webhooks } from "@prisma/client"; -import crypto from "node:crypto"; +import assert from "node:assert"; +import crypto, { randomUUID } from "node:crypto"; +import { Agent, fetch } from "undici"; +import { getConfig } from "./cache/get-config"; +import { env } from "./env"; import { prettifyError } from "./error"; +import { generateSecretHmac256 } from "./custom-auth-header"; -export const generateSignature = ( +function generateSignature( body: Record, - timestamp: string, + timestampSeconds: number, secret: string, -): string => { +): string { const _body = JSON.stringify(body); - const payload = `${timestamp}.${_body}`; + const payload = `${timestampSeconds}.${_body}`; return crypto.createHmac("sha256", secret).update(payload).digest("hex"); -}; +} -export const createWebhookRequestHeaders = async ( - webhook: Webhooks, - body: Record, -): Promise => { - const headers: { - Accept: string; - "Content-Type": string; - Authorization?: string; - "x-engine-signature"?: string; - "x-engine-timestamp"?: string; - } = { - Accept: "application/json", - "Content-Type": "application/json", - }; +function generateAuthorization(args: { + webhook: Webhooks; + timestamp: Date; + body: Record; +}): string { + const { webhook, timestamp, body } = args; - if (webhook.secret) { - const timestamp = Math.floor(Date.now() / 1000).toString(); - const signature = generateSignature(body, timestamp, webhook.secret); + if (env.ENABLE_CUSTOM_HMAC_AUTH) { + assert( + env.CUSTOM_HMAC_AUTH_CLIENT_ID, + 'Missing "CUSTOM_HMAC_AUTH_CLIENT_ID".', + ); + assert( + env.CUSTOM_HMAC_AUTH_CLIENT_SECRET, + 'Missing "CUSTOM_HMAC_AUTH_CLIENT_SECRET"', + ); - headers.Authorization = `Bearer ${webhook.secret}`; - headers["x-engine-signature"] = signature; - headers["x-engine-timestamp"] = timestamp; + return generateSecretHmac256({ + webhookUrl: webhook.url, + body, + timestamp, + nonce: randomUUID(), + clientId: env.CUSTOM_HMAC_AUTH_CLIENT_ID, + clientSecret: env.CUSTOM_HMAC_AUTH_CLIENT_SECRET, + }); } - return headers; -}; + return `Bearer ${webhook.secret}`; +} + +export function generateRequestHeaders(args: { + webhook: Webhooks; + body: Record; + timestamp: Date; +}): HeadersInit { + const { webhook, body, timestamp } = args; + + const timestampSeconds = Math.floor(timestamp.getTime() / 1000); + const signature = generateSignature(body, timestampSeconds, webhook.secret); + const authorization = generateAuthorization({ webhook, timestamp, body }); + return { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: authorization, + "x-engine-signature": signature, + "x-engine-timestamp": timestampSeconds.toString(), + }; +} export interface WebhookResponse { ok: boolean; @@ -50,11 +77,32 @@ export const sendWebhookRequest = async ( body: Record, ): Promise => { try { - const headers = await createWebhookRequestHeaders(webhook, body); + const config = await getConfig(); + + // If mTLS is enabled, provide the certificate with this request. + const dispatcher = + config.mtlsCertificate && config.mtlsPrivateKey + ? new Agent({ + connect: { + cert: config.mtlsCertificate, + key: config.mtlsPrivateKey, + // Validate the server's certificate. + rejectUnauthorized: true, + }, + }) + : undefined; + + const headers = generateRequestHeaders({ + webhook, + body, + timestamp: new Date(), + }); + const resp = await fetch(webhook.url, { method: "POST", headers: headers, body: JSON.stringify(body), + dispatcher, }); return { diff --git a/tests/unit/webhook.test.ts b/tests/unit/webhook.test.ts new file mode 100644 index 000000000..adc0d5dc0 --- /dev/null +++ b/tests/unit/webhook.test.ts @@ -0,0 +1,57 @@ +import type { Webhooks } from "@prisma/client"; +import { describe, expect, it } from "vitest"; +import { WebhooksEventTypes } from "../../src/shared/schemas/webhooks"; +import { generateRequestHeaders } from "../../src/shared/utils/webhook"; +import { generateSecretHmac256 } from "../../src/shared/utils/customAuthHeader"; + +describe("generateSecretHmac256", () => { + it("should generate a valid MAC header with correct structure and values", () => { + const timestamp = new Date("2024-01-01"); + const nonce = "6b98df0d-5f33-4121-96cb-77a0b9df2bbe"; + + const result = generateSecretHmac256({ + webhookUrl: "https://example.com/webhook", + body: { bodyArgName: "bodyArgValue" }, + timestamp, + nonce, + clientId: "testClientId", + clientSecret: "testClientSecret", + }); + + expect(result).toEqual( + `MAC id="testClientId" ts="1704067200000" nonce="6b98df0d-5f33-4121-96cb-77a0b9df2bbe" bodyhash="4Mknknli8NGCwC28djVf/Qa8vN3wtvfeRGKVha0MgjQ=" mac="Qbe9H5yeVvywoL3l1RFLBDC0YvDOCQnytNSlbTWXzEk="`, + ); + }); +}); + +describe("generateRequestHeaders", () => { + const webhook: Webhooks = { + id: 42, + name: "test webhook", + url: "https://www.example.com/webhook", + secret: "test-secret-string", + eventType: WebhooksEventTypes.SENT_TX, + createdAt: new Date(), + updatedAt: new Date(), + revokedAt: null, + }; + const body = { + name: "Alice", + age: 25, + occupation: ["Founder", "Developer"], + }; + const timestamp = new Date("2024-01-01"); + + it("Generate a consistent webhook header", () => { + const result = generateRequestHeaders({ webhook, body, timestamp }); + + expect(result).toEqual({ + Accept: "application/json", + Authorization: "Bearer test-secret-string", + "Content-Type": "application/json", + "x-engine-signature": + "ca272da65f1145b9cfadab6d55086ee458eccc03a2c5f7f5ea84094d95b219cc", + "x-engine-timestamp": "1704067200", + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 79a69d912..55f064b9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2372,7 +2372,7 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/curves@1.4.2": +"@noble/curves@1.4.2", "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== @@ -2386,13 +2386,6 @@ dependencies: "@noble/hashes" "1.6.0" -"@noble/curves@~1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" - integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== - dependencies: - "@noble/hashes" "1.5.0" - "@noble/hashes@1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" @@ -2403,11 +2396,6 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== -"@noble/hashes@1.5.0", "@noble/hashes@~1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" - integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== - "@noble/hashes@1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5" @@ -3274,7 +3262,7 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== -"@scure/base@~1.1.0", "@scure/base@~1.1.2", "@scure/base@~1.1.6", "@scure/base@~1.1.7": +"@scure/base@~1.1.0", "@scure/base@~1.1.2", "@scure/base@~1.1.6": version "1.1.9" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== @@ -10550,6 +10538,11 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== +undici@^6.20.1: + version "6.20.1" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.20.1.tgz#fbb87b1e2b69d963ff2d5410a40ffb4c9e81b621" + integrity sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA== + unenv@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.10.0.tgz#c3394a6c6e4cfe68d699f87af456fe3f0db39571"