tixbit 0.1.1 → 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,15 +1,60 @@
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";
5
- function normalizeExternalEventId(eventId) {
6
- if (!eventId) return eventId;
7
- const trimmed = eventId.trim();
8
- const providerPrefixedId = trimmed.match(/^([a-z]{5,})-([A-Z0-9]{6,})$/i);
9
- if (providerPrefixedId?.[2]) {
10
- return providerPrefixedId[2];
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");
11
38
  }
12
- return trimmed;
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;
13
58
  }
14
59
  function toAbsoluteUrl(baseUrl, value) {
15
60
  if (!value) return null;
@@ -21,37 +66,44 @@ function toAbsoluteUrl(baseUrl, value) {
21
66
  var TixBitClient = class {
22
67
  baseUrl;
23
68
  timeoutMs;
24
- apiKey;
69
+ paymentEndpoint;
70
+ paymentFetch;
25
71
  constructor(config = {}) {
26
72
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
27
73
  this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28
- 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));
29
78
  }
30
79
  // ── HTTP helpers ──────────────────────────────────────────────────────────
31
- async request(path, init) {
80
+ async request(path) {
32
81
  const url = `${this.baseUrl}${path}`;
33
82
  const controller = new AbortController();
34
83
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
35
84
  const headers = {
36
85
  Accept: "application/json",
37
- "User-Agent": USER_AGENT,
38
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
86
+ "User-Agent": USER_AGENT
39
87
  };
40
88
  try {
41
89
  const res = await fetch(url, {
42
- ...init,
43
- headers: { ...headers, ...init?.headers },
90
+ redirect: "error",
91
+ headers,
44
92
  signal: controller.signal
45
93
  });
46
94
  if (!res.ok) {
47
- const text = await res.text().catch(() => "");
48
95
  throw new TixBitApiError(
49
- `${res.status} ${res.statusText}: ${text.slice(0, 300)}`,
96
+ `Request failed (HTTP ${res.status})`,
50
97
  res.status,
51
98
  url
52
99
  );
53
100
  }
54
101
  return await res.json();
102
+ } catch (error) {
103
+ if (controller.signal.aborted) {
104
+ throw new TixBitTimeoutError(url, this.timeoutMs);
105
+ }
106
+ throw error;
55
107
  } finally {
56
108
  clearTimeout(timer);
57
109
  }
@@ -75,15 +127,40 @@ var TixBitClient = class {
75
127
  city: params.city,
76
128
  state: params.state,
77
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,
78
138
  startDate: params.startDate,
79
139
  endDate: params.endDate,
80
140
  page: params.page,
81
141
  size: params.size ?? 25
82
142
  });
83
143
  const data = await this.request(`/api/events/search${query}`);
144
+ const pagination = data.pagination;
84
145
  return {
85
146
  events: (data.events ?? []).map(normalizeEvent),
86
- 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
87
164
  };
88
165
  }
89
166
  // ── Browse (Location-Aware) ───────────────────────────────────────────────
@@ -97,19 +174,32 @@ var TixBitClient = class {
97
174
  */
98
175
  async browse(params = {}) {
99
176
  const query = this.qs({
177
+ page: params.page ?? 1,
100
178
  size: params.size ?? 18,
101
- context: "homepage",
102
- 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",
103
185
  nearLat: params.latitude,
104
186
  nearLng: params.longitude,
105
187
  preferCity: params.city,
106
188
  preferState: params.state,
107
- categoryEventType: params.categoryEventType
189
+ categoryEventType: params.categoryEventType,
190
+ locationMode: params.locationMode,
191
+ searchScope: params.searchScope,
192
+ parkingFilter: params.parkingFilter
108
193
  });
109
194
  const data = await this.request(`/api/events${query}`);
110
195
  return {
111
196
  events: (data.events ?? []).map(normalizeEvent),
112
- 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 }
113
203
  };
114
204
  }
115
205
  // ── Listings ──────────────────────────────────────────────────────────────
@@ -118,14 +208,16 @@ var TixBitClient = class {
118
208
  *
119
209
  * @example
120
210
  * ```ts
121
- * const listings = await client.getListings({ eventId: "abc123" });
211
+ * const listings = await client.getListings({ eventId: "EVENT123" });
122
212
  * ```
123
213
  */
124
214
  async getListings(params) {
125
215
  const query = this.qs({
126
216
  size: params.size ?? 50,
127
217
  page: params.page ?? 1,
128
- order_by_direction: params.orderByDirection ?? "asc"
218
+ order_by_direction: params.orderByDirection ?? "asc",
219
+ includeAll: params.includeAll,
220
+ refresh: params.refresh
129
221
  });
130
222
  const normalizedEventId = normalizeExternalEventId(params.eventId);
131
223
  const data = await this.request(`/api/events/${encodeURIComponent(normalizedEventId)}/listings${query}`);
@@ -133,13 +225,121 @@ var TixBitClient = class {
133
225
  return {
134
226
  listings,
135
227
  meta: {
136
- total: data.meta?.total ?? listings.length,
137
- page: data.meta?.page ?? params.page ?? 1,
138
- 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"]),
139
237
  cacheSource: data.meta?.cacheSource
140
238
  }
141
239
  };
142
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
+ }
143
343
  // ── Checkout Link ──────────────────────────────────────────────────────────
144
344
  /**
145
345
  * Create a checkout link for a listing.
@@ -150,26 +350,141 @@ var TixBitClient = class {
150
350
  * @example
151
351
  * ```ts
152
352
  * const link = client.createCheckoutLink({
153
- * listingId: "P2JO5OBX",
353
+ * listingId: "LISTING123",
154
354
  * quantity: 2,
155
355
  * });
156
356
  *
157
357
  * console.log(link.url);
158
- * // → "https://tixbit.com/checkout/process?listing=P2JO5OBX&quantity=2"
358
+ * // → "https://www.tixbit.com/checkout/process?listing=LISTING123&quantity=2"
159
359
  * ```
160
360
  */
161
361
  createCheckoutLink(params) {
162
- const quantity = Math.max(1, Math.min(8, Math.round(params.quantity)));
163
- 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}`;
164
372
  return {
165
373
  url,
166
- listingId: params.listingId,
167
- quantity
374
+ listingId,
375
+ quantity: params.quantity
168
376
  };
169
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
+ }
170
485
  // ── Event URL helper ──────────────────────────────────────────────────────
171
486
  /**
172
- * Build the direct URL to an event page on tixbit.com.
487
+ * Build the direct URL to an event page on www.tixbit.com.
173
488
  */
174
489
  eventUrl(slugOrId) {
175
490
  if (slugOrId.includes("/")) {
@@ -188,7 +503,7 @@ var TixBitClient = class {
188
503
  *
189
504
  * @example
190
505
  * ```ts
191
- * const seatmap = await client.getSeatmap({ eventId: "4BKJMDZ" });
506
+ * const seatmap = await client.getSeatmap({ eventId: "EVENT123" });
192
507
  * console.log(seatmap.venue_name); // "State Farm Arena"
193
508
  * console.log(seatmap.section_names); // ["101", "102", ...]
194
509
  * ```
@@ -268,6 +583,129 @@ var TixBitClient = class {
268
583
  }
269
584
  }
270
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
+ }
271
709
  function normalizeEvent(raw) {
272
710
  const e = raw;
273
711
  const rawId = str(e.external_event_id) ?? str(e.externalEventId) ?? str(e.id) ?? "";
@@ -281,9 +719,12 @@ function normalizeEvent(raw) {
281
719
  venue_name: str(e.venue_name) ?? str(e.venueName) ?? null,
282
720
  venue_city: str(e.venue_city) ?? str(e.venueCity) ?? null,
283
721
  venue_state: str(e.venue_state) ?? str(e.venueState) ?? null,
722
+ venue_country: str(e.venue_country) ?? str(e.venueCountry) ?? null,
284
723
  image_url: str(e.image_url) ?? str(e.imageUrl) ?? null,
724
+ category_slug: str(e.category_slug) ?? str(e.categorySlug) ?? null,
285
725
  category_name: str(e.category_name) ?? str(e.categoryName) ?? null,
286
726
  category_event_type: str(e.category_event_type) ?? str(e.categoryEventType) ?? null,
727
+ metadata: e.metadata && typeof e.metadata === "object" ? e.metadata : null,
287
728
  has_listings: Boolean(e.has_listings),
288
729
  inventory: normalizeInventory(e.inventory)
289
730
  };
@@ -293,10 +734,17 @@ function normalizeInventory(raw) {
293
734
  return { total_available: 0, min_price: 0, max_price: 0 };
294
735
  }
295
736
  const inv = raw;
737
+ const sources = inv.sources;
296
738
  return {
297
739
  total_available: num(inv.total_available) ?? 0,
298
740
  min_price: num(inv.min_price) ?? 0,
299
- 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
+ } : {}
300
748
  };
301
749
  }
302
750
  function normalizeListing(raw) {
@@ -304,16 +752,30 @@ function normalizeListing(raw) {
304
752
  const attrs = outer.attributes ?? outer;
305
753
  return {
306
754
  id: str(outer.id) ?? str(attrs.id) ?? "",
755
+ event_id: str(attrs.event_id) ?? null,
307
756
  // price_per_ticket is the fee-inclusive per-ticket price (in dollars, not cents)
308
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",
309
766
  quantity: num(attrs.quantity) ?? num(attrs.available_quantity) ?? 0,
310
767
  quantities_list: Array.isArray(attrs.quantities_list) ? attrs.quantities_list : [],
311
768
  section: str(attrs.section) ?? null,
312
769
  row: str(attrs.row) ?? null,
770
+ seat_from: str(attrs.seat_from) ?? null,
771
+ seat_to: str(attrs.seat_to) ?? null,
313
772
  seat_numbers: str(attrs.seat_numbers) ?? null,
314
773
  listing_hash: str(attrs.listing_hash) ?? "",
315
774
  notes: str(attrs.notes) ?? null,
316
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") : [],
317
779
  splits: Array.isArray(attrs.splits) ? attrs.splits : [],
318
780
  raw: outer
319
781
  };
@@ -336,6 +798,23 @@ function str(v) {
336
798
  function num(v) {
337
799
  return typeof v === "number" && Number.isFinite(v) ? v : null;
338
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
+ }
339
818
  var TixBitApiError = class extends Error {
340
819
  constructor(message, status, url) {
341
820
  super(message);
@@ -344,7 +823,17 @@ var TixBitApiError = class extends Error {
344
823
  this.name = "TixBitApiError";
345
824
  }
346
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
+ };
347
834
  export {
348
835
  TixBitApiError,
349
- TixBitClient
836
+ TixBitClient,
837
+ TixBitTimeoutError,
838
+ assertMppChallenge
350
839
  };