tixbit 0.1.0 → 0.1.2

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 CHANGED
@@ -1,41 +1,109 @@
1
+ // src/safety.ts
2
+ function trustedWebBase(base) {
3
+ const url = new URL(base);
4
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
5
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.origin === "https://www.tixbit.com" || local && ["http:", "https:"].includes(url.protocol))) {
6
+ throw new TypeError("Credential requests require https://www.tixbit.com or a loopback QA origin");
7
+ }
8
+ return url.origin;
9
+ }
10
+ function assertMppChallenge(challenge, maxAmountCents) {
11
+ const { amount, currency } = challenge.request;
12
+ if (!Number.isSafeInteger(maxAmountCents) || maxAmountCents <= 0 || challenge.method !== "tempo" || challenge.intent !== "charge" || currency !== "0x20c0000000000000000000000000000000000000" || typeof amount !== "string" || !/^\d{1,30}$/.test(amount) || BigInt(amount) <= 0n || BigInt(amount) > BigInt(maxAmountCents) * 10000n || challenge.request.splits !== void 0 || challenge.request.methodDetails !== void 0 && (!challenge.request.methodDetails || typeof challenge.request.methodDetails !== "object" || "splits" in challenge.request.methodDetails)) {
13
+ throw new TypeError("MPP challenge exceeds the approved total or uses an unsupported payment asset or intent");
14
+ }
15
+ }
16
+ function redact(value, secrets = []) {
17
+ if (typeof value === "string") {
18
+ let result = value.replace(/\bspt_[A-Za-z0-9_]+/g, "[REDACTED]");
19
+ for (const secret of secrets.filter(Boolean)) result = result.split(secret).join("[REDACTED]");
20
+ return result;
21
+ }
22
+ if (Array.isArray(value)) return value.map((item) => redact(item, secrets));
23
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, /token|secret|authorization|private.?key|password|credential/i.test(key) ? "[REDACTED]" : redact(item, secrets)]));
24
+ return value;
25
+ }
26
+
1
27
  // src/client.ts
2
- var DEFAULT_BASE_URL = "https://tixbit.com";
28
+ var DEFAULT_BASE_URL = "https://www.tixbit.com";
29
+ var DEFAULT_PAYMENT_ENDPOINT = "https://mcp.tixbit.com/api/purchase";
3
30
  var DEFAULT_TIMEOUT_MS = 15e3;
4
31
  var USER_AGENT = "@tixbit/sdk";
