@ophelio/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1098 @@
1
+ //#region src/core/errors.ts
2
+ var OphelioError = class extends Error {
3
+ /** HTTP status code, or null for non-HTTP failures (network, timeout). */
4
+ status;
5
+ /** AIP status string (e.g. 'NOT_FOUND'), or null when unavailable. */
6
+ code;
7
+ /** Raw `error.details` array from the response, when present. */
8
+ details;
9
+ /** The raw parsed response body, when present. */
10
+ raw;
11
+ constructor(message, options = {}) {
12
+ super(message);
13
+ this.name = this.constructor.name;
14
+ this.status = options.status ?? null;
15
+ this.code = options.code ?? null;
16
+ this.details = options.details;
17
+ this.raw = options.raw;
18
+ }
19
+ };
20
+ var InvalidArgumentError = class extends OphelioError {};
21
+ var UnauthenticatedError = class extends OphelioError {};
22
+ var PermissionDeniedError = class extends OphelioError {};
23
+ var NotFoundError = class extends OphelioError {};
24
+ var AlreadyExistsError = class extends OphelioError {};
25
+ var ResourceExhaustedError = class extends OphelioError {};
26
+ var InternalError = class extends OphelioError {};
27
+ var UnimplementedError = class extends OphelioError {};
28
+ var UnavailableError = class extends OphelioError {};
29
+ /** Request never reached the API or the response never arrived. */
30
+ var OphelioConnectionError = class extends OphelioError {};
31
+ /** The configured timeoutMs elapsed before the API responded. */
32
+ var OphelioTimeoutError = class extends OphelioError {};
33
+ /** X-Ophelio-Signature did not match the webhook payload. */
34
+ var WebhookSignatureVerificationError = class extends OphelioError {};
35
+ const AIP_CODE_TO_ERROR = {
36
+ INVALID_ARGUMENT: InvalidArgumentError,
37
+ UNAUTHENTICATED: UnauthenticatedError,
38
+ PERMISSION_DENIED: PermissionDeniedError,
39
+ NOT_FOUND: NotFoundError,
40
+ ALREADY_EXISTS: AlreadyExistsError,
41
+ RESOURCE_EXHAUSTED: ResourceExhaustedError,
42
+ INTERNAL: InternalError,
43
+ UNIMPLEMENTED: UnimplementedError,
44
+ UNAVAILABLE: UnavailableError
45
+ };
46
+ const HTTP_STATUS_TO_AIP = {
47
+ 400: "INVALID_ARGUMENT",
48
+ 401: "UNAUTHENTICATED",
49
+ 403: "PERMISSION_DENIED",
50
+ 404: "NOT_FOUND",
51
+ 409: "ALREADY_EXISTS",
52
+ 410: "NOT_FOUND",
53
+ 422: "INVALID_ARGUMENT",
54
+ 429: "RESOURCE_EXHAUSTED",
55
+ 500: "INTERNAL",
56
+ 501: "UNIMPLEMENTED",
57
+ 503: "UNAVAILABLE"
58
+ };
59
+ /**
60
+ * Build the most specific OphelioError for a non-2xx response. Prefers the
61
+ * AIP status string from the body; falls back to the HTTP status code when
62
+ * the body isn't a valid envelope (e.g. a proxy error page).
63
+ */
64
+ function errorFromResponse(httpStatus, bodyText) {
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(bodyText);
68
+ } catch {
69
+ parsed = void 0;
70
+ }
71
+ const envelope = parsed?.error;
72
+ const aipCode = typeof envelope?.status === "string" ? envelope.status : HTTP_STATUS_TO_AIP[httpStatus];
73
+ const message = typeof envelope?.message === "string" && envelope.message.length > 0 ? envelope.message : `Request failed with status ${httpStatus}.`;
74
+ return new (aipCode && AIP_CODE_TO_ERROR[aipCode] || OphelioError)(message, {
75
+ status: httpStatus,
76
+ code: aipCode ?? null,
77
+ details: Array.isArray(envelope?.details) ? envelope.details : void 0,
78
+ raw: parsed
79
+ });
80
+ }
81
+ //#endregion
82
+ //#region src/core/retry.ts
83
+ const BASE_DELAY_MS = 500;
84
+ const MAX_BACKOFF_MS = 8e3;
85
+ const MAX_RETRY_AFTER_MS = 3e4;
86
+ const RETRYABLE_STATUSES = new Set([
87
+ 429,
88
+ 502,
89
+ 503,
90
+ 504
91
+ ]);
92
+ function isRetryableStatus(status) {
93
+ return RETRYABLE_STATUSES.has(status);
94
+ }
95
+ /**
96
+ * Delay before retry attempt `attempt` (0-based). Exponential backoff with
97
+ * full jitter, unless the server sent a usable Retry-After (seconds or
98
+ * HTTP-date), which wins but is capped so a hostile header can't park the
99
+ * caller for minutes.
100
+ */
101
+ function backoffDelayMs(attempt, retryAfter) {
102
+ if (retryAfter) {
103
+ const seconds = Number(retryAfter);
104
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, MAX_RETRY_AFTER_MS);
105
+ const dateMs = Date.parse(retryAfter);
106
+ if (!Number.isNaN(dateMs)) return Math.min(Math.max(dateMs - Date.now(), 0), MAX_RETRY_AFTER_MS);
107
+ }
108
+ const cap = Math.min(MAX_BACKOFF_MS, BASE_DELAY_MS * 2 ** attempt);
109
+ return Math.random() * cap;
110
+ }
111
+ function sleep(ms) {
112
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
113
+ }
114
+ //#endregion
115
+ //#region src/core/transport.ts
116
+ const DEFAULT_BASE_URL = "https://api.ophel.io";
117
+ const DEFAULT_TIMEOUT_MS = 3e4;
118
+ const DEFAULT_MAX_RETRIES = 2;
119
+ var Transport = class {
120
+ apiKey;
121
+ baseUrl;
122
+ fetchFn;
123
+ headers;
124
+ timeoutMs;
125
+ maxRetries;
126
+ constructor(options) {
127
+ if (!options.apiKey && !options.headers) throw new OphelioError("Ophelio requires an apiKey (or custom headers carrying your own authentication).");
128
+ this.apiKey = options.apiKey;
129
+ this.baseUrl = (options.baseUrl ?? "https://api.ophel.io").replace(/\/+$/, "");
130
+ this.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
131
+ this.headers = options.headers ?? {};
132
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
133
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
134
+ }
135
+ async request(params) {
136
+ const url = this.buildUrl(params);
137
+ const headers = this.buildHeaders(params);
138
+ const maxAttempts = params.retryable ? this.maxRetries + 1 : 1;
139
+ const init = {
140
+ method: params.method,
141
+ headers,
142
+ ...params.body !== void 0 ? { body: JSON.stringify(params.body) } : {}
143
+ };
144
+ let lastError = new OphelioError("Request failed.");
145
+ let retryAfter = null;
146
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
147
+ if (attempt > 0) await sleep(backoffDelayMs(attempt - 1, retryAfter));
148
+ let response;
149
+ try {
150
+ response = await this.fetchFn(url, {
151
+ ...init,
152
+ signal: this.combineSignals(params.options?.signal)
153
+ });
154
+ } catch (error) {
155
+ lastError = this.normaliseFetchError(error, params.options?.signal);
156
+ if (params.options?.signal?.aborted) throw lastError;
157
+ if (attempt < maxAttempts - 1) continue;
158
+ throw lastError;
159
+ }
160
+ if (response.ok) return this.parseBody(response);
161
+ retryAfter = response.headers.get("retry-after");
162
+ const bodyText = await response.text().catch(() => "");
163
+ const error = errorFromResponse(response.status, bodyText);
164
+ if (params.retryable && isRetryableStatus(response.status) && attempt < maxAttempts - 1) {
165
+ lastError = error;
166
+ continue;
167
+ }
168
+ throw error;
169
+ }
170
+ throw lastError;
171
+ }
172
+ buildUrl(params) {
173
+ const search = new URLSearchParams();
174
+ for (const [key, value] of Object.entries(params.query ?? {})) if (value !== void 0) search.set(key, value);
175
+ const queryString = search.toString();
176
+ return `${this.baseUrl}${params.path}${queryString ? `?${queryString}` : ""}`;
177
+ }
178
+ buildHeaders(params) {
179
+ return {
180
+ Accept: "application/json",
181
+ ...params.body !== void 0 ? { "Content-Type": "application/json" } : {},
182
+ ...this.apiKey ? { "X-Api-Key": this.apiKey } : {},
183
+ ...this.headers,
184
+ ...params.idempotencyKey ? { "Idempotency-Key": params.idempotencyKey } : {},
185
+ ...params.options?.headers
186
+ };
187
+ }
188
+ combineSignals(callerSignal) {
189
+ const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
190
+ return callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal;
191
+ }
192
+ normaliseFetchError(error, callerSignal) {
193
+ if (callerSignal?.aborted) return new OphelioConnectionError("Request aborted by caller.", { raw: error });
194
+ if (error instanceof DOMException && error.name === "TimeoutError") return new OphelioTimeoutError(`Request timed out after ${this.timeoutMs} ms.`, { raw: error });
195
+ return new OphelioConnectionError(`Request failed to send: ${error instanceof Error ? error.message : String(error)}`, { raw: error });
196
+ }
197
+ async parseBody(response) {
198
+ const text = await response.text();
199
+ if (!text) return void 0;
200
+ try {
201
+ return JSON.parse(text);
202
+ } catch {
203
+ throw new OphelioError("API returned a non-JSON response body.", {
204
+ status: response.status,
205
+ raw: text
206
+ });
207
+ }
208
+ }
209
+ };
210
+ //#endregion
211
+ //#region src/resources/resource.ts
212
+ var APIResource = class {
213
+ transport;
214
+ constructor(transport) {
215
+ this.transport = transport;
216
+ }
217
+ };
218
+ //#endregion
219
+ //#region src/resources/admit.ts
220
+ var AdmitResource = class extends APIResource {
221
+ /** Gate admission check: `GET /admit?card=<cardId>`. */
222
+ check(params, options) {
223
+ return this.transport.request({
224
+ method: "GET",
225
+ path: "/api/v1/admit",
226
+ query: { card: params.card },
227
+ retryable: true,
228
+ options
229
+ });
230
+ }
231
+ };
232
+ //#endregion
233
+ //#region src/resources/billing-option-sales-channels.ts
234
+ var BillingOptionSalesChannelsResource = class extends APIResource {
235
+ /** Remove a sales-channel link. Create via billingOptions.createSalesChannelLink(). */
236
+ async delete(id, options) {
237
+ await this.transport.request({
238
+ method: "DELETE",
239
+ path: `/api/v1/billing-option-sales-channels/${encodeURIComponent(id)}`,
240
+ retryable: false,
241
+ options
242
+ });
243
+ }
244
+ };
245
+ //#endregion
246
+ //#region src/resources/billing-options.ts
247
+ var BillingOptionsResource = class extends APIResource {
248
+ async list(options) {
249
+ return (await this.transport.request({
250
+ method: "GET",
251
+ path: "/api/v1/billing-options",
252
+ retryable: true,
253
+ options
254
+ })).billing_options;
255
+ }
256
+ get(id, options) {
257
+ return this.transport.request({
258
+ method: "GET",
259
+ path: `/api/v1/billing-options/${encodeURIComponent(id)}`,
260
+ retryable: true,
261
+ options
262
+ });
263
+ }
264
+ update(id, params, options) {
265
+ return this.transport.request({
266
+ method: "PATCH",
267
+ path: `/api/v1/billing-options/${encodeURIComponent(id)}`,
268
+ body: params,
269
+ retryable: false,
270
+ options
271
+ });
272
+ }
273
+ async delete(id, options) {
274
+ await this.transport.request({
275
+ method: "DELETE",
276
+ path: `/api/v1/billing-options/${encodeURIComponent(id)}`,
277
+ retryable: false,
278
+ options
279
+ });
280
+ }
281
+ async listSalesChannels(id, options) {
282
+ return (await this.transport.request({
283
+ method: "GET",
284
+ path: `/api/v1/billing-options/${encodeURIComponent(id)}/sales-channels`,
285
+ retryable: true,
286
+ options
287
+ })).sales_channels;
288
+ }
289
+ createSalesChannelLink(id, params, options) {
290
+ return this.transport.request({
291
+ method: "POST",
292
+ path: `/api/v1/billing-options/${encodeURIComponent(id)}/sales-channels`,
293
+ body: params,
294
+ retryable: false,
295
+ options
296
+ });
297
+ }
298
+ };
299
+ //#endregion
300
+ //#region src/resources/customers.ts
301
+ var CustomersResource = class extends APIResource {
302
+ async list(options) {
303
+ return (await this.transport.request({
304
+ method: "GET",
305
+ path: "/api/v1/customers",
306
+ retryable: true,
307
+ options
308
+ })).customers;
309
+ }
310
+ get(id, options) {
311
+ return this.transport.request({
312
+ method: "GET",
313
+ path: `/api/v1/customers/${encodeURIComponent(id)}`,
314
+ retryable: true,
315
+ options
316
+ });
317
+ }
318
+ create(params, options) {
319
+ return this.transport.request({
320
+ method: "POST",
321
+ path: "/api/v1/customers",
322
+ body: params,
323
+ retryable: false,
324
+ options
325
+ });
326
+ }
327
+ update(id, params, options) {
328
+ return this.transport.request({
329
+ method: "PATCH",
330
+ path: `/api/v1/customers/${encodeURIComponent(id)}`,
331
+ body: params,
332
+ retryable: false,
333
+ options
334
+ });
335
+ }
336
+ async delete(id, options) {
337
+ await this.transport.request({
338
+ method: "DELETE",
339
+ path: `/api/v1/customers/${encodeURIComponent(id)}`,
340
+ retryable: false,
341
+ options
342
+ });
343
+ }
344
+ async listMemberships(id, options) {
345
+ return (await this.transport.request({
346
+ method: "GET",
347
+ path: `/api/v1/customers/${encodeURIComponent(id)}/memberships`,
348
+ retryable: true,
349
+ options
350
+ })).memberships;
351
+ }
352
+ };
353
+ //#endregion
354
+ //#region src/resources/entitlements.ts
355
+ var EntitlementsResource = class extends APIResource {
356
+ /**
357
+ * Redeem one quota unit for a member. Idempotent: the Idempotency-Key is
358
+ * generated once per call (and reused across the SDK's internal retries),
359
+ * so replays return the original redemption instead of double-spending.
360
+ */
361
+ redeem(params, options = {}) {
362
+ const { idempotencyKey, ...requestOptions } = options;
363
+ return this.transport.request({
364
+ method: "POST",
365
+ path: "/api/v1/entitlements/redeem",
366
+ body: params,
367
+ idempotencyKey: idempotencyKey ?? crypto.randomUUID(),
368
+ retryable: true,
369
+ options: requestOptions
370
+ });
371
+ }
372
+ /**
373
+ * Fire-and-forget batch usage reporting (one row per member). Idempotent
374
+ * per (key, member). Returns the recorded usage rows.
375
+ */
376
+ async recordUsage(params, options = {}) {
377
+ const { idempotencyKey, ...requestOptions } = options;
378
+ return (await this.transport.request({
379
+ method: "POST",
380
+ path: "/api/v1/entitlements/usage",
381
+ body: params,
382
+ idempotencyKey: idempotencyKey ?? crypto.randomUUID(),
383
+ retryable: true,
384
+ options: requestOptions
385
+ })).usage;
386
+ }
387
+ async list(options) {
388
+ return (await this.transport.request({
389
+ method: "GET",
390
+ path: "/api/v1/entitlements",
391
+ retryable: true,
392
+ options
393
+ })).entitlements;
394
+ }
395
+ get(id, options) {
396
+ return this.transport.request({
397
+ method: "GET",
398
+ path: `/api/v1/entitlements/${encodeURIComponent(id)}`,
399
+ retryable: true,
400
+ options
401
+ });
402
+ }
403
+ create(params, options) {
404
+ return this.transport.request({
405
+ method: "POST",
406
+ path: "/api/v1/entitlements",
407
+ body: params,
408
+ retryable: false,
409
+ options
410
+ });
411
+ }
412
+ update(id, params, options) {
413
+ return this.transport.request({
414
+ method: "PATCH",
415
+ path: `/api/v1/entitlements/${encodeURIComponent(id)}`,
416
+ body: params,
417
+ retryable: false,
418
+ options
419
+ });
420
+ }
421
+ async delete(id, options) {
422
+ await this.transport.request({
423
+ method: "DELETE",
424
+ path: `/api/v1/entitlements/${encodeURIComponent(id)}`,
425
+ retryable: false,
426
+ options
427
+ });
428
+ }
429
+ };
430
+ //#endregion
431
+ //#region src/resources/member-roles.ts
432
+ var MemberRolesResource = class extends APIResource {
433
+ async list(options) {
434
+ return (await this.transport.request({
435
+ method: "GET",
436
+ path: "/api/v1/member-roles",
437
+ retryable: true,
438
+ options
439
+ })).member_roles;
440
+ }
441
+ get(id, options) {
442
+ return this.transport.request({
443
+ method: "GET",
444
+ path: `/api/v1/member-roles/${encodeURIComponent(id)}`,
445
+ retryable: true,
446
+ options
447
+ });
448
+ }
449
+ create(params, options) {
450
+ return this.transport.request({
451
+ method: "POST",
452
+ path: "/api/v1/member-roles",
453
+ body: params,
454
+ retryable: false,
455
+ options
456
+ });
457
+ }
458
+ update(id, params, options) {
459
+ return this.transport.request({
460
+ method: "PATCH",
461
+ path: `/api/v1/member-roles/${encodeURIComponent(id)}`,
462
+ body: params,
463
+ retryable: false,
464
+ options
465
+ });
466
+ }
467
+ async delete(id, options) {
468
+ await this.transport.request({
469
+ method: "DELETE",
470
+ path: `/api/v1/member-roles/${encodeURIComponent(id)}`,
471
+ retryable: false,
472
+ options
473
+ });
474
+ }
475
+ };
476
+ //#endregion
477
+ //#region src/resources/members.ts
478
+ var MembersResource = class extends APIResource {
479
+ /**
480
+ * Active-member list for offline gate caching. Pass the previous
481
+ * response's `generated_at` as `since` to fetch incrementally.
482
+ */
483
+ sync(params = {}, options) {
484
+ return this.transport.request({
485
+ method: "GET",
486
+ path: "/api/v1/members/sync",
487
+ query: { since: params.since },
488
+ retryable: true,
489
+ options
490
+ });
491
+ }
492
+ get(id, options) {
493
+ return this.transport.request({
494
+ method: "GET",
495
+ path: `/api/v1/members/${encodeURIComponent(id)}`,
496
+ retryable: true,
497
+ options
498
+ });
499
+ }
500
+ async listEntitlements(id, params = {}, options) {
501
+ return (await this.transport.request({
502
+ method: "GET",
503
+ path: `/api/v1/members/${encodeURIComponent(id)}/entitlements`,
504
+ query: { type: params.type },
505
+ retryable: true,
506
+ options
507
+ })).entitlements;
508
+ }
509
+ /** Issue a fresh card id, invalidating the old card at the gate. */
510
+ reissueCard(id, params = {}, options) {
511
+ return this.transport.request({
512
+ method: "POST",
513
+ path: `/api/v1/members/${encodeURIComponent(id)}/reissue-card`,
514
+ body: params,
515
+ retryable: false,
516
+ options
517
+ });
518
+ }
519
+ };
520
+ //#endregion
521
+ //#region src/resources/memberships.ts
522
+ var MembershipsResource = class extends APIResource {
523
+ async list(options) {
524
+ return (await this.transport.request({
525
+ method: "GET",
526
+ path: "/api/v1/memberships",
527
+ retryable: true,
528
+ options
529
+ })).memberships;
530
+ }
531
+ get(id, options) {
532
+ return this.transport.request({
533
+ method: "GET",
534
+ path: `/api/v1/memberships/${encodeURIComponent(id)}`,
535
+ retryable: true,
536
+ options
537
+ });
538
+ }
539
+ create(params, options) {
540
+ return this.transport.request({
541
+ method: "POST",
542
+ path: "/api/v1/memberships",
543
+ body: params,
544
+ retryable: false,
545
+ options
546
+ });
547
+ }
548
+ update(id, params, options) {
549
+ return this.transport.request({
550
+ method: "PATCH",
551
+ path: `/api/v1/memberships/${encodeURIComponent(id)}`,
552
+ body: params,
553
+ retryable: false,
554
+ options
555
+ });
556
+ }
557
+ /** Cancellation is a state change, not a delete — see also `uncancel`. */
558
+ cancel(id, params, options) {
559
+ return this.transport.request({
560
+ method: "POST",
561
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/cancel`,
562
+ body: params,
563
+ retryable: false,
564
+ options
565
+ });
566
+ }
567
+ uncancel(id, options) {
568
+ return this.transport.request({
569
+ method: "POST",
570
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/uncancel`,
571
+ retryable: false,
572
+ options
573
+ });
574
+ }
575
+ async listTransactions(id, options) {
576
+ return (await this.transport.request({
577
+ method: "GET",
578
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/transactions`,
579
+ retryable: true,
580
+ options
581
+ })).transactions;
582
+ }
583
+ /**
584
+ * Report a payment outcome. Ophel.io never processes payments — your
585
+ * payment provider does; this records the result and drives dunning.
586
+ */
587
+ createTransaction(id, params, options) {
588
+ return this.transport.request({
589
+ method: "POST",
590
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/transactions`,
591
+ body: params,
592
+ retryable: false,
593
+ options
594
+ });
595
+ }
596
+ async listMembers(id, options) {
597
+ return (await this.transport.request({
598
+ method: "GET",
599
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/members`,
600
+ retryable: true,
601
+ options
602
+ })).members;
603
+ }
604
+ /** Open the next term. Rejected (409) while cancelled or cancellation-pending. */
605
+ renew(id, params, options) {
606
+ return this.transport.request({
607
+ method: "POST",
608
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/renew`,
609
+ body: params,
610
+ retryable: false,
611
+ options
612
+ });
613
+ }
614
+ /**
615
+ * Move to a linked higher plan. Applies immediately or schedules at term
616
+ * end — `scheduled_change` is non-null in the deferred case.
617
+ */
618
+ upgrade(id, params, options) {
619
+ return this.transport.request({
620
+ method: "POST",
621
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/upgrade`,
622
+ body: params,
623
+ retryable: false,
624
+ options
625
+ });
626
+ }
627
+ /** Mirror of upgrade towards a linked lower plan. */
628
+ downgrade(id, params, options) {
629
+ return this.transport.request({
630
+ method: "POST",
631
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/downgrade`,
632
+ body: params,
633
+ retryable: false,
634
+ options
635
+ });
636
+ }
637
+ async listScheduledChanges(id, params, options) {
638
+ return (await this.transport.request({
639
+ method: "GET",
640
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/scheduled-changes`,
641
+ query: params?.status ? { status: params.status } : void 0,
642
+ retryable: true,
643
+ options
644
+ })).scheduled_changes;
645
+ }
646
+ async cancelScheduledChange(id, changeId, options) {
647
+ await this.transport.request({
648
+ method: "DELETE",
649
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/scheduled-changes/${encodeURIComponent(changeId)}`,
650
+ retryable: false,
651
+ options
652
+ });
653
+ }
654
+ async listTermTransactions(id, termId, options) {
655
+ return (await this.transport.request({
656
+ method: "GET",
657
+ path: `/api/v1/memberships/${encodeURIComponent(id)}/terms/${encodeURIComponent(termId)}/transactions`,
658
+ retryable: true,
659
+ options
660
+ })).transactions;
661
+ }
662
+ };
663
+ //#endregion
664
+ //#region src/resources/plan-entitlements.ts
665
+ var PlanEntitlementsResource = class extends APIResource {
666
+ get(id, options) {
667
+ return this.transport.request({
668
+ method: "GET",
669
+ path: `/api/v1/plan-entitlements/${encodeURIComponent(id)}`,
670
+ retryable: true,
671
+ options
672
+ });
673
+ }
674
+ update(id, params, options) {
675
+ return this.transport.request({
676
+ method: "PATCH",
677
+ path: `/api/v1/plan-entitlements/${encodeURIComponent(id)}`,
678
+ body: params,
679
+ retryable: false,
680
+ options
681
+ });
682
+ }
683
+ /** Detach an entitlement from its plan. Attach via plans.createEntitlement(). */
684
+ async delete(id, options) {
685
+ await this.transport.request({
686
+ method: "DELETE",
687
+ path: `/api/v1/plan-entitlements/${encodeURIComponent(id)}`,
688
+ retryable: false,
689
+ options
690
+ });
691
+ }
692
+ };
693
+ //#endregion
694
+ //#region src/resources/plan-links.ts
695
+ var PlanLinksResource = class extends APIResource {
696
+ /** Remove an upgrade/downgrade link. Create via plans.createPlanLink(). */
697
+ async delete(id, options) {
698
+ await this.transport.request({
699
+ method: "DELETE",
700
+ path: `/api/v1/plan-links/${encodeURIComponent(id)}`,
701
+ retryable: false,
702
+ options
703
+ });
704
+ }
705
+ };
706
+ //#endregion
707
+ //#region src/resources/plan-pricing-group-mappings.ts
708
+ var PlanPricingGroupMappingsResource = class extends APIResource {
709
+ /** Remove a pricing mapping. Create via plans.createPricingGroupMapping(). */
710
+ async delete(id, options) {
711
+ await this.transport.request({
712
+ method: "DELETE",
713
+ path: `/api/v1/plan-pricing-group-mappings/${encodeURIComponent(id)}`,
714
+ retryable: false,
715
+ options
716
+ });
717
+ }
718
+ };
719
+ //#endregion
720
+ //#region src/resources/plans.ts
721
+ var PlansResource = class extends APIResource {
722
+ async list(options) {
723
+ return (await this.transport.request({
724
+ method: "GET",
725
+ path: "/api/v1/plans",
726
+ retryable: true,
727
+ options
728
+ })).plans;
729
+ }
730
+ get(id, options) {
731
+ return this.transport.request({
732
+ method: "GET",
733
+ path: `/api/v1/plans/${encodeURIComponent(id)}`,
734
+ retryable: true,
735
+ options
736
+ });
737
+ }
738
+ async listBillingOptions(id, options) {
739
+ return (await this.transport.request({
740
+ method: "GET",
741
+ path: `/api/v1/plans/${encodeURIComponent(id)}/billing-options`,
742
+ retryable: true,
743
+ options
744
+ })).billing_options;
745
+ }
746
+ async listEntitlements(id, options) {
747
+ return (await this.transport.request({
748
+ method: "GET",
749
+ path: `/api/v1/plans/${encodeURIComponent(id)}/entitlements`,
750
+ retryable: true,
751
+ options
752
+ })).entitlements;
753
+ }
754
+ create(params, options) {
755
+ return this.transport.request({
756
+ method: "POST",
757
+ path: "/api/v1/plans",
758
+ body: params,
759
+ retryable: false,
760
+ options
761
+ });
762
+ }
763
+ update(id, params, options) {
764
+ return this.transport.request({
765
+ method: "PATCH",
766
+ path: `/api/v1/plans/${encodeURIComponent(id)}`,
767
+ body: params,
768
+ retryable: false,
769
+ options
770
+ });
771
+ }
772
+ async delete(id, options) {
773
+ await this.transport.request({
774
+ method: "DELETE",
775
+ path: `/api/v1/plans/${encodeURIComponent(id)}`,
776
+ retryable: false,
777
+ options
778
+ });
779
+ }
780
+ createBillingOption(id, params, options) {
781
+ return this.transport.request({
782
+ method: "POST",
783
+ path: `/api/v1/plans/${encodeURIComponent(id)}/billing-options`,
784
+ body: params,
785
+ retryable: false,
786
+ options
787
+ });
788
+ }
789
+ async listPlanLinks(id, options) {
790
+ return (await this.transport.request({
791
+ method: "GET",
792
+ path: `/api/v1/plans/${encodeURIComponent(id)}/plan-links`,
793
+ retryable: true,
794
+ options
795
+ })).plan_links;
796
+ }
797
+ createPlanLink(id, params, options) {
798
+ return this.transport.request({
799
+ method: "POST",
800
+ path: `/api/v1/plans/${encodeURIComponent(id)}/plan-links`,
801
+ body: params,
802
+ retryable: false,
803
+ options
804
+ });
805
+ }
806
+ createEntitlement(id, params, options) {
807
+ return this.transport.request({
808
+ method: "POST",
809
+ path: `/api/v1/plans/${encodeURIComponent(id)}/entitlements`,
810
+ body: params,
811
+ retryable: false,
812
+ options
813
+ });
814
+ }
815
+ async listPricingGroupMappings(id, options) {
816
+ return (await this.transport.request({
817
+ method: "GET",
818
+ path: `/api/v1/plans/${encodeURIComponent(id)}/pricing-group-mappings`,
819
+ retryable: true,
820
+ options
821
+ })).pricing_group_mappings;
822
+ }
823
+ createPricingGroupMapping(id, params, options) {
824
+ return this.transport.request({
825
+ method: "POST",
826
+ path: `/api/v1/plans/${encodeURIComponent(id)}/pricing-group-mappings`,
827
+ body: params,
828
+ retryable: false,
829
+ options
830
+ });
831
+ }
832
+ };
833
+ //#endregion
834
+ //#region src/resources/pricing-groups.ts
835
+ var PricingGroupsResource = class extends APIResource {
836
+ async list(options) {
837
+ return (await this.transport.request({
838
+ method: "GET",
839
+ path: "/api/v1/pricing-groups",
840
+ retryable: true,
841
+ options
842
+ })).pricing_groups;
843
+ }
844
+ get(id, options) {
845
+ return this.transport.request({
846
+ method: "GET",
847
+ path: `/api/v1/pricing-groups/${encodeURIComponent(id)}`,
848
+ retryable: true,
849
+ options
850
+ });
851
+ }
852
+ create(params, options) {
853
+ return this.transport.request({
854
+ method: "POST",
855
+ path: "/api/v1/pricing-groups",
856
+ body: params,
857
+ retryable: false,
858
+ options
859
+ });
860
+ }
861
+ update(id, params, options) {
862
+ return this.transport.request({
863
+ method: "PATCH",
864
+ path: `/api/v1/pricing-groups/${encodeURIComponent(id)}`,
865
+ body: params,
866
+ retryable: false,
867
+ options
868
+ });
869
+ }
870
+ async delete(id, options) {
871
+ await this.transport.request({
872
+ method: "DELETE",
873
+ path: `/api/v1/pricing-groups/${encodeURIComponent(id)}`,
874
+ retryable: false,
875
+ options
876
+ });
877
+ }
878
+ };
879
+ //#endregion
880
+ //#region src/resources/sales-channels.ts
881
+ var SalesChannelsResource = class extends APIResource {
882
+ async list(options) {
883
+ return (await this.transport.request({
884
+ method: "GET",
885
+ path: "/api/v1/sales-channels",
886
+ retryable: true,
887
+ options
888
+ })).sales_channels;
889
+ }
890
+ get(id, options) {
891
+ return this.transport.request({
892
+ method: "GET",
893
+ path: `/api/v1/sales-channels/${encodeURIComponent(id)}`,
894
+ retryable: true,
895
+ options
896
+ });
897
+ }
898
+ create(params, options) {
899
+ return this.transport.request({
900
+ method: "POST",
901
+ path: "/api/v1/sales-channels",
902
+ body: params,
903
+ retryable: false,
904
+ options
905
+ });
906
+ }
907
+ update(id, params, options) {
908
+ return this.transport.request({
909
+ method: "PATCH",
910
+ path: `/api/v1/sales-channels/${encodeURIComponent(id)}`,
911
+ body: params,
912
+ retryable: false,
913
+ options
914
+ });
915
+ }
916
+ async delete(id, options) {
917
+ await this.transport.request({
918
+ method: "DELETE",
919
+ path: `/api/v1/sales-channels/${encodeURIComponent(id)}`,
920
+ retryable: false,
921
+ options
922
+ });
923
+ }
924
+ };
925
+ //#endregion
926
+ //#region src/resources/webhook-subscriptions.ts
927
+ var WebhookSubscriptionsResource = class extends APIResource {
928
+ async list(options) {
929
+ return (await this.transport.request({
930
+ method: "GET",
931
+ path: "/api/v1/webhook-subscriptions",
932
+ retryable: true,
933
+ options
934
+ })).webhook_subscriptions;
935
+ }
936
+ get(id, options) {
937
+ return this.transport.request({
938
+ method: "GET",
939
+ path: `/api/v1/webhook-subscriptions/${encodeURIComponent(id)}`,
940
+ retryable: true,
941
+ options
942
+ });
943
+ }
944
+ async listDeliveries(id, options) {
945
+ return (await this.transport.request({
946
+ method: "GET",
947
+ path: `/api/v1/webhook-subscriptions/${encodeURIComponent(id)}/deliveries`,
948
+ retryable: true,
949
+ options
950
+ })).webhook_deliveries;
951
+ }
952
+ /** The HMAC signing secret is only returned on creation — store it then. */
953
+ create(params, options) {
954
+ return this.transport.request({
955
+ method: "POST",
956
+ path: "/api/v1/webhook-subscriptions",
957
+ body: params,
958
+ retryable: false,
959
+ options
960
+ });
961
+ }
962
+ update(id, params, options) {
963
+ return this.transport.request({
964
+ method: "PATCH",
965
+ path: `/api/v1/webhook-subscriptions/${encodeURIComponent(id)}`,
966
+ body: params,
967
+ retryable: false,
968
+ options
969
+ });
970
+ }
971
+ async delete(id, options) {
972
+ await this.transport.request({
973
+ method: "DELETE",
974
+ path: `/api/v1/webhook-subscriptions/${encodeURIComponent(id)}`,
975
+ retryable: false,
976
+ options
977
+ });
978
+ }
979
+ };
980
+ //#endregion
981
+ //#region src/webhooks/verify.ts
982
+ const encoder = new TextEncoder();
983
+ async function computeSignatureHex(payload, secret) {
984
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
985
+ name: "HMAC",
986
+ hash: "SHA-256"
987
+ }, false, ["sign"]);
988
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
989
+ return Array.from(new Uint8Array(signature)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
990
+ }
991
+ function timingSafeEqualHex(a, b) {
992
+ if (a.length !== b.length) return false;
993
+ let mismatch = 0;
994
+ for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
995
+ return mismatch === 0;
996
+ }
997
+ /**
998
+ * Verify the `X-Ophelio-Signature` header against the raw request body.
999
+ * Pass the body exactly as received — re-serialising the parsed JSON can
1000
+ * change key order/whitespace and break verification.
1001
+ */
1002
+ async function verifyWebhookSignature(payload, signature, secret) {
1003
+ const provided = signature.startsWith("sha256=") ? signature.slice(7) : signature;
1004
+ return timingSafeEqualHex(await computeSignatureHex(payload, secret), provided.toLowerCase());
1005
+ }
1006
+ /**
1007
+ * Verify and parse a webhook delivery in one step. Throws
1008
+ * WebhookSignatureVerificationError when the signature doesn't match, or
1009
+ * OphelioError when the payload isn't a structurally valid event.
1010
+ */
1011
+ async function constructEvent(payload, signature, secret) {
1012
+ if (!await verifyWebhookSignature(payload, signature, secret)) throw new WebhookSignatureVerificationError("Webhook signature verification failed.");
1013
+ let parsed;
1014
+ try {
1015
+ parsed = JSON.parse(payload);
1016
+ } catch {
1017
+ throw new OphelioError("Webhook payload is not valid JSON.");
1018
+ }
1019
+ const event = parsed;
1020
+ if (!event || typeof event.id !== "string" || typeof event.type !== "string" || typeof event.create_time !== "string" || typeof event.data !== "object" || event.data === null) throw new OphelioError("Webhook payload is not a valid Ophelio event.");
1021
+ return event;
1022
+ }
1023
+ //#endregion
1024
+ //#region src/client.ts
1025
+ /**
1026
+ * Ophel.io API client.
1027
+ *
1028
+ * ```ts
1029
+ * const ophelio = new Ophelio({ apiKey: process.env.OPHELIO_API_KEY })
1030
+ * const result = await ophelio.admit.check({ card: 'M-123456' })
1031
+ * ```
1032
+ */
1033
+ var Ophelio = class Ophelio {
1034
+ admit;
1035
+ members;
1036
+ memberships;
1037
+ customers;
1038
+ entitlements;
1039
+ plans;
1040
+ billingOptions;
1041
+ memberRoles;
1042
+ pricingGroups;
1043
+ salesChannels;
1044
+ planEntitlements;
1045
+ planLinks;
1046
+ planPricingGroupMappings;
1047
+ billingOptionSalesChannels;
1048
+ webhookSubscriptions;
1049
+ /** Webhook helpers — usable without a client instance (no API key). */
1050
+ static webhooks = {
1051
+ verifySignature: verifyWebhookSignature,
1052
+ constructEvent
1053
+ };
1054
+ webhooks = Ophelio.webhooks;
1055
+ constructor(options) {
1056
+ const transport = new Transport(options);
1057
+ this.admit = new AdmitResource(transport);
1058
+ this.members = new MembersResource(transport);
1059
+ this.memberships = new MembershipsResource(transport);
1060
+ this.customers = new CustomersResource(transport);
1061
+ this.entitlements = new EntitlementsResource(transport);
1062
+ this.plans = new PlansResource(transport);
1063
+ this.billingOptions = new BillingOptionsResource(transport);
1064
+ this.memberRoles = new MemberRolesResource(transport);
1065
+ this.pricingGroups = new PricingGroupsResource(transport);
1066
+ this.salesChannels = new SalesChannelsResource(transport);
1067
+ this.planEntitlements = new PlanEntitlementsResource(transport);
1068
+ this.planLinks = new PlanLinksResource(transport);
1069
+ this.planPricingGroupMappings = new PlanPricingGroupMappingsResource(transport);
1070
+ this.billingOptionSalesChannels = new BillingOptionSalesChannelsResource(transport);
1071
+ this.webhookSubscriptions = new WebhookSubscriptionsResource(transport);
1072
+ }
1073
+ };
1074
+ /**
1075
+ * Canonical webhook event codes for the Ophel.io platform.
1076
+ *
1077
+ * This is the protocol-level source of truth shared by the API (event
1078
+ * emission), the web app (subscription UI), and the SDK (typed webhook
1079
+ * events). UI copy (labels, descriptions, categories) lives in ophelio-web.
1080
+ */
1081
+ const WEBHOOK_EVENT_CODES = [
1082
+ "membership.created",
1083
+ "membership.renewed",
1084
+ "membership.renewal_due",
1085
+ "membership.payment_failed",
1086
+ "membership.expired",
1087
+ "membership.grace_entered",
1088
+ "membership.cancelled",
1089
+ "membership.upgraded",
1090
+ "membership.downgraded",
1091
+ "scheduled_change.failed",
1092
+ "entitlement.used",
1093
+ "member.card_issued",
1094
+ "member.removed",
1095
+ "plan.updated"
1096
+ ];
1097
+ //#endregion
1098
+ export { AlreadyExistsError, DEFAULT_BASE_URL, InternalError, InvalidArgumentError, NotFoundError, Ophelio, OphelioConnectionError, OphelioError, OphelioTimeoutError, PermissionDeniedError, ResourceExhaustedError, UnauthenticatedError, UnavailableError, UnimplementedError, WEBHOOK_EVENT_CODES, WebhookSignatureVerificationError, constructEvent, verifyWebhookSignature };