// verifySignature — verify an HMAC-SHA256 signature over a payload. // // Demonstrates: the Web Crypto API (`crypto.subtle`) — available on every // serverless host (Workers, Scaleway node, your own node) without any import — // wrapped in `Effect.promise`, plus controlling the HTTP status with // `ServerlessHttpError` (401 when the signature does not match). // // SIGNING_SECRET=s3cret voltro serverless dev functions/verifySignature.serverless.ts import { Effect, Schema } from 'effect' import { defineServerless, ServerlessHttpError } from '@voltro/serverless' const toHex = (buf: ArrayBuffer): string => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('') const hmacHex = async (secret: string, payload: string): Promise => { const enc = new TextEncoder() const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']) return toHex(await crypto.subtle.sign('HMAC', key, enc.encode(payload))) } // Constant-time-ish compare — avoid leaking match position via early return. const safeEqual = (a: string, b: string): boolean => { if (a.length !== b.length) return false let diff = 0 for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i) return diff === 0 } export default defineServerless({ name: 'verify-signature', method: 'POST', input: Schema.Struct({ payload: Schema.String, signature: Schema.String.pipe(Schema.pattern(/^[0-9a-f]+$/i)), }), output: Schema.Struct({ valid: Schema.Literal(true) }), handler: ({ payload, signature }, ctx) => Effect.gen(function* () { const secret = ctx.env.SIGNING_SECRET if (!secret) { return yield* Effect.fail( new ServerlessHttpError({ status: 503, message: 'signing-not-configured', detail: 'Set SIGNING_SECRET.' }), ) } const expected = yield* Effect.promise(() => hmacHex(secret, payload)) if (!safeEqual(expected, signature.toLowerCase())) { return yield* Effect.fail(new ServerlessHttpError({ status: 401, message: 'invalid-signature' })) } return { valid: true as const } }), })