32
+ function normalizePaymentEndpoint(value) {
33
+ let url;
34
+ try {
35
+ url = new URL(value);
36
+ } catch {
37
+ throw new TypeError("paymentEndpoint must be an absolute URL");
38
+ }
39
+ const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
40
+ const isTixBitHost = url.origin === "https://mcp.tixbit.com" && url.pathname === "/api/purchase";
41
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
42
+ throw new TypeError(
43
+ "paymentEndpoint must use HTTPS (HTTP is allowed only for loopback QA)"
44
+ );
45
+ }
46
+ if (url.username || url.password || url.hash || url.search) {
47
+ throw new TypeError("paymentEndpoint must not contain credentials or a fragment");
48
+ }
49
+ if (!isLoopback && !isTixBitHost) {
50
+ throw new TypeError(
51
+ "paymentEndpoint must use a TixBit host or a loopback address for local QA"
52
+ );
53
+ }
54
+ return url.toString().replace(/\/$/, "");
55
+ }
56
+ function normalizeExternalEventId(eventId) {
57
+ return eventId;
58
+ }
59
+ function toAbsoluteUrl(baseUrl, value) {
60
+ if (!value) return null;
61
+ if (value.startsWith("http://") || value.startsWith("https://")) {
62
+ return value;
63
+ }
64
+ return `${baseUrl}${value.startsWith("/") ? "" : "/"}${value}`;
65
+ }
5
66
  var TixBitClient = class {
6
67
  baseUrl;
7
68
  timeoutMs;
8
- apiKey;
69
+ paymentEndpoint;
70
+ paymentFetch;
9
71
  constructor(config = {}) {
10
72
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
11
73
  this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12
- this.apiKey = config.apiKey;
74
+ this.paymentEndpoint = normalizePaymentEndpoint(
75
+ config.paymentEndpoint ?? DEFAULT_PAYMENT_ENDPOINT
76
+ );
77
+ this.paymentFetch = config.paymentFetch ?? ((input, init) => fetch(input, init));
13
78
  }
14
79
  // ── HTTP helpers ──────────────────────────────────────────────────────────
15
- async request(path, init) {
80
+ async request(path) {
16
81
  const url = `${this.baseUrl}${path}`;
17
82
  const controller = new AbortController();
18
83
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
19
84
  const headers = {
20
85
  Accept: "application/json",
21
- "User-Agent": USER_AGENT,
22
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
86
+ "User-Agent": USER_AGENT
23
87
  };
24
88
  try {
25
89
  const res = await fetch(url, {
26
- ...init,
27
- headers: { ...headers, ...init?.headers },
90
+ redirect: "error",
91
+ headers,
28
92
  signal: controller.signal
29
93
  });
30
94
  if (!res.ok) {
31
- const text = await res.text().catch(() => "");
32
95
  throw new TixBitApiError(
33
- `${res.status} ${res.statusText}: ${text.slice(0, 300)}`,
96
+ `Request failed (HTTP ${res.status})`,
34
97
  res.status,
35
98
  url
36
99
  );
37
100
  }
38
101
  return await res.json();
102
+ } catch (error) {
103
+ if (controller.signal.aborted) {
104
+ throw new TixBitTimeoutError(url, this.timeoutMs);
105
+ }
106
+ throw error;
39
107
  } finally {
40
108
  clearTimeout(timer);
41
109
  }
@@ -59,15 +127,40 @@ var TixBitClient = class {
59
127
  city: params.city,
60
128
  state: params.state,
61
129
  category: params.category,
130
+ league: params.league,
131
+ categoryEventType: params.categoryEventType,
132
+ performerId: params.performerId,
133
+ venueId: params.venueId,
134
+ parkingFilter: params.parkingFilter,
135
+ nearLat: params.nearLat,
136
+ nearLng: params.nearLng,
137
+ locationMode: params.locationMode,
62
138
  startDate: params.startDate,
63
139
  endDate: params.endDate,
64
140
  page: params.page,
65
141
  size: params.size ?? 25
66
142
  });
67
143
  const data = await this.request(`/api/events/search${query}`);
144
+ const pagination = data.pagination;
68
145
  return {
69
146
  events: (data.events ?? []).map(normalizeEvent),
70
- pagination: data.pagination
147
+ pagination: {
148
+ ...pagination,
149
+ totalExact: pagination.totalExact ?? pagination.total !== null
150
+ }
151
+ };
152
+ }
153
+ /** Get one public event by ID or slug. */
154
+ async getEvent(eventId) {
155
+ const normalizedEventId = normalizeExternalEventId(eventId);
156
+ const data = await this.request(
157
+ `/api/events/${encodeURIComponent(normalizedEventId)}`
158
+ );
159
+ const publicEventId = normalizeExternalEventId(data.external_id ?? data.id);
160
+ return {
161
+ ...data,
162
+ id: publicEventId,
163
+ external_id: publicEventId
71
164
  };
72
165
  }
73
166
  // ── Browse (Location-Aware) ───────────────────────────────────────────────
@@ -81,19 +174,32 @@ var TixBitClient = class {
81
174
  */
82
175
  async browse(params = {}) {
83
176
  const query = this.qs({
177
+ page: params.page ?? 1,
84
178
  size: params.size ?? 18,
85
- context: "homepage",
86
- recommendation: "upcoming",
179
+ query: params.query,
180
+ category: params.category,
181
+ league: params.league,
182
+ date: params.date,
183
+ context: params.context ?? "homepage",
184
+ recommendation: params.recommendation ?? "upcoming",
87
185
  nearLat: params.latitude,
88
186
  nearLng: params.longitude,
89
187
  preferCity: params.city,
90
188
  preferState: params.state,
91
- categoryEventType: params.categoryEventType
189
+ categoryEventType: params.categoryEventType,
190
+ locationMode: params.locationMode,
191
+ searchScope: params.searchScope,
192
+ parkingFilter: params.parkingFilter
92
193
  });
93
194
  const data = await this.request(`/api/events${query}`);
94
195
  return {
95
196
  events: (data.events ?? []).map(normalizeEvent),
96
- total: data.total
197
+ total: data.total,
198
+ totalExact: data.totalExact ?? data.total !== null,
199
+ hasMore: data.hasMore ?? false,
200
+ page: data.page ?? params.page ?? 1,
201
+ pageSize: data.pageSize ?? params.size ?? 18,
202
+ ...data.degraded === void 0 ? {} : { degraded: data.degraded }
97
203
  };
98
204
  }
99
205
  // ── Listings ──────────────────────────────────────────────────────────────
@@ -102,27 +208,138 @@ var TixBitClient = class {
102
208
  *
103
209
  * @example
104
210
  * ```ts
105
- * const listings = await client.getListings({ eventId: "abc123" });
211
+ * const listings = await client.getListings({ eventId: "EVENT123" });
106
212
  * ```
107
213
  */
108
214
  async getListings(params) {
109
215
  const query = this.qs({
110
216
  size: params.size ?? 50,
111
217
  page: params.page ?? 1,
112
- order_by_direction: params.orderByDirection ?? "asc"
218
+ order_by_direction: params.orderByDirection ?? "asc",
219
+ includeAll: params.includeAll,
220
+ refresh: params.refresh
113
221
  });
114
- const data = await this.request(`/api/events/${encodeURIComponent(params.eventId)}/listings${query}`);
222
+ const normalizedEventId = normalizeExternalEventId(params.eventId);
223
+ const data = await this.request(`/api/events/${encodeURIComponent(normalizedEventId)}/listings${query}`);
115
224
  const listings = (data.data ?? []).map(normalizeListing);
116
225
  return {
117
226
  listings,
118
227
  meta: {
119
- total: data.meta?.total ?? listings.length,
120
- page: data.meta?.page ?? params.page ?? 1,
121
- size: data.meta?.size ?? params.size ?? 50,
228
+ total: readListingMetaNumber(data.meta, "total_count", "total") ?? listings.length,
229
+ page: readListingMetaNumber(data.meta, "current_page_number", "page") ?? params.page ?? 1,
230
+ size: readListingMetaNumber(data.meta, "current_page_size", "size") ?? params.size ?? 50,
231
+ totalPages: readListingMetaNumber(data.meta, "total_pages") ?? void 0,
232
+ currentPageTotalCount: readListingMetaNumber(data.meta, "current_page_total_count") ?? void 0,
233
+ cache: readBoolean(data.meta?.cache) ?? void 0,
234
+ cacheExpiresAt: readNullableString(data.meta?.cache_expires_at),
235
+ cacheState: readEnum(data.meta?.cache_state, ["fresh", "stale", "unavailable"]),
236
+ freshness: readEnum(data.meta?.freshness, ["live", "cached"]),
122
237
  cacheSource: data.meta?.cacheSource
123
238
  }
124
239
  };
125
240
  }
241
+ /** Get one public listing by listing ID. */
242
+ async getListing(listingId) {
243
+ const data = await this.request(`/api/listings/${encodeURIComponent(listingId)}`);
244
+ return {
245
+ listing: normalizeListing(data.listing),
246
+ disclosures: data.disclosures ?? []
247
+ };
248
+ }
249
+ /** Refreshed inventory, not a reservation or negotiated offer. */
250
+ async quoteListings(params) {
251
+ if (!params.eventId || params.eventId !== params.eventId.trim()) throw new TypeError("Copy the exact event ID from search");
252
+ if (params.size !== void 0 && (!Number.isInteger(params.size) || params.size < 1 || params.size > 100)) throw new RangeError("size must be 1-100");
253
+ const result = await this.getListings({ ...params, refresh: true });
254
+ if (result.meta.freshness !== "live") throw new Error("Live inventory is unavailable; no live quote can be provided");
255
+ return result;
256
+ }
257
+ async agentRequest(path, body, token) {
258
+ const base = trustedWebBase(this.baseUrl);
259
+ const headers = { Accept: "application/json", "Content-Type": "application/json" };
260
+ if (token) headers.Authorization = `Bearer ${token}`;
261
+ const response = await fetch(`${base}${path}`, {
262
+ method: body === void 0 ? "GET" : "POST",
263
+ headers,
264
+ redirect: "error",
265
+ signal: AbortSignal.timeout(this.timeoutMs),
266
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
267
+ }).catch(() => {
268
+ throw new Error("Request did not complete; reconcile any write before retrying");
269
+ });
270
+ if (!response.ok) throw new TixBitApiError(`Request failed (HTTP ${response.status})`, response.status, `${base}${path}`);
271
+ const result = await response.json().catch(() => {
272
+ throw new Error("Invalid server response");
273
+ });
274
+ if (result?.success !== true || !result.data || typeof result.data !== "object" || Array.isArray(result.data)) throw new Error("Invalid server response");
275
+ return redact(result, [token ?? ""]);
276
+ }
277
+ /** Public browser entry points. This does not create or import a CLI session. */
278
+ getAuthorizationInfo() {
279
+ return {
280
+ apiKeyRequired: false,
281
+ cliSessionSupported: false,
282
+ signInUrl: "https://www.tixbit.com/sign-in",
283
+ sellerUrl: "https://www.tixbit.com/sell",
284
+ linkWalletUrl: "https://link.com/agents",
285
+ action: "Sign in in your browser for selling or browser checkout. Browser sign-in does not authenticate this CLI. No TixBit developer API key is needed."
286
+ };
287
+ }
288
+ sellerAuthorizationRequired() {
289
+ return {
290
+ success: false,
291
+ status: "authorization_required",
292
+ data: this.getAuthorizationInfo(),
293
+ error: { code: "SELLER_SIGN_IN_REQUIRED", message: "Seller operations require your signed-in user and seller access." },
294
+ action: "Open the sign-in URL, then continue on the seller page. Browser-to-CLI session authorization is not supported. No listing was read or submitted; do not copy browser cookies or tokens."
295
+ };
296
+ }
297
+ /** Stripe Link checkout. No confirmation means quote only. Never retries payment. */
298
+ async buyTickets(params) {
299
+ trustedWebBase(this.baseUrl);
300
+ if (!/^[A-Za-z0-9]{4,12}$/.test(params.listingId)) throw new TypeError("Link supports 4-12 alphanumeric listing IDs only; use browser checkout for native listings");
301
+ if (!Number.isInteger(params.quantity) || params.quantity < 1 || params.quantity > 8) throw new RangeError("quantity must be 1-8");
302
+ if (!Number.isSafeInteger(params.maxAmountCents) || params.maxAmountCents <= 0) throw new RangeError("A positive total maxAmountCents is required");
303
+ const email = normalizeBuyerEmail(params.email);
304
+ if (params.confirm === true && !params.sharedPaymentToken) {
305
+ return {
306
+ success: false,
307
+ status: "authorization_required",
308
+ data: { checkoutUrl: this.createCheckoutLink(params).url, maxAmountCents: params.maxAmountCents, automaticPayment: false },
309
+ error: { code: "PAYMENT_AUTHORIZATION_REQUIRED", message: "A user-approved payment method is required, not a TixBit API key." },
310
+ action: "Open the checkout URL to sign in and pay in your browser. Review and approve the final total there; this CLI cap is not transferred to browser checkout. No payment was sent. Automatic Link authorization is unavailable without public merchant discovery."
311
+ };
312
+ }
313
+ if (params.confirm === true && !/^spt_[A-Za-z0-9_]+$/.test(params.sharedPaymentToken ?? "")) throw new TypeError("Invalid user-approved Link payment credential");
314
+ const body = { listingId: params.listingId, quantity: params.quantity, email, maxAmountCents: params.maxAmountCents };
315
+ const quote = await this.agentRequest("/api/agentic/checkout", body);
316
+ const validQuote = (data) => data && data.listingId === params.listingId && data.quantity === params.quantity && data.currency === "usd" && Number.isSafeInteger(data.amountCents) && data.amountCents > 0 && data.amountCents <= params.maxAmountCents;
317
+ if (!validQuote(quote.data)) throw new Error("Checkout quote is invalid or exceeds the approved total; no payment sent");
318
+ if (params.confirm !== true) return quote;
319
+ if (quote.data?.maxAmountCentsSupported !== true) throw new Error("Server does not support the total cap; no payment sent");
320
+ try {
321
+ const paid = await this.agentRequest("/api/agentic/checkout", { ...body, sharedPaymentToken: params.sharedPaymentToken });
322
+ if (!validQuote(paid.data) || typeof paid.data?.orderId !== "string" || typeof paid.data?.status !== "string") throw new Error("Invalid payment outcome");
323
+ return { success: true, data: { listingId: params.listingId, quantity: params.quantity, amountCents: paid.data.amountCents, currency: "usd", orderId: paid.data.orderId, status: paid.data.status }, action: "Payment accepted. Check order fulfillment before any further purchase." };
324
+ } catch {
325
+ return { success: false, status: "unknown", error: { code: "PAYMENT_OUTCOME_UNKNOWN", message: "Payment or fulfillment status is unknown." }, action: "Do not retry or send another token. Reconcile this purchase with TixBit support and Stripe Link first." };
326
+ }
327
+ }
328
+ async listSellerListings(accessToken = "") {
329
+ if (!accessToken) return this.sellerAuthorizationRequired();
330
+ return this.agentRequest("/api/sell/listings", void 0, accessToken);
331
+ }
332
+ async createSellerListing(body, accessToken, confirm) {
333
+ trustedWebBase(this.baseUrl);
334
+ if (confirm !== true) throw new TypeError("Seller creation requires explicit confirmation");
335
+ if (!accessToken) return this.sellerAuthorizationRequired();
336
+ if (!body || typeof body !== "object" || Array.isArray(body) || !("termsAccepted" in body) || body.termsAccepted !== true) throw new TypeError("Listing JSON must explicitly include termsAccepted: true after seller approval");
337
+ try {
338
+ return await this.agentRequest("/api/sell/listings", body, accessToken);
339
+ } catch {
340
+ return { success: false, status: "unknown", error: { code: "SELLER_CREATE_UNCONFIRMED", message: "Listing creation was not confirmed." }, action: "Read sell list and reconcile before retrying. Submission does not guarantee a live listing." };
341
+ }
342
+ }
126
343
  // ── Checkout Link ──────────────────────────────────────────────────────────
127
344
  /**
128
345
  * Create a checkout link for a listing.
@@ -133,29 +350,148 @@ var TixBitClient = class {
133
350
  * @example
134
351
  * ```ts
135
352
  * const link = client.createCheckoutLink({
136
- * listingId: "P2JO5OBX",
353
+ * listingId: "LISTING123",
137
354
  * quantity: 2,
138
355
  * });
139
356
  *
140
357
  * console.log(link.url);
141
- * // → "https://tixbit.com/checkout/process?listing=P2JO5OBX&quantity=2"
358
+ * // → "https://www.tixbit.com/checkout/process?listing=LISTING123&quantity=2"
142
359
  * ```
143
360
  */
144
361
  createCheckoutLink(params) {
145
- const quantity = Math.max(1, Math.min(8, Math.round(params.quantity)));
146
- const url = `${this.baseUrl}/checkout/process?listing=${encodeURIComponent(params.listingId)}&quantity=${quantity}`;
362
+ const listingId = params.listingId;
363
+ if (!/^[A-Za-z0-9_-]{2,50}$/.test(listingId)) {
364
+ throw new TypeError(
365
+ "listingId must be 2-50 letters, numbers, hyphens, or underscores"
366
+ );
367
+ }
368
+ if (!Number.isInteger(params.quantity) || params.quantity < 1 || params.quantity > 8) {
369
+ throw new RangeError("quantity must be an integer from 1 to 8");
370
+ }
371
+ const url = `${this.baseUrl}/checkout/process?listing=${encodeURIComponent(listingId)}&quantity=${params.quantity}`;
147
372
  return {
148
373
  url,
149
- listingId: params.listingId,
150
- quantity
374
+ listingId,
375
+ quantity: params.quantity
151
376
  };
152
377
  }
378
+ // ── Machine checkout ─────────────────────────────────────────────────────
379
+ /**
380
+ * Purchase tickets through the public MPP endpoint.
381
+ *
382
+ * Configure `paymentFetch` with the official mppx client to automatically
383
+ * answer the server's 402 challenge. Reuse the returned idempotency key for
384
+ * any recovery check after a timeout or pending result.
385
+ */
386
+ async purchaseTickets(params) {
387
+ const { listingId, quantity, email, idempotencyKey, name } = normalizePurchaseTicketsParams(params);
388
+ const controller = new AbortController();
389
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
390
+ try {
391
+ const response = await this.paymentFetch(this.paymentEndpoint, {
392
+ method: "POST",
393
+ redirect: "error",
394
+ headers: {
395
+ Accept: "application/json",
396
+ "Content-Type": "application/json",
397
+ "User-Agent": USER_AGENT
398
+ },
399
+ body: JSON.stringify({
400
+ listingId,
401
+ quantity,
402
+ email,
403
+ idempotencyKey,
404
+ ...name ? { name } : {}
405
+ }),
406
+ signal: controller.signal
407
+ });
408
+ const body = await response.json().catch(() => ({}));
409
+ const orderReference = normalizeOrderReference(body.orderReference);
410
+ const receiptUrl = normalizeReceiptUrl(body.receiptUrl);
411
+ if (response.status === 402) {
412
+ const order = normalizePurchaseOrder(
413
+ body.order,
414
+ orderReference,
415
+ "payment_required"
416
+ );
417
+ return {
418
+ success: false,
419
+ status: "payment_required",
420
+ idempotencyKey,
421
+ orderReference,
422
+ receiptUrl,
423
+ ...order ? { order } : {},
424
+ action: "Configure the official mppx payment client, then retry with the same idempotency key."
425
+ };
426
+ }
427
+ if (isPurchaseStatus(body.status)) {
428
+ if (body.status === "fulfilled" && response.ok && body.success === true && !orderReference) {
429
+ return {
430
+ success: false,
431
+ status: "pending",
432
+ idempotencyKey,
433
+ orderReference: null,
434
+ receiptUrl,
435
+ error: {
436
+ code: "PURCHASE_OUTCOME_AMBIGUOUS",
437
+ message: "The purchase response did not include an order reference."
438
+ },
439
+ action: "Retry with the same idempotency key. Do not create a new payment or purchase attempt."
440
+ };
441
+ }
442
+ if (body.status === "fulfilled" && (!response.ok || body.success !== true)) {
443
+ return rejectedPurchaseResult(
444
+ idempotencyKey,
445
+ orderReference,
446
+ receiptUrl,
447
+ response.status
448
+ );
449
+ }
450
+ const order = normalizePurchaseOrder(body.order, orderReference, body.status);
451
+ return {
452
+ success: body.status === "fulfilled",
453
+ status: body.status,
454
+ idempotencyKey,
455
+ orderReference,
456
+ receiptUrl,
457
+ ...order ? { order } : {},
458
+ ...body.status === "manual_review_required" ? { paid: true } : {},
459
+ ...safePurchaseOutcomeDetails(body.status)
460
+ };
461
+ }
462
+ return rejectedPurchaseResult(
463
+ idempotencyKey,
464
+ orderReference,
465
+ receiptUrl,
466
+ response.status
467
+ );
468
+ } catch {
469
+ return {
470
+ success: false,
471
+ status: "pending",
472
+ idempotencyKey,
473
+ orderReference: null,
474
+ receiptUrl: null,
475
+ error: {
476
+ code: controller.signal.aborted ? "PURCHASE_TIMEOUT" : "PURCHASE_NETWORK_ERROR",
477
+ message: controller.signal.aborted ? `Purchase status is unknown after ${this.timeoutMs}ms.` : "Purchase status is unknown because the request did not complete."
478
+ },
479
+ action: "Retry with the same idempotency key. Do not create a new payment or purchase attempt."
480
+ };
481
+ } finally {
482
+ clearTimeout(timer);
483
+ }
484
+ }
153
485
  // ── Event URL helper ──────────────────────────────────────────────────────
154
486
  /**
155
- * Build the direct URL to an event page on tixbit.com.
487
+ * Build the direct URL to an event page on www.tixbit.com.
156
488
  */
157
489
  eventUrl(slugOrId) {
158
- return `${this.baseUrl}/events/${slugOrId}`;
490
+ if (slugOrId.includes("/")) {
491
+ return `${this.baseUrl}/events/${slugOrId}`;
492
+ }
493
+ const normalized = normalizeExternalEventId(slugOrId);
494
+ return `${this.baseUrl}/events/${normalized}`;
159
495
  }
160
496
  // ── Seatmap ───────────────────────────────────────────────────────────────
161
497
  /**
@@ -167,33 +503,33 @@ var TixBitClient = class {
167
503
  *
168
504
  * @example
169
505
  * ```ts
170
- * const seatmap = await client.getSeatmap({ eventId: "4BKJMDZ" });
506
+ * const seatmap = await client.getSeatmap({ eventId: "EVENT123" });
171
507
  * console.log(seatmap.venue_name); // "State Farm Arena"
172
508
  * console.log(seatmap.section_names); // ["101", "102", ...]
173
509
  * ```
174
510
  */
175
511
  async getSeatmap(params) {
176
- const data = await this.request(`/api/events/${encodeURIComponent(params.eventId)}/seating-chart`);
512
+ const normalizedEventId = normalizeExternalEventId(params.eventId);
513
+ const data = await this.request(`/api/events/${encodeURIComponent(normalizedEventId)}/seating-chart`);
177
514
  let zones = [];
178
515
  let sectionNames = [];
179
- if (data.has_coordinates && data.coordinates) {
516
+ const coordinatesUrl = data.coordinates_url ?? data.coordinates ?? null;
517
+ if (data.has_coordinates && coordinatesUrl) {
180
518
  try {
181
- zones = await this.fetchAndParseCoordinates(data.coordinates);
182
- sectionNames = zones.flatMap(
183
- (z) => z.sections.map((s) => s.name)
184
- );
519
+ zones = await this.fetchAndParseCoordinates(coordinatesUrl);
520
+ sectionNames = zones.flatMap((z) => z.sections.map((s) => s.name));
185
521
  } catch {
186
522
  }
187
523
  }
188
524
  return {
189
525
  success: data.success,
190
- event_id: data.event_id,
526
+ event_id: normalizeExternalEventId(data.event_id),
191
527
  venue_id: data.venue_id,
192
528
  venue_name: data.venue_name,
193
529
  configuration_id: data.configuration_id,
194
530
  configuration_name: data.configuration_name,
195
- background_image: data.background_image ?? null,
196
- coordinates_url: data.coordinates ?? null,
531
+ background_image: toAbsoluteUrl(this.baseUrl, data.background_image ?? null),
532
+ coordinates_url: toAbsoluteUrl(this.baseUrl, coordinatesUrl),
197
533
  has_coordinates: data.has_coordinates,
198
534
  capacity: data.capacity ?? null,
199
535
  venue: data.venue_data,
@@ -223,12 +559,22 @@ var TixBitClient = class {
223
559
  id: zone.id,
224
560
  name: zone.name,
225
561
  sections: (zone.sections ?? []).map((section) => {
226
- const label = section.labels?.[0];
562
+ const labels = Array.isArray(section.labels) ? section.labels.filter((label) => label && typeof label.text === "string").map((label) => ({
563
+ text: label.text,
564
+ x: typeof label.x === "number" ? label.x : 0,
565
+ y: typeof label.y === "number" ? label.y : 0,
566
+ size: typeof label.size === "number" ? label.size : void 0,
567
+ angle: typeof label.angle === "number" ? label.angle : void 0
568
+ })) : [];
569
+ const primaryLabel = labels.find((label) => label.text.toUpperCase() === section.name.toUpperCase()) ?? labels[0];
570
+ const shapePath = section.shape && typeof section.shape.path === "string" ? section.shape.path : null;
227
571
  return {
228
572
  id: section.id,
229
573
  name: section.name,
230
- x: label?.x ?? 0,
231
- y: label?.y ?? 0
574
+ x: primaryLabel?.x ?? 0,
575
+ y: primaryLabel?.y ?? 0,
576
+ labels,
577
+ shape_path: shapePath
232
578
  };
233
579
  })
234
580
  }));
@@ -237,20 +583,148 @@ var TixBitClient = class {
237
583
  }
238
584
  }
239
585
  };
586
+ function normalizeBuyerEmail(value) {
587
+ const email = value.trim().toLowerCase();
588
+ const match = email.match(/^([^\s@]{1,64})@([^\s@]{1,253})$/);
589
+ if (!match || email.length > 254 || match[1].startsWith(".") || match[1].endsWith(".") || match[1].includes("..") || !match[2].includes(".") || match[2].split(".").some(
590
+ (label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)
591
+ )) {
592
+ throw new TypeError("email must be a valid email address");
593
+ }
594
+ return email;
595
+ }
596
+ function normalizePurchaseTicketsParams(params) {
597
+ const listingId = params.listingId;
598
+ if (!/^[A-Za-z0-9_-]{2,100}$/.test(listingId)) {
599
+ throw new TypeError(
600
+ "listingId must be 2-100 letters, numbers, hyphens, or underscores"
601
+ );
602
+ }
603
+ if (!Number.isInteger(params.quantity) || params.quantity < 1 || params.quantity > 8) {
604
+ throw new RangeError("quantity must be an integer from 1 to 8");
605
+ }
606
+ const email = normalizeBuyerEmail(params.email);
607
+ const idempotencyKey = params.idempotencyKey ?? crypto.randomUUID();
608
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
609
+ idempotencyKey
610
+ )) {
611
+ throw new TypeError("idempotencyKey must be an unguessable UUID v4");
612
+ }
613
+ const name = params.name?.trim();
614
+ if (name && name.length > 120) {
615
+ throw new TypeError("name must be at most 120 characters");
616
+ }
617
+ return { listingId, quantity: params.quantity, email, idempotencyKey, name };
618
+ }
619
+ function normalizePurchaseOrder(value, orderReference, status) {
620
+ if (!value || typeof value !== "object" || !orderReference) return void 0;
621
+ const order = value;
622
+ if (order.reference !== orderReference || order.status !== status || typeof order.listingId !== "string" || !/^[A-Za-z0-9_-]{2,100}$/.test(order.listingId) || !Number.isInteger(order.quantity) || order.quantity < 1 || order.quantity > 8 || !isNonNegativeNumber(order.pricePerTicket) || !isNonNegativeNumber(order.subtotal) || !isNonNegativeNumber(order.fees) || !isNonNegativeNumber(order.total) || typeof order.currency !== "string" || !/^[A-Z]{3}$/.test(order.currency)) {
623
+ return void 0;
624
+ }
625
+ return {
626
+ reference: orderReference,
627
+ status,
628
+ listingId: order.listingId,
629
+ quantity: order.quantity,
630
+ pricePerTicket: order.pricePerTicket,
631
+ subtotal: order.subtotal,
632
+ fees: order.fees,
633
+ total: order.total,
634
+ currency: order.currency,
635
+ ...typeof order.section === "string" && order.section.length <= 120 ? { section: order.section } : {},
636
+ ...typeof order.row === "string" && order.row.length <= 120 ? { row: order.row } : {}
637
+ };
638
+ }
639
+ function isNonNegativeNumber(value) {
640
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
641
+ }
642
+ function normalizeOrderReference(value) {
643
+ return typeof value === "string" && /^TBM-[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{10}$/.test(value) ? value : null;
644
+ }
645
+ function normalizeReceiptUrl(value) {
646
+ if (typeof value !== "string") return null;
647
+ try {
648
+ const url = new URL(value);
649
+ return url.protocol === "https:" && !url.username && !url.password ? url.toString() : null;
650
+ } catch {
651
+ return null;
652
+ }
653
+ }
654
+ function safePurchaseOutcomeDetails(status) {
655
+ switch (status) {
656
+ case "pending":
657
+ return {
658
+ error: {
659
+ code: "PURCHASE_PENDING",
660
+ message: "Purchase status is still pending."
661
+ },
662
+ action: "Retry with the same idempotency key. Do not create a new payment or purchase attempt."
663
+ };
664
+ case "manual_review_required":
665
+ return {
666
+ error: {
667
+ code: "FULFILLMENT_REVIEW_REQUIRED",
668
+ message: "Payment was received, but fulfillment requires manual review."
669
+ },
670
+ action: "Do not submit another payment or purchase. Contact TixBit support with the order reference."
671
+ };
672
+ case "payment_failed":
673
+ return {
674
+ error: {
675
+ code: "PAYMENT_VERIFICATION_FAILED",
676
+ message: "Payment verification failed."
677
+ },
678
+ action: "Obtain a new MPP credential for the same idempotency key."
679
+ };
680
+ case "rejected":
681
+ return {
682
+ error: {
683
+ code: "PURCHASE_REJECTED",
684
+ message: "The purchase request was rejected."
685
+ },
686
+ action: "Correct the request and retry with the same idempotency key."
687
+ };
688
+ default:
689
+ return {};
690
+ }
691
+ }
692
+ function rejectedPurchaseResult(idempotencyKey, orderReference, receiptUrl, httpStatus) {
693
+ return {
694
+ success: false,
695
+ status: "rejected",
696
+ idempotencyKey,
697
+ orderReference,
698
+ receiptUrl,
699
+ error: {
700
+ code: `HTTP_${httpStatus}`,
701
+ message: "Purchase request was rejected or returned an invalid response."
702
+ },
703
+ action: "Review the request and retry with the same idempotency key if appropriate."
704
+ };
705
+ }
706
+ function isPurchaseStatus(value) {
707
+ return value === "fulfilled" || value === "payment_required" || value === "pending" || value === "manual_review_required" || value === "payment_failed" || value === "rejected";
708
+ }
240
709
  function normalizeEvent(raw) {
241
710
  const e = raw;
711
+ const rawId = str(e.external_event_id) ?? str(e.externalEventId) ?? str(e.id) ?? "";
712
+ const externalEventId = normalizeExternalEventId(rawId);
242
713
  return {
243
- id: str(e.id) ?? str(e.external_event_id) ?? "",
244
- external_event_id: str(e.external_event_id) ?? str(e.externalEventId) ?? str(e.id) ?? "",
245
- slug: str(e.slug) ?? str(e.external_event_id) ?? "",
714
+ id: externalEventId,
715
+ external_event_id: externalEventId,
716
+ slug: str(e.slug) ?? externalEventId,
246
717
  name: str(e.name) ?? str(e.performer) ?? "",
247
718
  date: resolveDate(e),
248
719
  venue_name: str(e.venue_name) ?? str(e.venueName) ?? null,
249
720
  venue_city: str(e.venue_city) ?? str(e.venueCity) ?? null,
250
721
  venue_state: str(e.venue_state) ?? str(e.venueState) ?? null,
722
+ venue_country: str(e.venue_country) ?? str(e.venueCountry) ?? null,
251
723
  image_url: str(e.image_url) ?? str(e.imageUrl) ?? null,
724
+ category_slug: str(e.category_slug) ?? str(e.categorySlug) ?? null,
252
725
  category_name: str(e.category_name) ?? str(e.categoryName) ?? null,
253
726
  category_event_type: str(e.category_event_type) ?? str(e.categoryEventType) ?? null,
727
+ metadata: e.metadata && typeof e.metadata === "object" ? e.metadata : null,
254
728
  has_listings: Boolean(e.has_listings),
255
729
  inventory: normalizeInventory(e.inventory)
256
730
  };
@@ -260,10 +734,17 @@ function normalizeInventory(raw) {
260
734
  return { total_available: 0, min_price: 0, max_price: 0 };
261
735
  }
262
736
  const inv = raw;
737
+ const sources = inv.sources;
263
738
  return {
264
739
  total_available: num(inv.total_available) ?? 0,
265
740
  min_price: num(inv.min_price) ?? 0,
266
- max_price: num(inv.max_price) ?? 0
741
+ max_price: num(inv.max_price) ?? 0,
742
+ ...sources ? {
743
+ sources: {
744
+ local: num(sources.local) ?? 0,
745
+ marketplace: num(sources.marketplace) ?? 0
746
+ }
747
+ } : {}
267
748
  };
268
749
  }
269
750
  function normalizeListing(raw) {
@@ -271,16 +752,30 @@ function normalizeListing(raw) {
271
752
  const attrs = outer.attributes ?? outer;
272
753
  return {
273
754
  id: str(outer.id) ?? str(attrs.id) ?? "",
755
+ event_id: str(attrs.event_id) ?? null,
274
756
  // price_per_ticket is the fee-inclusive per-ticket price (in dollars, not cents)
275
757
  price: num(attrs.price_per_ticket) ?? num(attrs.price) ?? 0,
758
+ total_price: num(attrs.total_price),
759
+ base_price_per_ticket: num(attrs.base_price_per_ticket),
760
+ base_total_price: num(attrs.base_total_price),
761
+ face_value: num(attrs.face_value),
762
+ fee_amount_per_ticket: num(attrs.fee_amount_per_ticket),
763
+ fee_amount_total: num(attrs.fee_amount_total),
764
+ fee_percent: num(attrs.fee_percent),
765
+ currency: str(attrs.currency) ?? "USD",
276
766
  quantity: num(attrs.quantity) ?? num(attrs.available_quantity) ?? 0,
277
767
  quantities_list: Array.isArray(attrs.quantities_list) ? attrs.quantities_list : [],
278
768
  section: str(attrs.section) ?? null,
279
769
  row: str(attrs.row) ?? null,
770
+ seat_from: str(attrs.seat_from) ?? null,
771
+ seat_to: str(attrs.seat_to) ?? null,
280
772
  seat_numbers: str(attrs.seat_numbers) ?? null,
281
773
  listing_hash: str(attrs.listing_hash) ?? "",
282
774
  notes: str(attrs.notes) ?? null,
283
775
  delivery_method: str(attrs.delivery_method) ?? str(attrs.delivery_type) ?? null,
776
+ in_hand_date: str(attrs.in_hand_date) ?? null,
777
+ stock_type: str(attrs.stock_type) ?? null,
778
+ disclosure_ids: Array.isArray(attrs.disclosure_ids) ? attrs.disclosure_ids.filter((id) => typeof id === "string") : [],
284
779
  splits: Array.isArray(attrs.splits) ? attrs.splits : [],
285
780
  raw: outer
286
781
  };
@@ -303,6 +798,23 @@ function str(v) {
303
798
  function num(v) {
304
799
  return typeof v === "number" && Number.isFinite(v) ? v : null;
305
800
  }
801
+ function readBoolean(value) {
802
+ return typeof value === "boolean" ? value : null;
803
+ }
804
+ function readNullableString(value) {
805
+ return value === null ? null : typeof value === "string" ? value : void 0;
806
+ }
807
+ function readEnum(value, choices) {
808
+ return typeof value === "string" && choices.includes(value) ? value : void 0;
809
+ }
810
+ function readListingMetaNumber(meta, ...keys) {
811
+ if (!meta) return null;
812
+ for (const key of keys) {
813
+ const value = num(meta[key]);
814
+ if (value !== null) return value;
815
+ }
816
+ return null;
817
+ }
306
818
  var TixBitApiError = class extends Error {
307
819
  constructor(message, status, url) {
308
820
  super(message);
@@ -311,7 +823,17 @@ var TixBitApiError = class extends Error {
311
823
  this.name = "TixBitApiError";
312
824
  }
313
825
  };
826
+ var TixBitTimeoutError = class extends Error {
827
+ constructor(url, timeoutMs) {
828
+ super(`Request timed out after ${timeoutMs}ms: ${url}`);
829
+ this.url = url;
830
+ this.timeoutMs = timeoutMs;
831
+ this.name = "TixBitTimeoutError";
832
+ }
833
+ };
314
834
  export {
315
835
  TixBitApiError,
316
- TixBitClient
836
+ TixBitClient,
837
+ TixBitTimeoutError,
838
+ assertMppChallenge
317
839
  };