Skip to content

SDK's missing types for incoming Webhooks Requests #140

@Chinoman10

Description

@Chinoman10

I'd like to have strong types on our API for webhook requests that come from Lemon Squeezy.
I was surprised to find out that the SDK is only meant to execute requests, not handle incoming ones.

Not only did I have to write (and fix!) the signature verification (the docs example didn't work for me), but I'm having a hard time with matching the types.
Example:
On the subscription_created event, I get the following object:

{
  "meta": {
    "event_name": "subscription_created",
    "custom_data": {
      // some custom data, if any
    }
  },
  "data": {
    "type": "subscription",
    "id": "1",
    "attributes": {
// ...
  }
}

But the 'Subscription' exported type from the SDK is:

type Subscription = {
    jsonapi: {
        version: string;
    };
    links: Pick<Links, "self">;
    data: SubscriptionData;
    included?: Data<Record<string, unknown>, unknown>[] | undefined;
}

But 'SubscriptionData' isn't even an exported type, so I can't import it in my project and use that type to define what my webhook endpoint receives (in addition to the 'meta' object).


As for the verification of signed requests, the docs show the following Node example:

import crypto from "node:crypto";

const secret = 'SIGNING_SECRET';
const hmac = crypto.createHmac('sha256', secret);
const digest = Buffer.from(hmac.update(request.rawBody).digest('hex'), 'utf8');
const signature = Buffer.from(request.get('X-Signature') || '', 'utf8');

if (!crypto.timingSafeEqual(digest, signature)) {
    throw new Error('Invalid signature.');
}

But I had to change it to the following due to TS errors (ignore the diff. structure):

import crypto from 'node:crypto';

export default function verifySignature(
  secret: string,
  signature_header: string | undefined,
  payload: string,
): boolean {
  const hmac = crypto.createHmac('sha256', secret);
  const digest = Uint8Array.from(
    Buffer.from(hmac.update(payload).digest('hex'), 'utf8'),
  );

  const signature = Uint8Array.from(Buffer.from(signature_header || '', 'utf8'));

  if (
    digest.length !== signature.length ||
    !crypto.timingSafeEqual(digest, signature)
  ) {
    return false;
  }

  return true;
}

Essentially, Buffer.from needed to be wrapped in an Uint8Array.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions