AtlatestRepositorysigil-lemonsqueezy
sigil-lemonsqueezy / tree / src / lemonsqueezywebhook.sgl
1
;;; (lemonsqueezy webhook) - Webhook management and verification.2
;;;3
;;; Handles HMAC-SHA256 signature verification of incoming webhooks,4
;;; parsing of webhook payloads, and CRUD operations for webhook endpoints.5
;;;6
;;; Webhook payloads use a modified JSON:API format with a top-level7
;;; meta field containing event_name and custom_data.9
(define-library (lemonsqueezy webhook)10
(import (sigil core)11
(sigil dict)12
(sigil string)13
(sigil struct)14
(sigil json)15
(sigil crypto)16
(lemonsqueezy))18
(export ;; Records19
ls-webhook20
ls-webhook?21
ls-webhook-id22
ls-webhook-url23
ls-webhook-events24
ls-webhook-store-id25
ls-webhook-test-mode26
ls-webhook-created-at27
ls-webhook-updated-at29
ls-webhook-event30
ls-webhook-event?31
ls-webhook-event-name32
ls-webhook-event-data33
ls-webhook-event-custom-data34
ls-webhook-event-meta36
;; Parsing37
parse-webhook38
parse-webhook-event40
;; Verification41
ls-verify-webhook43
;; API functions44
ls-create-webhook45
ls-webhooks46
ls-webhook-get47
ls-webhook-update48
ls-webhook-delete)50
(begin52
;; ---------------------------------------------------------------53
;; Records54
;; ---------------------------------------------------------------56
(define-struct ls-webhook57
(id)58
(url default: "")59
(events default: '())60
(store-id default: #f)61
(test-mode default: #f)62
(created-at default: #f)63
(updated-at default: #f))65
(define-struct ls-webhook-event66
(name)67
(data default: #{})68
(custom-data default: #{})69
(meta default: #{}))71
;; ---------------------------------------------------------------72
;; Parsing73
;; ---------------------------------------------------------------75
;;; Parse a JSON:API webhook resource into an ls-webhook record.76
(define (parse-webhook resource)77
(let ((events-raw (jsonapi-attr resource events: #f)))78
(ls-webhook79
id: (jsonapi-id resource)80
url: (jsonapi-attr resource url: "")81
events: (if (and events-raw (array? events-raw))82
(array->list events-raw)83
(if (and events-raw (list? events-raw))84
events-raw85
'()))86
store-id: (jsonapi-attr resource store_id: #f)87
test-mode: (jsonapi-attr resource test_mode: #f)88
created-at: (jsonapi-attr resource created_at: #f)89
updated-at: (jsonapi-attr resource updated_at: #f))))91
;;; Parse a raw webhook payload (as a JSON string or parsed dict)92
;;; into an ls-webhook-event record.93
;;;94
;;; The payload has the structure:95
;;; { meta: { event_name, custom_data }, data: { type, id, attributes, ... } }96
(define (parse-webhook-event payload)97
(let* ((parsed (if (string? payload) (json-decode payload) payload))98
(meta (dict-ref parsed meta: #{}))99
(event-name (dict-ref meta event_name: ""))100
(custom-data (let ((cd (dict-ref meta custom_data: #f)))101
(if (or (not cd) (eq? cd 'null)) #{} cd)))102
(data (dict-ref parsed data: #{})))103
(ls-webhook-event104
name: event-name105
data: data106
custom-data: custom-data107
meta: meta)))109
;; ---------------------------------------------------------------110
;; Verification111
;; ---------------------------------------------------------------113
;;; Verify a webhook signature using HMAC-SHA256.114
;;;115
;;; secret: the webhook signing secret (string)116
;;; raw-body: the raw request body (string, NOT parsed JSON)117
;;; signature: the X-Signature header value (hex string)118
;;;119
;;; Returns #t if the signature is valid, #f otherwise.120
;;; Uses timing-safe comparison to prevent timing attacks.121
(define (ls-verify-webhook secret raw-body signature)122
(let ((computed (hmac-sha256 secret raw-body)))123
(timing-safe-equal? computed signature)))125
;; ---------------------------------------------------------------126
;; API functions127
;; ---------------------------------------------------------------129
;;; Create a webhook endpoint.130
;;;131
;;; store-id: the store ID (string)132
;;; url: the webhook URL to receive events133
;;; events: list of event name strings (e.g., '("order_created" "subscription_created"))134
;;; secret: signing secret for HMAC verification (6-40 chars recommended)135
;;;136
;;; Returns an ls-webhook record.137
(define (ls-create-webhook client store-id webhook-url events secret)138
(let* ((body #{ data:139
#{ type: "webhooks"140
attributes:141
#{ url: webhook-url142
events: (list->array events)143
secret: secret }144
relationships:145
#{ store:146
#{ data: #{ type: "stores" id: store-id } } } } })147
(api-url (ls-api-url client "v1" "webhooks"))148
(response (ls-post/json client api-url body)))149
(parse-webhook (jsonapi-data response))))151
;;; List webhooks with optional filtering and pagination.152
(define (ls-webhooks client . rest)153
(apply ls-list-endpoint client parse-webhook '("v1" "webhooks") rest))155
;;; Get a single webhook by ID.156
(define (ls-webhook-get client webhook-id)157
(ls-get-endpoint client parse-webhook '("v1" "webhooks") webhook-id))159
;;; Update a webhook.160
;;; updates is a dict of attributes to change (e.g., url:, events:, secret:).161
;;; Returns an ls-webhook record.162
(define (ls-webhook-update client webhook-id updates)163
(let* ((body #{ data:164
#{ type: "webhooks"165
id: webhook-id166
attributes: updates } })167
(url (ls-api-url client "v1" "webhooks" webhook-id))168
(response (ls-patch/json client url body)))169
(parse-webhook (jsonapi-data response))))171
;;; Delete a webhook by ID.172
(define (ls-webhook-delete client webhook-id)173
(let ((url (ls-api-url client "v1" "webhooks" webhook-id)))174
(ls-delete/json client url)175
#t))177
))