AtlatestRepositorysigil-lemonsqueezy
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-level
7;;; meta field containing event_name and custom_data.
8
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 ;; Records
19 ls-webhook
20 ls-webhook?
21 ls-webhook-id
22 ls-webhook-url
23 ls-webhook-events
24 ls-webhook-store-id
25 ls-webhook-test-mode
26 ls-webhook-created-at
27 ls-webhook-updated-at
29 ls-webhook-event
30 ls-webhook-event?
31 ls-webhook-event-name
32 ls-webhook-event-data
33 ls-webhook-event-custom-data
34 ls-webhook-event-meta
36 ;; Parsing
37 parse-webhook
38 parse-webhook-event
40 ;; Verification
41 ls-verify-webhook
43 ;; API functions
44 ls-create-webhook
45 ls-webhooks
46 ls-webhook-get
47 ls-webhook-update
48 ls-webhook-delete)
50 (begin
52 ;; ---------------------------------------------------------------
53 ;; Records
54 ;; ---------------------------------------------------------------
56 (define-struct ls-webhook
57 (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-event
66 (name)
67 (data default: #{})
68 (custom-data default: #{})
69 (meta default: #{}))
71 ;; ---------------------------------------------------------------
72 ;; Parsing
73 ;; ---------------------------------------------------------------
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-webhook
79 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-raw
85 '()))
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-event
104 name: event-name
105 data: data
106 custom-data: custom-data
107 meta: meta)))
109 ;; ---------------------------------------------------------------
110 ;; Verification
111 ;; ---------------------------------------------------------------
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 functions
127 ;; ---------------------------------------------------------------
129 ;;; Create a webhook endpoint.
130 ;;;
131 ;;; store-id: the store ID (string)
132 ;;; url: the webhook URL to receive events
133 ;;; 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-url
142 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-id
166 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 ))