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