AtlatestRepositorysigil-lemonsqueezy
1;;; (lemonsqueezy license) - License key management.
2;;;
3;;; Two separate APIs:
4;;; 1. Main API (requires Bearer auth) — list/retrieve/update license keys
5;;; 2. License API (no auth needed) — validate/activate/deactivate keys
6;;;
7;;; The License API is designed for client applications and uses different
8;;; headers (application/json, application/x-www-form-urlencoded).
9
10(define-library (lemonsqueezy license)
11 (import (sigil core)
12 (sigil dict)
13 (sigil string)
14 (sigil struct)
15 (sigil json)
16 (only (sigil http) url-encode-value)
17 (sigil http client)
18 (lemonsqueezy))
20 (export ;; Records
21 ls-license-key
22 ls-license-key?
23 ls-license-key-id
24 ls-license-key-store-id
25 ls-license-key-customer-id
26 ls-license-key-order-id
27 ls-license-key-product-id
28 ls-license-key-key
29 ls-license-key-key-short
30 ls-license-key-activation-limit
31 ls-license-key-instances-count
32 ls-license-key-disabled
33 ls-license-key-status
34 ls-license-key-expires-at
35 ls-license-key-created-at
36 ls-license-key-updated-at
38 ls-license-validation
39 ls-license-validation?
40 ls-license-validation-valid
41 ls-license-validation-error
42 ls-license-validation-license-key
43 ls-license-validation-instance
44 ls-license-validation-meta
46 ls-license-activation
47 ls-license-activation?
48 ls-license-activation-activated
49 ls-license-activation-error
50 ls-license-activation-license-key
51 ls-license-activation-instance
52 ls-license-activation-meta
54 ;; Parsing
55 parse-license-key
56 parse-license-validation
57 parse-license-activation
59 ;; Main API functions (requires auth)
60 ls-license-keys
61 ls-license-key-get
62 ls-license-key-update
64 ;; License API functions (no auth required)
65 ls-validate-license
66 ls-activate-license
67 ls-deactivate-license)
69 (begin
71 ;; ---------------------------------------------------------------
72 ;; Records
73 ;; ---------------------------------------------------------------
75 (define-struct ls-license-key
76 (id)
77 (store-id default: #f)
78 (customer-id default: #f)
79 (order-id default: #f)
80 (product-id default: #f)
81 (key default: "")
82 (key-short default: "")
83 (activation-limit default: 0)
84 (instances-count default: 0)
85 (disabled default: #f)
86 (status default: "inactive")
87 (expires-at default: #f)
88 (created-at default: #f)
89 (updated-at default: #f))
91 (define-struct ls-license-validation
92 (valid default: #f)
93 (error default: #f)
94 (license-key default: #{})
95 (instance default: #f)
96 (meta default: #{}))
98 (define-struct ls-license-activation
99 (activated default: #f)
100 (error default: #f)
101 (license-key default: #{})
102 (instance default: #f)
103 (meta default: #{}))
105 ;; ---------------------------------------------------------------
106 ;; Parsing
107 ;; ---------------------------------------------------------------
109 ;;; Parse a JSON:API license key resource into an ls-license-key record.
110 (define (parse-license-key resource)
111 (ls-license-key
112 id: (jsonapi-id resource)
113 store-id: (jsonapi-attr resource store_id: #f)
114 customer-id: (jsonapi-attr resource customer_id: #f)
115 order-id: (jsonapi-attr resource order_id: #f)
116 product-id: (jsonapi-attr resource product_id: #f)
117 key: (jsonapi-attr resource key: "")
118 key-short: (jsonapi-attr resource key_short: "")
119 activation-limit: (jsonapi-attr resource activation_limit: 0)
120 instances-count: (jsonapi-attr resource instances_count: 0)
121 disabled: (jsonapi-attr resource disabled: #f)
122 status: (jsonapi-attr resource status: "inactive")
123 expires-at: (jsonapi-attr resource expires_at: #f)
124 created-at: (jsonapi-attr resource created_at: #f)
125 updated-at: (jsonapi-attr resource updated_at: #f)))
127 ;;; Normalize a dict field: null → empty dict, missing → empty dict.
128 (define (dict-or-empty data key)
129 (let ((v (dict-ref data key #f)))
130 (if (or (not v) (eq? v 'null)) #{} v)))
132 ;;; Parse a License API validation response.
133 (define (parse-license-validation data)
134 (ls-license-validation
135 valid: (dict-ref data valid: #f)
136 error: (maybe-null (dict-ref data error: #f))
137 license-key: (dict-or-empty data license_key:)
138 instance: (maybe-null (dict-ref data instance: #f))
139 meta: (dict-or-empty data meta:)))
141 ;;; Parse a License API activation/deactivation response.
142 (define (parse-license-activation data)
143 (ls-license-activation
144 activated: (dict-ref data activated: #f)
145 error: (maybe-null (dict-ref data error: #f))
146 license-key: (dict-or-empty data license_key:)
147 instance: (maybe-null (dict-ref data instance: #f))
148 meta: (dict-or-empty data meta:)))
150 ;; ---------------------------------------------------------------
151 ;; License API helpers (no Bearer auth)
152 ;; ---------------------------------------------------------------
154 ;;; The License API uses different headers than the main API.
155 (define license-api-headers
156 #{ accept: "application/json"
157 content-type: "application/x-www-form-urlencoded" })
159 ;;; License API base URL (hardcoded; does not use client base-url
160 ;;; since these endpoints require no authentication).
161 (define license-api-base "https://api.lemonsqueezy.com")
163 ;;; Build a form-urlencoded body from an alist of (key . value) pairs.
164 (define (form-encode-body params)
165 (string-join
166 (map (lambda (p)
167 (string-append (url-encode-value (car p))
168 "="
169 (url-encode-value (cdr p))))
170 (filter (lambda (p) (cdr p)) params))
171 "&"))
173 ;;; Make a POST request to the License API (form-urlencoded, no auth).
174 (define (license-post url params)
175 (let* ((body (form-encode-body params))
176 (response (http-post url body headers: license-api-headers)))
177 (if (not (http-response? response))
178 (error "License API request failed: no response"))
179 (let ((status (http-response-status response))
180 (resp-body (http-response-body response)))
181 (if (>= status 500)
182 (error (string-append "License API server error "
183 (number->string status))))
184 (if (and resp-body (not (string=? resp-body "")))
185 (json-decode resp-body)
186 #{}))))
188 ;; ---------------------------------------------------------------
189 ;; Main API functions (requires auth)
190 ;; ---------------------------------------------------------------
192 ;;; List license keys with optional filtering and pagination.
193 (define (ls-license-keys client . rest)
194 (apply ls-list-endpoint client parse-license-key '("v1" "license-keys") rest))
196 ;;; Get a single license key by ID (main API).
197 (define (ls-license-key-get client license-key-id)
198 (ls-get-endpoint client parse-license-key '("v1" "license-keys") license-key-id))
200 ;;; Update a license key.
201 ;;; updates is a dict of attributes (e.g., activation_limit:, disabled:, expires_at:).
202 ;;; Returns an ls-license-key record.
203 (define (ls-license-key-update client license-key-id updates)
204 (let* ((body #{ data:
205 #{ type: "license-keys"
206 id: license-key-id
207 attributes: updates } })
208 (url (ls-api-url client "v1" "license-keys" license-key-id))
209 (response (ls-patch/json client url body)))
210 (parse-license-key (jsonapi-data response))))
212 ;; ---------------------------------------------------------------
213 ;; License API functions (no auth required)
214 ;; ---------------------------------------------------------------
216 ;;; Validate a license key.
217 ;;; license-key: the full license key string
218 ;;; instance-id: optional instance UUID to validate a specific activation
219 ;;;
220 ;;; Returns an ls-license-validation record.
221 ;;; Rate limit: 60 requests/minute.
222 (define (ls-validate-license license-key . rest)
223 (let* ((instance-id (if (null? rest) #f (car rest)))
224 (params (list (cons "license_key" license-key)
225 (cons "instance_id" instance-id)))
226 (url (string-append license-api-base "/v1/licenses/validate"))
227 (data (license-post url params)))
228 (parse-license-validation data)))
230 ;;; Activate a license key.
231 ;;; license-key: the full license key string
232 ;;; instance-name: label for this activation (e.g., "example.com")
233 ;;;
234 ;;; Returns an ls-license-activation record.
235 ;;; Rate limit: 60 requests/minute.
236 (define (ls-activate-license license-key instance-name)
237 (let* ((params (list (cons "license_key" license-key)
238 (cons "instance_name" instance-name)))
239 (url (string-append license-api-base "/v1/licenses/activate"))
240 (data (license-post url params)))
241 (parse-license-activation data)))
243 ;;; Deactivate a license key.
244 ;;; license-key: the full license key string
245 ;;; instance-id: the UUID of the instance to deactivate
246 ;;;
247 ;;; Returns an ls-license-activation record (with deactivated: #t).
248 ;;; Rate limit: 60 requests/minute.
249 (define (ls-deactivate-license license-key instance-id)
250 (let* ((params (list (cons "license_key" license-key)
251 (cons "instance_id" instance-id)))
252 (url (string-append license-api-base "/v1/licenses/deactivate"))
253 (data (license-post url params)))
254 (parse-license-activation data)))
256 ))