@seatlayer/server 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.js ADDED
@@ -0,0 +1,657 @@
1
+ // src/errors.ts
2
+ var SeatLayerError = class extends Error {
3
+ status;
4
+ /** Machine-readable code: `body.code ?? body.error`. */
5
+ code;
6
+ body;
7
+ /** Correlation id from `X-Request-ID`. Quote it in support requests. */
8
+ requestId;
9
+ constructor(status, body, requestId) {
10
+ const code = body.code ?? body.error ?? "unknown_error";
11
+ super(body.message ?? `SeatLayer API error ${status} (${code})`);
12
+ this.name = "SeatLayerError";
13
+ this.status = status;
14
+ this.code = code;
15
+ this.body = body;
16
+ this.requestId = requestId;
17
+ }
18
+ };
19
+ var SeatLayerAuthError = class extends SeatLayerError {
20
+ constructor(status, body, requestId) {
21
+ super(status, body, requestId);
22
+ this.name = "SeatLayerAuthError";
23
+ }
24
+ /**
25
+ * True when the key's mode and the event's mode disagree — the most common
26
+ * cause of a "works locally, 403s in production" report.
27
+ */
28
+ get isModeMismatch() {
29
+ return this.code === "mode_mismatch";
30
+ }
31
+ };
32
+ var SeatLayerNotFoundError = class extends SeatLayerError {
33
+ constructor(status, body, requestId) {
34
+ super(status, body, requestId);
35
+ this.name = "SeatLayerNotFoundError";
36
+ }
37
+ };
38
+ var SeatLayerConflictError = class extends SeatLayerError {
39
+ /** Per-object conflicts, when the endpoint reports them. */
40
+ conflicts;
41
+ constructor(status, body, requestId) {
42
+ super(status, body, requestId);
43
+ this.name = "SeatLayerConflictError";
44
+ this.conflicts = Array.isArray(body.conflicts) ? body.conflicts : [];
45
+ }
46
+ /** True when best-available could not find enough free inventory. */
47
+ get isSoldOut() {
48
+ return this.body.reason === "sold_out" || this.body.reason === "not_enough_together";
49
+ }
50
+ };
51
+ var SeatLayerValidationError = class extends SeatLayerError {
52
+ constructor(status, body, requestId) {
53
+ super(status, body, requestId);
54
+ this.name = "SeatLayerValidationError";
55
+ }
56
+ };
57
+ var SeatLayerRateLimitError = class extends SeatLayerError {
58
+ retryAfterSeconds;
59
+ constructor(status, body, requestId, retryAfterSeconds) {
60
+ super(status, body, requestId);
61
+ this.name = "SeatLayerRateLimitError";
62
+ this.retryAfterSeconds = retryAfterSeconds;
63
+ }
64
+ };
65
+ var SeatLayerConnectionError = class extends Error {
66
+ cause;
67
+ constructor(message, cause) {
68
+ super(message);
69
+ this.name = "SeatLayerConnectionError";
70
+ this.cause = cause;
71
+ }
72
+ };
73
+ function errorFromResponse(status, body, requestId, retryAfterSeconds) {
74
+ if (status === 401 || status === 403) return new SeatLayerAuthError(status, body, requestId);
75
+ if (status === 404) return new SeatLayerNotFoundError(status, body, requestId);
76
+ if (status === 409) return new SeatLayerConflictError(status, body, requestId);
77
+ if (status === 422) return new SeatLayerValidationError(status, body, requestId);
78
+ if (status === 429) {
79
+ return new SeatLayerRateLimitError(status, body, requestId, retryAfterSeconds);
80
+ }
81
+ return new SeatLayerError(status, body, requestId);
82
+ }
83
+
84
+ // src/http.ts
85
+ var DEFAULT_BASE_URL = "https://api.seatlayer.io";
86
+ var DEFAULT_MAX_RETRIES = 3;
87
+ var DEFAULT_TIMEOUT_MS = 3e4;
88
+ var IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
89
+ function assertValidIdempotencyKey(key) {
90
+ if (!IDEMPOTENCY_KEY_PATTERN.test(key)) {
91
+ throw new TypeError(
92
+ `Invalid Idempotency-Key ${JSON.stringify(key)}: allowed characters are A-Z a-z 0-9 . _ : - and the length must be 1-128.`
93
+ );
94
+ }
95
+ }
96
+ function shouldSendIdempotencyKey(method) {
97
+ return method !== "GET" && method !== "HEAD";
98
+ }
99
+ function isRetryableStatus(status) {
100
+ return status === 429 || status === 408 || status >= 500 && status < 600;
101
+ }
102
+ function backoffMs(attempt, retryAfterSeconds) {
103
+ if (retryAfterSeconds !== null) return retryAfterSeconds * 1e3;
104
+ const ceiling = Math.min(8e3, 250 * 2 ** attempt);
105
+ return Math.random() * ceiling;
106
+ }
107
+ function parseRetryAfter(response, body) {
108
+ const header = response.headers.get("retry-after");
109
+ if (header) {
110
+ const seconds = Number(header);
111
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds;
112
+ }
113
+ const field = body.retryAfterSeconds;
114
+ if (typeof field === "number" && Number.isFinite(field)) return field;
115
+ return 1;
116
+ }
117
+ function sleep(ms) {
118
+ return new Promise((resolve) => setTimeout(resolve, ms));
119
+ }
120
+ var HttpClient = class {
121
+ baseUrl;
122
+ /** Whether this client is pointed at test-mode or live-mode data. */
123
+ mode;
124
+ #secretKey;
125
+ #maxRetries;
126
+ #timeoutMs;
127
+ #fetch;
128
+ constructor(options) {
129
+ if (!options.secretKey) {
130
+ throw new TypeError("A SeatLayer secret key is required.");
131
+ }
132
+ if (options.secretKey.startsWith("pk_")) {
133
+ throw new TypeError(
134
+ "That is a publishable key. The server SDK needs a secret key (sk_live_\u2026 or sk_test_\u2026)."
135
+ );
136
+ }
137
+ if (!options.secretKey.startsWith("sk_")) {
138
+ throw new TypeError("A SeatLayer secret key starts with sk_live_ or sk_test_.");
139
+ }
140
+ this.#secretKey = options.secretKey;
141
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
142
+ this.#maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
143
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
144
+ this.#fetch = options.fetch ?? globalThis.fetch;
145
+ this.mode = options.secretKey.startsWith("sk_test_") ? "test" : options.secretKey.startsWith("sk_live_") ? "live" : "unknown";
146
+ }
147
+ async request(method, path, options = {}) {
148
+ const url = new URL(this.baseUrl + path);
149
+ for (const [key, value] of Object.entries(options.query ?? {})) {
150
+ if (value !== void 0) url.searchParams.set(key, String(value));
151
+ }
152
+ const headers = {
153
+ Authorization: `Bearer ${this.#secretKey}`,
154
+ Accept: "application/json",
155
+ "User-Agent": "@seatlayer/server"
156
+ };
157
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
158
+ if (shouldSendIdempotencyKey(method)) {
159
+ const key = options.idempotencyKey ?? crypto.randomUUID();
160
+ assertValidIdempotencyKey(key);
161
+ headers["Idempotency-Key"] = key;
162
+ }
163
+ let lastError;
164
+ for (let attempt = 0; attempt < this.#maxRetries; attempt++) {
165
+ const timeout = AbortSignal.timeout(this.#timeoutMs);
166
+ const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
167
+ let response;
168
+ try {
169
+ response = await this.#fetch(url, {
170
+ method,
171
+ headers,
172
+ signal,
173
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
174
+ });
175
+ } catch (cause) {
176
+ if (options.signal?.aborted) throw cause;
177
+ lastError = new SeatLayerConnectionError(
178
+ `Request to ${method} ${path} failed: ${cause?.message ?? "unknown error"}`,
179
+ cause
180
+ );
181
+ if (attempt < this.#maxRetries - 1) {
182
+ await sleep(backoffMs(attempt, null));
183
+ continue;
184
+ }
185
+ throw lastError;
186
+ }
187
+ const requestId = response.headers.get("x-request-id");
188
+ if (response.ok) {
189
+ if (response.status === 204) return void 0;
190
+ const text = await response.text();
191
+ return text ? JSON.parse(text) : void 0;
192
+ }
193
+ const body = await response.json().catch(() => ({}));
194
+ const retryAfter = parseRetryAfter(response, body);
195
+ if (isRetryableStatus(response.status) && attempt < this.#maxRetries - 1) {
196
+ await sleep(backoffMs(attempt, response.status === 429 ? retryAfter : null));
197
+ continue;
198
+ }
199
+ throw errorFromResponse(response.status, body, requestId, retryAfter);
200
+ }
201
+ throw lastError ?? new SeatLayerConnectionError("Request failed with no attempts made.", null);
202
+ }
203
+ get(path, options) {
204
+ return this.request("GET", path, options);
205
+ }
206
+ post(path, options) {
207
+ return this.request("POST", path, options);
208
+ }
209
+ put(path, options) {
210
+ return this.request("PUT", path, options);
211
+ }
212
+ patch(path, options) {
213
+ return this.request("PATCH", path, options);
214
+ }
215
+ delete(path, options) {
216
+ return this.request("DELETE", path, options);
217
+ }
218
+ };
219
+
220
+ // src/resources/charts.ts
221
+ var Charts = class {
222
+ #http;
223
+ constructor(http) {
224
+ this.#http = http;
225
+ }
226
+ /**
227
+ * One page of charts. Pass `cursor` from the previous page's `nextCursor`;
228
+ * its absence means the list is exhausted.
229
+ */
230
+ list(options = {}) {
231
+ return this.#http.get("/v1/charts", {
232
+ query: {
233
+ workspaceId: options.workspaceId,
234
+ externalRef: options.externalRef,
235
+ limit: options.limit,
236
+ cursor: options.cursor,
237
+ ...options.archived ? { archived: "1" } : {}
238
+ }
239
+ });
240
+ }
241
+ /**
242
+ * Every chart, paging transparently.
243
+ *
244
+ * An async iterator rather than an array: the whole point of paginating was
245
+ * to stop loading an unbounded list into memory, and returning `ChartMeta[]`
246
+ * would hand that problem straight back to the caller.
247
+ *
248
+ * for await (const chart of seatlayer.charts.listAll()) { … }
249
+ */
250
+ async *listAll(options = {}) {
251
+ let cursor;
252
+ do {
253
+ const page = await this.list({ ...options, cursor });
254
+ for (const chart of page.charts) yield chart;
255
+ cursor = page.nextCursor;
256
+ } while (cursor);
257
+ }
258
+ create(params, options = {}) {
259
+ return this.#http.post("/v1/charts", { body: params, idempotencyKey: options.idempotencyKey });
260
+ }
261
+ retrieve(chartId) {
262
+ return this.#http.get(`/v1/charts/${encodeURIComponent(chartId)}`);
263
+ }
264
+ /**
265
+ * Replace a chart document.
266
+ *
267
+ * `expectedUpdatedAt` is required by the API for optimistic concurrency and
268
+ * is not optional here either: without it two concurrent writers silently
269
+ * overwrite each other, and a seat map is exactly the kind of document where
270
+ * that loses work. Read it from `retrieve()` immediately before writing.
271
+ *
272
+ * The Designer is the authoring surface. Reach for this for bulk programmatic
273
+ * edits and migrations, not for drawing.
274
+ */
275
+ update(chartId, params) {
276
+ return this.#http.put(`/v1/charts/${encodeURIComponent(chartId)}`, { body: params });
277
+ }
278
+ delete(chartId) {
279
+ return this.#http.delete(`/v1/charts/${encodeURIComponent(chartId)}`);
280
+ }
281
+ /** Copy a chart — the usual way to provision a venue from a template. */
282
+ copy(chartId, options = {}) {
283
+ return this.#http.post(`/v1/charts/${encodeURIComponent(chartId)}/duplicate`, {
284
+ idempotencyKey: options.idempotencyKey
285
+ });
286
+ }
287
+ archive(chartId) {
288
+ return this.#http.post(`/v1/charts/${encodeURIComponent(chartId)}/archive`);
289
+ }
290
+ unarchive(chartId) {
291
+ return this.#http.post(`/v1/charts/${encodeURIComponent(chartId)}/unarchive`);
292
+ }
293
+ /** Publish the draft. An event can only be created from a published chart. */
294
+ publish(chartId) {
295
+ return this.#http.post(`/v1/charts/${encodeURIComponent(chartId)}/publish`);
296
+ }
297
+ };
298
+
299
+ // src/resources/events.ts
300
+ var Events = class {
301
+ #http;
302
+ constructor(http) {
303
+ this.#http = http;
304
+ }
305
+ /**
306
+ * One page of events. Pass `cursor` from the previous page's `nextCursor`.
307
+ *
308
+ * Live availability `counts` cost one round-trip per event server-side. They
309
+ * are included by default because most callers want them; pass
310
+ * `counts: false` when paging a whole catalogue, where you almost certainly
311
+ * do not.
312
+ */
313
+ list(options = {}) {
314
+ return this.#http.get("/v1/events", {
315
+ query: {
316
+ workspaceId: options.workspaceId,
317
+ externalRef: options.externalRef,
318
+ limit: options.limit,
319
+ cursor: options.cursor,
320
+ ...options.counts === false ? { counts: "0" } : {}
321
+ }
322
+ });
323
+ }
324
+ /**
325
+ * Every event, paging transparently. Defaults to `counts: false` — you are
326
+ * walking the whole list, so per-event availability is rarely what you want
327
+ * and always what it costs.
328
+ *
329
+ * for await (const event of seatlayer.events.listAll()) { … }
330
+ */
331
+ async *listAll(options = {}) {
332
+ let cursor;
333
+ do {
334
+ const page = await this.list({ counts: false, ...options, cursor });
335
+ for (const event of page.events) yield event;
336
+ cursor = page.nextCursor;
337
+ } while (cursor);
338
+ }
339
+ create(params, options = {}) {
340
+ return this.#http.post("/v1/events", { body: params, idempotencyKey: options.idempotencyKey });
341
+ }
342
+ retrieve(eventKey) {
343
+ return this.#http.get(`/v1/events/${encodeURIComponent(eventKey)}`);
344
+ }
345
+ update(eventKey, params) {
346
+ return this.#http.patch(`/v1/events/${encodeURIComponent(eventKey)}`, { body: params });
347
+ }
348
+ delete(eventKey) {
349
+ return this.#http.delete(`/v1/events/${encodeURIComponent(eventKey)}`);
350
+ }
351
+ /** Move a live event onto the latest published version of its chart. */
352
+ updateChart(eventKey) {
353
+ return this.#http.post(`/v1/events/${encodeURIComponent(eventKey)}/update-chart`);
354
+ }
355
+ /** Stop buyer sales. Existing holds keep their TTL. */
356
+ close(eventKey) {
357
+ return this.#http.post(`/v1/events/${encodeURIComponent(eventKey)}/close`);
358
+ }
359
+ reopen(eventKey) {
360
+ return this.#http.post(`/v1/events/${encodeURIComponent(eventKey)}/reopen`);
361
+ }
362
+ archive(eventKey) {
363
+ return this.#http.post(`/v1/events/${encodeURIComponent(eventKey)}/archive`);
364
+ }
365
+ /** Read the checkout window (ms) buyers get for this event. */
366
+ retrieveHoldTtl(eventKey) {
367
+ return this.#http.get(`/v1/events/${encodeURIComponent(eventKey)}/hold-ttl`);
368
+ }
369
+ updateHoldTtl(eventKey, holdTtlMs) {
370
+ return this.#http.post(`/v1/events/${encodeURIComponent(eventKey)}/hold-ttl`, {
371
+ body: { holdTtlMs }
372
+ });
373
+ }
374
+ retrieveReport(eventKey) {
375
+ return this.#http.get(`/v1/events/${encodeURIComponent(eventKey)}/report`);
376
+ }
377
+ retrieveLog(eventKey) {
378
+ return this.#http.get(`/v1/events/${encodeURIComponent(eventKey)}/log`);
379
+ }
380
+ };
381
+
382
+ // src/resources/inventory.ts
383
+ var Inventory = class {
384
+ #http;
385
+ constructor(http) {
386
+ this.#http = http;
387
+ }
388
+ #path(eventKey, suffix) {
389
+ return `/v1/events/${encodeURIComponent(eventKey)}${suffix}`;
390
+ }
391
+ hold(eventKey, params, options = {}) {
392
+ return this.#http.post(this.#path(eventKey, "/hold"), {
393
+ body: params,
394
+ idempotencyKey: options.idempotencyKey
395
+ });
396
+ }
397
+ /**
398
+ * Ask us to pick the best free objects and hold them.
399
+ *
400
+ * The picker is the same one the buyer widget uses, so a phone order and a
401
+ * web order get the same answer for the same inventory. `qty` above the
402
+ * server cap is clamped, not rejected.
403
+ */
404
+ holdBestAvailable(eventKey, params, options = {}) {
405
+ return this.#http.post(this.#path(eventKey, "/best-available"), {
406
+ body: params,
407
+ idempotencyKey: options.idempotencyKey
408
+ });
409
+ }
410
+ /**
411
+ * Pick and book in one call — the box-office shape, where payment is already
412
+ * taken and there is no buyer session to hold against.
413
+ *
414
+ * Prefer this over holdBestAvailable-then-book for that case: a failure
415
+ * between the two calls would strand inventory until the TTL expired.
416
+ */
417
+ bookBestAvailable(eventKey, params, options = {}) {
418
+ return this.#http.post(this.#path(eventKey, "/best-available-book"), {
419
+ body: params,
420
+ idempotencyKey: options.idempotencyKey
421
+ });
422
+ }
423
+ /**
424
+ * Push an active hold's expiry out by a fresh window before it lapses.
425
+ *
426
+ * Use this rather than release-and-re-hold when an order is taking longer
427
+ * than the checkout window — invoiced sales, a phone order on hold. Releasing
428
+ * first hands the seats to whoever is racing for them in between. The server
429
+ * clamps the window and the DO caps how many times one hold can be renewed;
430
+ * a hold that is gone, expired, or at its cap answers 409 `cannot_extend`.
431
+ */
432
+ extendHold(eventKey, params) {
433
+ return this.#http.post(this.#path(eventKey, "/extend"), { body: params });
434
+ }
435
+ /** Authoritative items and prices for a hold. Charge from this, not the browser. */
436
+ retrieveHold(eventKey, holdId) {
437
+ return this.#http.get(this.#path(eventKey, `/holds/${encodeURIComponent(holdId)}`));
438
+ }
439
+ /** Free a hold early. Requires both the labels and the hold id. */
440
+ release(eventKey, params) {
441
+ return this.#http.post(this.#path(eventKey, "/release"), { body: params });
442
+ }
443
+ book(eventKey, params, options = {}) {
444
+ return this.#http.post(this.#path(eventKey, "/book"), {
445
+ body: params,
446
+ idempotencyKey: options.idempotencyKey
447
+ });
448
+ }
449
+ boxOfficeBook(eventKey, params, options = {}) {
450
+ return this.#http.post(this.#path(eventKey, "/box-book"), {
451
+ body: params,
452
+ idempotencyKey: options.idempotencyKey
453
+ });
454
+ }
455
+ /** Reverse a booking. Requires a key with cancel authority. */
456
+ unbook(eventKey, params) {
457
+ return this.#http.post(this.#path(eventKey, "/unbook"), { body: params });
458
+ }
459
+ /** Hold inventory back from sale (house seats, holds for production). */
460
+ block(eventKey, params) {
461
+ return this.#http.post(this.#path(eventKey, "/block"), { body: params });
462
+ }
463
+ unblock(eventKey, params) {
464
+ return this.#http.post(this.#path(eventKey, "/unblock"), { body: params });
465
+ }
466
+ unblockAll(eventKey) {
467
+ return this.#http.post(this.#path(eventKey, "/unblock-all"));
468
+ }
469
+ retrieveAvailability(eventKey) {
470
+ return this.#http.get(this.#path(eventKey, "/availability"));
471
+ }
472
+ updateAvailability(eventKey, params) {
473
+ return this.#http.post(this.#path(eventKey, "/availability"), { body: params });
474
+ }
475
+ };
476
+
477
+ // src/resources/sessions.ts
478
+ var Sessions = class {
479
+ #http;
480
+ constructor(http) {
481
+ this.#http = http;
482
+ }
483
+ /**
484
+ * Mint a manage-session token for the control room.
485
+ *
486
+ * `capabilities` is required here even though the API defaults it. That
487
+ * default grants all four — including `event:cancel`, which un-books paid
488
+ * inventory. Granting the ability to reverse sales by forgetting an argument
489
+ * is not a default worth inheriting, so this SDK makes you say it.
490
+ *
491
+ * `allowedOrigin` must be an https origin; the token is bound to it.
492
+ */
493
+ // `async` so the guard below rejects rather than throwing synchronously —
494
+ // a sync throw from a promise-returning method escapes `.catch()` and
495
+ // surfaces as an unhandled error in the caller's request handler.
496
+ async createManageSession(eventKey, params) {
497
+ if (!params.capabilities?.length) {
498
+ throw new TypeError(
499
+ 'capabilities is required: pass the smallest set the page needs, e.g. ["event:view"]. Omitting it server-side grants event:cancel, which can reverse paid bookings.'
500
+ );
501
+ }
502
+ return this.#http.post(`/v1/events/${encodeURIComponent(eventKey)}/manage-sessions`, {
503
+ body: params
504
+ });
505
+ }
506
+ /** Revoke a manage token before it expires (staff logout, permission change). */
507
+ revokeManageSession(eventKey, sessionId) {
508
+ return this.#http.delete(
509
+ `/v1/events/${encodeURIComponent(eventKey)}/manage-sessions/${encodeURIComponent(sessionId)}`
510
+ );
511
+ }
512
+ /**
513
+ * Mint a designer-session token so an organiser can edit a chart inside your
514
+ * own UI. Requires a chartId that already exists — create or copy one first.
515
+ */
516
+ createDesignerSession(params) {
517
+ return this.#http.post("/v1/designer/sessions", { body: params });
518
+ }
519
+ revokeDesignerSession(sessionId) {
520
+ return this.#http.delete(`/v1/designer/sessions/${encodeURIComponent(sessionId)}`);
521
+ }
522
+ };
523
+
524
+ // src/resources/webhooks.ts
525
+ var Webhooks = class {
526
+ #http;
527
+ constructor(http) {
528
+ this.#http = http;
529
+ }
530
+ list() {
531
+ return this.#http.get("/v1/webhooks");
532
+ }
533
+ create(params) {
534
+ return this.#http.post("/v1/webhooks", { body: params });
535
+ }
536
+ update(webhookId, params) {
537
+ return this.#http.patch(`/v1/webhooks/${encodeURIComponent(webhookId)}`, { body: params });
538
+ }
539
+ delete(webhookId) {
540
+ return this.#http.delete(`/v1/webhooks/${encodeURIComponent(webhookId)}`);
541
+ }
542
+ listDeliveries(webhookId) {
543
+ return this.#http.get(`/v1/webhooks/${encodeURIComponent(webhookId)}/deliveries`);
544
+ }
545
+ };
546
+
547
+ // src/resources/workspaces.ts
548
+ var Workspaces = class {
549
+ #http;
550
+ constructor(http) {
551
+ this.#http = http;
552
+ }
553
+ list() {
554
+ return this.#http.get("/v1/workspaces");
555
+ }
556
+ create(params, options = {}) {
557
+ return this.#http.post("/v1/workspaces", { body: params, idempotencyKey: options.idempotencyKey });
558
+ }
559
+ retrieve(workspaceId) {
560
+ return this.#http.get(`/v1/workspaces/${encodeURIComponent(workspaceId)}`);
561
+ }
562
+ /**
563
+ * Rename, re-reference, or disable a workspace.
564
+ *
565
+ * The organisation's default workspace cannot be disabled — the API answers
566
+ * 409 `default_workspace_required`. Promote another one first.
567
+ */
568
+ update(workspaceId, params) {
569
+ return this.#http.patch(`/v1/workspaces/${encodeURIComponent(workspaceId)}`, { body: params });
570
+ }
571
+ };
572
+
573
+ // src/webhooks-verify.ts
574
+ import { createHmac, timingSafeEqual } from "crypto";
575
+ var WebhookVerificationError = class extends Error {
576
+ constructor(message) {
577
+ super(message);
578
+ this.name = "WebhookVerificationError";
579
+ }
580
+ };
581
+ function verifyWebhook(options) {
582
+ const { payload, signature, secret } = options;
583
+ if (!secret) throw new WebhookVerificationError("A webhook signing secret is required.");
584
+ if (!signature) {
585
+ throw new WebhookVerificationError("Missing X-SeatLayer-Signature header.");
586
+ }
587
+ const [scheme, provided] = signature.split("=");
588
+ if (scheme !== "sha256" || !provided) {
589
+ throw new WebhookVerificationError(
590
+ `Unsupported signature format ${JSON.stringify(signature)}; expected "sha256=<hex>".`
591
+ );
592
+ }
593
+ const body = typeof payload === "string" ? Buffer.from(payload, "utf8") : Buffer.from(payload);
594
+ const expected = createHmac("sha256", secret).update(body).digest("hex");
595
+ const a = Buffer.from(expected, "hex");
596
+ const b = Buffer.from(provided, "hex");
597
+ if (a.length !== b.length || !timingSafeEqual(a, b)) {
598
+ throw new WebhookVerificationError("Webhook signature did not match.");
599
+ }
600
+ try {
601
+ return JSON.parse(body.toString("utf8"));
602
+ } catch (cause) {
603
+ throw new WebhookVerificationError(
604
+ `Signature verified but the body is not valid JSON: ${cause.message}`
605
+ );
606
+ }
607
+ }
608
+
609
+ // src/index.ts
610
+ var SeatLayer = class {
611
+ charts;
612
+ events;
613
+ inventory;
614
+ sessions;
615
+ webhooks;
616
+ workspaces;
617
+ /** `test` or `live`, derived from the key prefix. */
618
+ mode;
619
+ #http;
620
+ constructor(options) {
621
+ const resolved = typeof options === "string" ? { secretKey: options } : options;
622
+ this.#http = new HttpClient(resolved);
623
+ this.mode = this.#http.mode;
624
+ this.charts = new Charts(this.#http);
625
+ this.events = new Events(this.#http);
626
+ this.inventory = new Inventory(this.#http);
627
+ this.sessions = new Sessions(this.#http);
628
+ this.webhooks = new Webhooks(this.#http);
629
+ this.workspaces = new Workspaces(this.#http);
630
+ }
631
+ /** Dependency-aware readiness probe. Unauthenticated upstream. */
632
+ ready() {
633
+ return this.#http.get("/health/ready");
634
+ }
635
+ /**
636
+ * Escape hatch for surface this SDK does not wrap yet. Carries the same auth,
637
+ * retry, idempotency and error mapping as everything else.
638
+ */
639
+ request(method, path, options) {
640
+ return this.#http.request(method, path, options);
641
+ }
642
+ };
643
+ var index_default = SeatLayer;
644
+ export {
645
+ SeatLayer,
646
+ SeatLayerAuthError,
647
+ SeatLayerConflictError,
648
+ SeatLayerConnectionError,
649
+ SeatLayerError,
650
+ SeatLayerNotFoundError,
651
+ SeatLayerRateLimitError,
652
+ SeatLayerValidationError,
653
+ WebhookVerificationError,
654
+ index_default as default,
655
+ verifyWebhook
656
+ };
657
+ //# sourceMappingURL=index.js.map