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/cli.js CHANGED
@@ -3,18 +3,78 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
 
6
+ // src/safety.ts
7
+ function usdCents(raw) {
8
+ if (!raw || !/^\d+(\.\d{1,2})?$/.test(raw)) throw new TypeError("--max-price must be a positive total USD amount with at most two decimals");
9
+ const [dollars, cents = ""] = raw.split(".");
10
+ const amount = BigInt(dollars) * 100n + BigInt(cents.padEnd(2, "0"));
11
+ if (amount <= 0n || amount > BigInt(Number.MAX_SAFE_INTEGER)) throw new RangeError("Invalid --max-price");
12
+ return Number(amount);
13
+ }
14
+ function positiveInteger(raw, name, max) {
15
+ const value = Number(raw);
16
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < 1 || value > max) throw new TypeError(`${name} must be an integer from 1 to ${max}`);
17
+ return value;
18
+ }
19
+ function trustedWebBase(base) {
20
+ const url = new URL(base);
21
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
22
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.origin === "https://www.tixbit.com" || local && ["http:", "https:"].includes(url.protocol))) {
23
+ throw new TypeError("Credential requests require https://www.tixbit.com or a loopback QA origin");
24
+ }
25
+ return url.origin;
26
+ }
27
+ function assertMppChallenge(challenge, maxAmountCents) {
28
+ const { amount, currency } = challenge.request;
29
+ 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)) {
30
+ throw new TypeError("MPP challenge exceeds the approved total or uses an unsupported payment asset or intent");
31
+ }
32
+ }
33
+ function redact(value, secrets = []) {
34
+ if (typeof value === "string") {
35
+ let result = value.replace(/\bspt_[A-Za-z0-9_]+/g, "[REDACTED]");
36
+ for (const secret of secrets.filter(Boolean)) result = result.split(secret).join("[REDACTED]");
37
+ return result;
38
+ }
39
+ if (Array.isArray(value)) return value.map((item) => redact(item, secrets));
40
+ 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)]));
41
+ return value;
42
+ }
43
+
44
+ // src/cli.ts
45
+ import { createRequire } from "module";
46
+
6
47
  // src/client.ts
7
- var DEFAULT_BASE_URL = "https://tixbit.com";
48
+ var DEFAULT_BASE_URL = "https://www.tixbit.com";
49
+ var DEFAULT_PAYMENT_ENDPOINT = "https://mcp.tixbit.com/api/purchase";
8
50
  var DEFAULT_TIMEOUT_MS = 15e3;
9
51
  var USER_AGENT = "@tixbit/sdk";
10
- function normalizeExternalEventId(eventId) {
11
- if (!eventId) return eventId;
12
- const trimmed = eventId.trim();
13
- const providerPrefixedId = trimmed.match(/^([a-z]{5,})-([A-Z0-9]{6,})$/i);
14
- if (providerPrefixedId?.[2]) {
15
- return providerPrefixedId[2];
52
+ function normalizePaymentEndpoint(value) {
53
+ let url;
54
+ try {
55
+ url = new URL(value);
56
+ } catch {
57
+ throw new TypeError("paymentEndpoint must be an absolute URL");
58
+ }
59
+ const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
60
+ const isTixBitHost = url.origin === "https://mcp.tixbit.com" && url.pathname === "/api/purchase";
61
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
62
+ throw new TypeError(
63
+ "paymentEndpoint must use HTTPS (HTTP is allowed only for loopback QA)"
64
+ );
65
+ }
66
+ if (url.username || url.password || url.hash || url.search) {
67
+ throw new TypeError("paymentEndpoint must not contain credentials or a fragment");
16
68
  }
17
- return trimmed;
69
+ if (!isLoopback && !isTixBitHost) {
70
+ throw new TypeError(
71
+ "paymentEndpoint must use a TixBit host or a loopback address for local QA"
72
+ );
73
+ }
74
+ return url.toString().replace(/\/$/, "");
75
+ }
76
+ function normalizeExternalEventId(eventId) {
77
+ return eventId;
18
78
  }
19
79
  function toAbsoluteUrl(baseUrl, value) {
20
80
  if (!value) return null;
@@ -26,37 +86,44 @@ function toAbsoluteUrl(baseUrl, value) {
26
86
  var TixBitClient = class {
27
87
  baseUrl;
28
88
  timeoutMs;
29
- apiKey;
89
+ paymentEndpoint;
90
+ paymentFetch;
30
91
  constructor(config = {}) {
31
92
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
32
93
  this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
33
- this.apiKey = config.apiKey;
94
+ this.paymentEndpoint = normalizePaymentEndpoint(
95
+ config.paymentEndpoint ?? DEFAULT_PAYMENT_ENDPOINT
96
+ );
97
+ this.paymentFetch = config.paymentFetch ?? ((input, init) => fetch(input, init));
34
98
  }
35
99
  // ── HTTP helpers ──────────────────────────────────────────────────────────
36
- async request(path, init) {
100
+ async request(path) {
37
101
  const url = `${this.baseUrl}${path}`;
38
102
  const controller = new AbortController();
39
103
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
40
104
  const headers = {
41
105
  Accept: "application/json",
42
- "User-Agent": USER_AGENT,
43
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
106
+ "User-Agent": USER_AGENT
44
107
  };
45
108
  try {
46
109
  const res = await fetch(url, {
47
- ...init,
48
- headers: { ...headers, ...init?.headers },
110
+ redirect: "error",
111
+ headers,
49
112
  signal: controller.signal
50
113
  });
51
114
  if (!res.ok) {
52
- const text = await res.text().catch(() => "");
53
115
  throw new TixBitApiError(
54
- `${res.status} ${res.statusText}: ${text.slice(0, 300)}`,
116
+ `Request failed (HTTP ${res.status})`,
55
117
  res.status,
56
118
  url
57
119
  );
58
120
  }
59
121
  return await res.json();
122
+ } catch (error) {
123
+ if (controller.signal.aborted) {
124
+ throw new TixBitTimeoutError(url, this.timeoutMs);
125
+ }
126
+ throw error;
60
127
  } finally {
61
128
  clearTimeout(timer);
62
129
  }
@@ -80,15 +147,40 @@ var TixBitClient = class {
80
147
  city: params.city,
81
148
  state: params.state,
82
149
  category: params.category,
150
+ league: params.league,
151
+ categoryEventType: params.categoryEventType,
152
+ performerId: params.performerId,
153
+ venueId: params.venueId,
154
+ parkingFilter: params.parkingFilter,
155
+ nearLat: params.nearLat,
156
+ nearLng: params.nearLng,
157
+ locationMode: params.locationMode,
83
158
  startDate: params.startDate,
84
159
  endDate: params.endDate,
85
160
  page: params.page,
86
161
  size: params.size ?? 25
87
162
  });
88
163
  const data = await this.request(`/api/events/search${query}`);
164
+ const pagination = data.pagination;
89
165
  return {
90
166
  events: (data.events ?? []).map(normalizeEvent),
91
- pagination: data.pagination
167
+ pagination: {
168
+ ...pagination,
169
+ totalExact: pagination.totalExact ?? pagination.total !== null
170
+ }
171
+ };
172
+ }
173
+ /** Get one public event by ID or slug. */
174
+ async getEvent(eventId) {
175
+ const normalizedEventId = normalizeExternalEventId(eventId);
176
+ const data = await this.request(
177
+ `/api/events/${encodeURIComponent(normalizedEventId)}`
178
+ );
179
+ const publicEventId = normalizeExternalEventId(data.external_id ?? data.id);
180
+ return {
181
+ ...data,
182
+ id: publicEventId,
183
+ external_id: publicEventId
92
184
  };
93
185
  }
94
186
  // ── Browse (Location-Aware) ───────────────────────────────────────────────
@@ -102,19 +194,32 @@ var TixBitClient = class {
102
194
  */
103
195
  async browse(params = {}) {
104
196
  const query = this.qs({
197
+ page: params.page ?? 1,
105
198
  size: params.size ?? 18,
106
- context: "homepage",
107
- recommendation: "upcoming",
199
+ query: params.query,
200
+ category: params.category,
201
+ league: params.league,
202
+ date: params.date,
203
+ context: params.context ?? "homepage",
204
+ recommendation: params.recommendation ?? "upcoming",
108
205
  nearLat: params.latitude,
109
206
  nearLng: params.longitude,
110
207
  preferCity: params.city,
111
208
  preferState: params.state,
112
- categoryEventType: params.categoryEventType
209
+ categoryEventType: params.categoryEventType,
210
+ locationMode: params.locationMode,
211
+ searchScope: params.searchScope,
212
+ parkingFilter: params.parkingFilter
113
213
  });
114
214
  const data = await this.request(`/api/events${query}`);
115
215
  return {
116
216
  events: (data.events ?? []).map(normalizeEvent),
117
- total: data.total
217
+ total: data.total,
218
+ totalExact: data.totalExact ?? data.total !== null,
219
+ hasMore: data.hasMore ?? false,
220
+ page: data.page ?? params.page ?? 1,
221
+ pageSize: data.pageSize ?? params.size ?? 18,
222
+ ...data.degraded === void 0 ? {} : { degraded: data.degraded }
118
223
  };
119
224
  }
120
225
  // ── Listings ──────────────────────────────────────────────────────────────
@@ -123,14 +228,16 @@ var TixBitClient = class {
123
228
  *
124
229
  * @example
125
230
  * ```ts
126
- * const listings = await client.getListings({ eventId: "abc123" });
231
+ * const listings = await client.getListings({ eventId: "EVENT123" });
127
232
  * ```
128
233
  */
129
234
  async getListings(params) {
130
235
  const query = this.qs({
131
236
  size: params.size ?? 50,
132
237
  page: params.page ?? 1,
133
- order_by_direction: params.orderByDirection ?? "asc"
238
+ order_by_direction: params.orderByDirection ?? "asc",
239
+ includeAll: params.includeAll,
240
+ refresh: params.refresh
134
241
  });
135
242
  const normalizedEventId = normalizeExternalEventId(params.eventId);
136
243
  const data = await this.request(`/api/events/${encodeURIComponent(normalizedEventId)}/listings${query}`);
@@ -138,13 +245,121 @@ var TixBitClient = class {
138
245
  return {
139
246
  listings,
140
247
  meta: {
141
- total: data.meta?.total ?? listings.length,
142
- page: data.meta?.page ?? params.page ?? 1,
143
- size: data.meta?.size ?? params.size ?? 50,
248
+ total: readListingMetaNumber(data.meta, "total_count", "total") ?? listings.length,
249
+ page: readListingMetaNumber(data.meta, "current_page_number", "page") ?? params.page ?? 1,
250
+ size: readListingMetaNumber(data.meta, "current_page_size", "size") ?? params.size ?? 50,
251
+ totalPages: readListingMetaNumber(data.meta, "total_pages") ?? void 0,
252
+ currentPageTotalCount: readListingMetaNumber(data.meta, "current_page_total_count") ?? void 0,
253
+ cache: readBoolean(data.meta?.cache) ?? void 0,
254
+ cacheExpiresAt: readNullableString(data.meta?.cache_expires_at),
255
+ cacheState: readEnum(data.meta?.cache_state, ["fresh", "stale", "unavailable"]),
256
+ freshness: readEnum(data.meta?.freshness, ["live", "cached"]),
144
257
  cacheSource: data.meta?.cacheSource
145
258
  }
146
259
  };
147
260
  }
261
+ /** Get one public listing by listing ID. */
262
+ async getListing(listingId) {
263
+ const data = await this.request(`/api/listings/${encodeURIComponent(listingId)}`);
264
+ return {
265
+ listing: normalizeListing(data.listing),
266
+ disclosures: data.disclosures ?? []
267
+ };
268
+ }
269
+ /** Refreshed inventory, not a reservation or negotiated offer. */
270
+ async quoteListings(params) {
271
+ if (!params.eventId || params.eventId !== params.eventId.trim()) throw new TypeError("Copy the exact event ID from search");
272
+ if (params.size !== void 0 && (!Number.isInteger(params.size) || params.size < 1 || params.size > 100)) throw new RangeError("size must be 1-100");
273
+ const result = await this.getListings({ ...params, refresh: true });
274
+ if (result.meta.freshness !== "live") throw new Error("Live inventory is unavailable; no live quote can be provided");
275
+ return result;
276
+ }
277
+ async agentRequest(path, body, token) {
278
+ const base = trustedWebBase(this.baseUrl);
279
+ const headers = { Accept: "application/json", "Content-Type": "application/json" };
280
+ if (token) headers.Authorization = `Bearer ${token}`;
281
+ const response = await fetch(`${base}${path}`, {
282
+ method: body === void 0 ? "GET" : "POST",
283
+ headers,
284
+ redirect: "error",
285
+ signal: AbortSignal.timeout(this.timeoutMs),
286
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
287
+ }).catch(() => {
288
+ throw new Error("Request did not complete; reconcile any write before retrying");
289
+ });
290
+ if (!response.ok) throw new TixBitApiError(`Request failed (HTTP ${response.status})`, response.status, `${base}${path}`);
291
+ const result = await response.json().catch(() => {
292
+ throw new Error("Invalid server response");
293
+ });
294
+ if (result?.success !== true || !result.data || typeof result.data !== "object" || Array.isArray(result.data)) throw new Error("Invalid server response");
295
+ return redact(result, [token ?? ""]);
296
+ }
297
+ /** Public browser entry points. This does not create or import a CLI session. */
298
+ getAuthorizationInfo() {
299
+ return {
300
+ apiKeyRequired: false,
301
+ cliSessionSupported: false,
302
+ signInUrl: "https://www.tixbit.com/sign-in",
303
+ sellerUrl: "https://www.tixbit.com/sell",
304
+ linkWalletUrl: "https://link.com/agents",
305
+ 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."
306
+ };
307
+ }
308
+ sellerAuthorizationRequired() {
309
+ return {
310
+ success: false,
311
+ status: "authorization_required",
312
+ data: this.getAuthorizationInfo(),
313
+ error: { code: "SELLER_SIGN_IN_REQUIRED", message: "Seller operations require your signed-in user and seller access." },
314
+ 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."
315
+ };
316
+ }
317
+ /** Stripe Link checkout. No confirmation means quote only. Never retries payment. */
318
+ async buyTickets(params) {
319
+ trustedWebBase(this.baseUrl);
320
+ 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");
321
+ if (!Number.isInteger(params.quantity) || params.quantity < 1 || params.quantity > 8) throw new RangeError("quantity must be 1-8");
322
+ if (!Number.isSafeInteger(params.maxAmountCents) || params.maxAmountCents <= 0) throw new RangeError("A positive total maxAmountCents is required");
323
+ const email = normalizeBuyerEmail(params.email);
324
+ if (params.confirm === true && !params.sharedPaymentToken) {
325
+ return {
326
+ success: false,
327
+ status: "authorization_required",
328
+ data: { checkoutUrl: this.createCheckoutLink(params).url, maxAmountCents: params.maxAmountCents, automaticPayment: false },
329
+ error: { code: "PAYMENT_AUTHORIZATION_REQUIRED", message: "A user-approved payment method is required, not a TixBit API key." },
330
+ 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."
331
+ };
332
+ }
333
+ if (params.confirm === true && !/^spt_[A-Za-z0-9_]+$/.test(params.sharedPaymentToken ?? "")) throw new TypeError("Invalid user-approved Link payment credential");
334
+ const body = { listingId: params.listingId, quantity: params.quantity, email, maxAmountCents: params.maxAmountCents };
335
+ const quote = await this.agentRequest("/api/agentic/checkout", body);
336
+ 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;
337
+ if (!validQuote(quote.data)) throw new Error("Checkout quote is invalid or exceeds the approved total; no payment sent");
338
+ if (params.confirm !== true) return quote;
339
+ if (quote.data?.maxAmountCentsSupported !== true) throw new Error("Server does not support the total cap; no payment sent");
340
+ try {
341
+ const paid = await this.agentRequest("/api/agentic/checkout", { ...body, sharedPaymentToken: params.sharedPaymentToken });
342
+ if (!validQuote(paid.data) || typeof paid.data?.orderId !== "string" || typeof paid.data?.status !== "string") throw new Error("Invalid payment outcome");
343
+ 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." };
344
+ } catch {
345
+ 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." };
346
+ }
347
+ }
348
+ async listSellerListings(accessToken = "") {
349
+ if (!accessToken) return this.sellerAuthorizationRequired();
350
+ return this.agentRequest("/api/sell/listings", void 0, accessToken);
351
+ }
352
+ async createSellerListing(body, accessToken, confirm) {
353
+ trustedWebBase(this.baseUrl);
354
+ if (confirm !== true) throw new TypeError("Seller creation requires explicit confirmation");
355
+ if (!accessToken) return this.sellerAuthorizationRequired();
356
+ 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");
357
+ try {
358
+ return await this.agentRequest("/api/sell/listings", body, accessToken);
359
+ } catch {
360
+ 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." };
361
+ }
362
+ }
148
363
  // ── Checkout Link ──────────────────────────────────────────────────────────
149
364
  /**
150
365
  * Create a checkout link for a listing.
@@ -155,26 +370,141 @@ var TixBitClient = class {
155
370
  * @example
156
371
  * ```ts
157
372
  * const link = client.createCheckoutLink({
158
- * listingId: "P2JO5OBX",
373
+ * listingId: "LISTING123",
159
374
  * quantity: 2,
160
375
  * });
161
376
  *
162
377
  * console.log(link.url);
163
- * // → "https://tixbit.com/checkout/process?listing=P2JO5OBX&quantity=2"
378
+ * // → "https://www.tixbit.com/checkout/process?listing=LISTING123&quantity=2"
164
379
  * ```
165
380
  */
166
381
  createCheckoutLink(params) {
167
- const quantity = Math.max(1, Math.min(8, Math.round(params.quantity)));
168
- const url = `${this.baseUrl}/checkout/process?listing=${encodeURIComponent(params.listingId)}&quantity=${quantity}`;
382
+ const listingId = params.listingId;
383
+ if (!/^[A-Za-z0-9_-]{2,50}$/.test(listingId)) {
384
+ throw new TypeError(
385
+ "listingId must be 2-50 letters, numbers, hyphens, or underscores"
386
+ );
387
+ }
388
+ if (!Number.isInteger(params.quantity) || params.quantity < 1 || params.quantity > 8) {
389
+ throw new RangeError("quantity must be an integer from 1 to 8");
390
+ }
391
+ const url = `${this.baseUrl}/checkout/process?listing=${encodeURIComponent(listingId)}&quantity=${params.quantity}`;
169
392
  return {
170
393
  url,
171
- listingId: params.listingId,
172
- quantity
394
+ listingId,
395
+ quantity: params.quantity
173
396
  };
174
397
  }
398
+ // ── Machine checkout ─────────────────────────────────────────────────────
399
+ /**
400
+ * Purchase tickets through the public MPP endpoint.
401
+ *
402
+ * Configure `paymentFetch` with the official mppx client to automatically
403
+ * answer the server's 402 challenge. Reuse the returned idempotency key for
404
+ * any recovery check after a timeout or pending result.
405
+ */
406
+ async purchaseTickets(params) {
407
+ const { listingId, quantity, email, idempotencyKey, name } = normalizePurchaseTicketsParams(params);
408
+ const controller = new AbortController();
409
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
410
+ try {
411
+ const response = await this.paymentFetch(this.paymentEndpoint, {
412
+ method: "POST",
413
+ redirect: "error",
414
+ headers: {
415
+ Accept: "application/json",
416
+ "Content-Type": "application/json",
417
+ "User-Agent": USER_AGENT
418
+ },
419
+ body: JSON.stringify({
420
+ listingId,
421
+ quantity,
422
+ email,
423
+ idempotencyKey,
424
+ ...name ? { name } : {}
425
+ }),
426
+ signal: controller.signal
427
+ });
428
+ const body = await response.json().catch(() => ({}));
429
+ const orderReference = normalizeOrderReference(body.orderReference);
430
+ const receiptUrl = normalizeReceiptUrl(body.receiptUrl);
431
+ if (response.status === 402) {
432
+ const order = normalizePurchaseOrder(
433
+ body.order,
434
+ orderReference,
435
+ "payment_required"
436
+ );
437
+ return {
438
+ success: false,
439
+ status: "payment_required",
440
+ idempotencyKey,
441
+ orderReference,
442
+ receiptUrl,
443
+ ...order ? { order } : {},
444
+ action: "Configure the official mppx payment client, then retry with the same idempotency key."
445
+ };
446
+ }
447
+ if (isPurchaseStatus(body.status)) {
448
+ if (body.status === "fulfilled" && response.ok && body.success === true && !orderReference) {
449
+ return {
450
+ success: false,
451
+ status: "pending",
452
+ idempotencyKey,
453
+ orderReference: null,
454
+ receiptUrl,
455
+ error: {
456
+ code: "PURCHASE_OUTCOME_AMBIGUOUS",
457
+ message: "The purchase response did not include an order reference."
458
+ },
459
+ action: "Retry with the same idempotency key. Do not create a new payment or purchase attempt."
460
+ };
461
+ }
462
+ if (body.status === "fulfilled" && (!response.ok || body.success !== true)) {
463
+ return rejectedPurchaseResult(
464
+ idempotencyKey,
465
+ orderReference,
466
+ receiptUrl,
467
+ response.status
468
+ );
469
+ }
470
+ const order = normalizePurchaseOrder(body.order, orderReference, body.status);
471
+ return {
472
+ success: body.status === "fulfilled",
473
+ status: body.status,
474
+ idempotencyKey,
475
+ orderReference,
476
+ receiptUrl,
477
+ ...order ? { order } : {},
478
+ ...body.status === "manual_review_required" ? { paid: true } : {},
479
+ ...safePurchaseOutcomeDetails(body.status)
480
+ };
481
+ }
482
+ return rejectedPurchaseResult(
483
+ idempotencyKey,
484
+ orderReference,
485
+ receiptUrl,
486
+ response.status
487
+ );
488
+ } catch {
489
+ return {
490
+ success: false,
491
+ status: "pending",
492
+ idempotencyKey,
493
+ orderReference: null,
494
+ receiptUrl: null,
495
+ error: {
496
+ code: controller.signal.aborted ? "PURCHASE_TIMEOUT" : "PURCHASE_NETWORK_ERROR",
497
+ message: controller.signal.aborted ? `Purchase status is unknown after ${this.timeoutMs}ms.` : "Purchase status is unknown because the request did not complete."
498
+ },
499
+ action: "Retry with the same idempotency key. Do not create a new payment or purchase attempt."
500
+ };
501
+ } finally {
502
+ clearTimeout(timer);
503
+ }
504
+ }
175
505
  // ── Event URL helper ──────────────────────────────────────────────────────
176
506
  /**
177
- * Build the direct URL to an event page on tixbit.com.
507
+ * Build the direct URL to an event page on www.tixbit.com.
178
508
  */
179
509
  eventUrl(slugOrId) {
180
510
  if (slugOrId.includes("/")) {
@@ -193,7 +523,7 @@ var TixBitClient = class {
193
523
  *
194
524
  * @example
195
525
  * ```ts
196
- * const seatmap = await client.getSeatmap({ eventId: "4BKJMDZ" });
526
+ * const seatmap = await client.getSeatmap({ eventId: "EVENT123" });
197
527
  * console.log(seatmap.venue_name); // "State Farm Arena"
198
528
  * console.log(seatmap.section_names); // ["101", "102", ...]
199
529
  * ```
@@ -273,6 +603,129 @@ var TixBitClient = class {
273
603
  }
274
604
  }
275
605
  };
606
+ function normalizeBuyerEmail(value) {
607
+ const email = value.trim().toLowerCase();
608
+ const match = email.match(/^([^\s@]{1,64})@([^\s@]{1,253})$/);
609
+ if (!match || email.length > 254 || match[1].startsWith(".") || match[1].endsWith(".") || match[1].includes("..") || !match[2].includes(".") || match[2].split(".").some(
610
+ (label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)
611
+ )) {
612
+ throw new TypeError("email must be a valid email address");
613
+ }
614
+ return email;
615
+ }
616
+ function normalizePurchaseTicketsParams(params) {
617
+ const listingId = params.listingId;
618
+ if (!/^[A-Za-z0-9_-]{2,100}$/.test(listingId)) {
619
+ throw new TypeError(
620
+ "listingId must be 2-100 letters, numbers, hyphens, or underscores"
621
+ );
622
+ }
623
+ if (!Number.isInteger(params.quantity) || params.quantity < 1 || params.quantity > 8) {
624
+ throw new RangeError("quantity must be an integer from 1 to 8");
625
+ }
626
+ const email = normalizeBuyerEmail(params.email);
627
+ const idempotencyKey = params.idempotencyKey ?? crypto.randomUUID();
628
+ 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(
629
+ idempotencyKey
630
+ )) {
631
+ throw new TypeError("idempotencyKey must be an unguessable UUID v4");
632
+ }
633
+ const name = params.name?.trim();
634
+ if (name && name.length > 120) {
635
+ throw new TypeError("name must be at most 120 characters");
636
+ }
637
+ return { listingId, quantity: params.quantity, email, idempotencyKey, name };
638
+ }
639
+ function normalizePurchaseOrder(value, orderReference, status) {
640
+ if (!value || typeof value !== "object" || !orderReference) return void 0;
641
+ const order = value;
642
+ 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)) {
643
+ return void 0;
644
+ }
645
+ return {
646
+ reference: orderReference,
647
+ status,
648
+ listingId: order.listingId,
649
+ quantity: order.quantity,
650
+ pricePerTicket: order.pricePerTicket,
651
+ subtotal: order.subtotal,
652
+ fees: order.fees,
653
+ total: order.total,
654
+ currency: order.currency,
655
+ ...typeof order.section === "string" && order.section.length <= 120 ? { section: order.section } : {},
656
+ ...typeof order.row === "string" && order.row.length <= 120 ? { row: order.row } : {}
657
+ };
658
+ }
659
+ function isNonNegativeNumber(value) {
660
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
661
+ }
662
+ function normalizeOrderReference(value) {
663
+ return typeof value === "string" && /^TBM-[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{10}$/.test(value) ? value : null;
664
+ }
665
+ function normalizeReceiptUrl(value) {
666
+ if (typeof value !== "string") return null;
667
+ try {
668
+ const url = new URL(value);
669
+ return url.protocol === "https:" && !url.username && !url.password ? url.toString() : null;
670
+ } catch {
671
+ return null;
672
+ }
673
+ }
674
+ function safePurchaseOutcomeDetails(status) {
675
+ switch (status) {
676
+ case "pending":
677
+ return {
678
+ error: {
679
+ code: "PURCHASE_PENDING",
680
+ message: "Purchase status is still pending."
681
+ },
682
+ action: "Retry with the same idempotency key. Do not create a new payment or purchase attempt."
683
+ };
684
+ case "manual_review_required":
685
+ return {
686
+ error: {
687
+ code: "FULFILLMENT_REVIEW_REQUIRED",
688
+ message: "Payment was received, but fulfillment requires manual review."
689
+ },
690
+ action: "Do not submit another payment or purchase. Contact TixBit support with the order reference."
691
+ };
692
+ case "payment_failed":
693
+ return {
694
+ error: {
695
+ code: "PAYMENT_VERIFICATION_FAILED",
696
+ message: "Payment verification failed."
697
+ },
698
+ action: "Obtain a new MPP credential for the same idempotency key."
699
+ };
700
+ case "rejected":
701
+ return {
702
+ error: {
703
+ code: "PURCHASE_REJECTED",
704
+ message: "The purchase request was rejected."
705
+ },
706
+ action: "Correct the request and retry with the same idempotency key."
707
+ };
708
+ default:
709
+ return {};
710
+ }
711
+ }
712
+ function rejectedPurchaseResult(idempotencyKey, orderReference, receiptUrl, httpStatus) {
713
+ return {
714
+ success: false,
715
+ status: "rejected",
716
+ idempotencyKey,
717
+ orderReference,
718
+ receiptUrl,
719
+ error: {
720
+ code: `HTTP_${httpStatus}`,
721
+ message: "Purchase request was rejected or returned an invalid response."
722
+ },
723
+ action: "Review the request and retry with the same idempotency key if appropriate."
724
+ };
725
+ }
726
+ function isPurchaseStatus(value) {
727
+ return value === "fulfilled" || value === "payment_required" || value === "pending" || value === "manual_review_required" || value === "payment_failed" || value === "rejected";
728
+ }
276
729
  function normalizeEvent(raw) {
277
730
  const e = raw;
278
731
  const rawId = str(e.external_event_id) ?? str(e.externalEventId) ?? str(e.id) ?? "";
@@ -286,9 +739,12 @@ function normalizeEvent(raw) {
286
739
  venue_name: str(e.venue_name) ?? str(e.venueName) ?? null,
287
740
  venue_city: str(e.venue_city) ?? str(e.venueCity) ?? null,
288
741
  venue_state: str(e.venue_state) ?? str(e.venueState) ?? null,
742
+ venue_country: str(e.venue_country) ?? str(e.venueCountry) ?? null,
289
743
  image_url: str(e.image_url) ?? str(e.imageUrl) ?? null,
744
+ category_slug: str(e.category_slug) ?? str(e.categorySlug) ?? null,
290
745
  category_name: str(e.category_name) ?? str(e.categoryName) ?? null,
291
746
  category_event_type: str(e.category_event_type) ?? str(e.categoryEventType) ?? null,
747
+ metadata: e.metadata && typeof e.metadata === "object" ? e.metadata : null,
292
748
  has_listings: Boolean(e.has_listings),
293
749
  inventory: normalizeInventory(e.inventory)
294
750
  };
@@ -298,10 +754,17 @@ function normalizeInventory(raw) {
298
754
  return { total_available: 0, min_price: 0, max_price: 0 };
299
755
  }
300
756
  const inv = raw;
757
+ const sources = inv.sources;
301
758
  return {
302
759
  total_available: num(inv.total_available) ?? 0,
303
760
  min_price: num(inv.min_price) ?? 0,
304
- max_price: num(inv.max_price) ?? 0
761
+ max_price: num(inv.max_price) ?? 0,
762
+ ...sources ? {
763
+ sources: {
764
+ local: num(sources.local) ?? 0,
765
+ marketplace: num(sources.marketplace) ?? 0
766
+ }
767
+ } : {}
305
768
  };
306
769
  }
307
770
  function normalizeListing(raw) {
@@ -309,16 +772,30 @@ function normalizeListing(raw) {
309
772
  const attrs = outer.attributes ?? outer;
310
773
  return {
311
774
  id: str(outer.id) ?? str(attrs.id) ?? "",
775
+ event_id: str(attrs.event_id) ?? null,
312
776
  // price_per_ticket is the fee-inclusive per-ticket price (in dollars, not cents)
313
777
  price: num(attrs.price_per_ticket) ?? num(attrs.price) ?? 0,
778
+ total_price: num(attrs.total_price),
779
+ base_price_per_ticket: num(attrs.base_price_per_ticket),
780
+ base_total_price: num(attrs.base_total_price),
781
+ face_value: num(attrs.face_value),
782
+ fee_amount_per_ticket: num(attrs.fee_amount_per_ticket),
783
+ fee_amount_total: num(attrs.fee_amount_total),
784
+ fee_percent: num(attrs.fee_percent),
785
+ currency: str(attrs.currency) ?? "USD",
314
786
  quantity: num(attrs.quantity) ?? num(attrs.available_quantity) ?? 0,
315
787
  quantities_list: Array.isArray(attrs.quantities_list) ? attrs.quantities_list : [],
316
788
  section: str(attrs.section) ?? null,
317
789
  row: str(attrs.row) ?? null,
790
+ seat_from: str(attrs.seat_from) ?? null,
791
+ seat_to: str(attrs.seat_to) ?? null,
318
792
  seat_numbers: str(attrs.seat_numbers) ?? null,
319
793
  listing_hash: str(attrs.listing_hash) ?? "",
320
794
  notes: str(attrs.notes) ?? null,
321
795
  delivery_method: str(attrs.delivery_method) ?? str(attrs.delivery_type) ?? null,
796
+ in_hand_date: str(attrs.in_hand_date) ?? null,
797
+ stock_type: str(attrs.stock_type) ?? null,
798
+ disclosure_ids: Array.isArray(attrs.disclosure_ids) ? attrs.disclosure_ids.filter((id) => typeof id === "string") : [],
322
799
  splits: Array.isArray(attrs.splits) ? attrs.splits : [],
323
800
  raw: outer
324
801
  };
@@ -341,6 +818,23 @@ function str(v) {
341
818
  function num(v) {
342
819
  return typeof v === "number" && Number.isFinite(v) ? v : null;
343
820
  }
821
+ function readBoolean(value) {
822
+ return typeof value === "boolean" ? value : null;
823
+ }
824
+ function readNullableString(value) {
825
+ return value === null ? null : typeof value === "string" ? value : void 0;
826
+ }
827
+ function readEnum(value, choices) {
828
+ return typeof value === "string" && choices.includes(value) ? value : void 0;
829
+ }
830
+ function readListingMetaNumber(meta, ...keys) {
831
+ if (!meta) return null;
832
+ for (const key of keys) {
833
+ const value = num(meta[key]);
834
+ if (value !== null) return value;
835
+ }
836
+ return null;
837
+ }
344
838
  var TixBitApiError = class extends Error {
345
839
  constructor(message, status, url) {
346
840
  super(message);
@@ -349,13 +843,30 @@ var TixBitApiError = class extends Error {
349
843
  this.name = "TixBitApiError";
350
844
  }
351
845
  };
846
+ var TixBitTimeoutError = class extends Error {
847
+ constructor(url, timeoutMs) {
848
+ super(`Request timed out after ${timeoutMs}ms: ${url}`);
849
+ this.url = url;
850
+ this.timeoutMs = timeoutMs;
851
+ this.name = "TixBitTimeoutError";
852
+ }
853
+ };
352
854
 
353
855
  // src/cli.ts
354
- var client = new TixBitClient({
355
- baseUrl: process.env.TIXBIT_BASE_URL,
356
- apiKey: process.env.TIXBIT_API_KEY
357
- });
856
+ var client = (() => {
857
+ try {
858
+ return new TixBitClient({ baseUrl: process.env.TIXBIT_BASE_URL });
859
+ } catch (error) {
860
+ return handleError(error);
861
+ }
862
+ })();
863
+ var packageJson = createRequire(import.meta.url)("../package.json");
358
864
  function output(data, json) {
865
+ data = redact(data, [process.env.TIXBIT_LINK_TOKEN ?? "", process.env.TIXBIT_ACCESS_TOKEN ?? "", process.env.MPPX_PRIVATE_KEY ?? ""]);
866
+ if (data && typeof data === "object" && !Array.isArray(data)) {
867
+ data = { success: true, ...data };
868
+ if (data.success === false) process.exitCode = 2;
869
+ }
359
870
  if (json) {
360
871
  process.stdout.write(JSON.stringify(data, null, 2) + "\n");
361
872
  return;
@@ -368,6 +879,30 @@ function output(data, json) {
368
879
  }
369
880
  process.stdout.write(JSON.stringify(data, null, 2) + "\n");
370
881
  }
882
+ function outputPurchaseResult(result) {
883
+ if (result.success && result.order) {
884
+ process.stdout.write(
885
+ `
886
+ Purchase complete
887
+
888
+ Order: ${result.orderReference}
889
+ Tickets: ${result.order.quantity}
890
+ Total: ${result.order.currency} ${result.order.total.toFixed(2)}
891
+ ` + (result.receiptUrl ? ` Receipt: ${result.receiptUrl}
892
+ ` : "") + "\n"
893
+ );
894
+ return;
895
+ }
896
+ process.stdout.write(
897
+ `
898
+ Purchase ${result.status}
899
+
900
+ Order: ${result.orderReference ?? "not assigned"}
901
+ Action: ${result.action ?? "Review the JSON result before retrying."}
902
+
903
+ `
904
+ );
905
+ }
371
906
  function formatItem(item) {
372
907
  if (!item || typeof item !== "object") return String(item);
373
908
  const e = item;
@@ -403,26 +938,33 @@ function formatItem(item) {
403
938
  return JSON.stringify(item, null, 2);
404
939
  }
405
940
  function handleError(err) {
406
- const msg = err instanceof Error ? err.message : String(err);
407
- process.stderr.write(`Error: ${msg}
408
- `);
941
+ const msg = err instanceof TypeError || err instanceof RangeError ? err.message : "Request failed. Check input and service availability; do not retry an uncertain write.";
942
+ output({ success: false, error: { code: "COMMAND_FAILED", message: msg } }, true);
409
943
  process.exit(1);
410
944
  }
411
- var program = new Command().name("tixbit").description("Search events, browse listings, and buy tickets on TixBit").version("0.1.0");
412
- program.command("search [query]").description("Search for events by keyword, city, state, or category").option("--city <city>", "Filter by city").option("--state <state>", "Filter by state (2-letter code)").option("--category <slug>", "Filter by category slug (e.g. nba-basketball)").option("--start-date <date>", "Events on or after this date (ISO)").option("--end-date <date>", "Events on or before this date (ISO)").option("--page <n>", "Page number", "1").option("--size <n>", "Results per page", "10").option("--json", "Output raw JSON (for agents)", false).action(async (query, opts) => {
945
+ var program = new Command().name("tixbit").description("Search events, browse listings, and buy TixBit tickets").version(packageJson.version);
946
+ program.command("search [query]").description("Search for events by keyword, city, state, or category").option("--city <city>", "Filter by city").option("--state <state>", "Filter by state (2-letter code)").option("--category <slug>", "Filter by category slug (e.g. nba-basketball)").option("--league <league>", "Filter by league abbreviation (e.g. NBA)").option("--event-type <type>", "SPORT, CONCERT, THEATER, or ALL").option("--performer-id <id>", "Filter by public performer ID").option("--venue-id <id>", "Filter by public venue ID").option("--parking <mode>", "Parking filter: exclude, only, or include").option("--near-lat <lat>", "Latitude for manual location filtering").option("--near-lng <lng>", "Longitude for manual location filtering").option("--location-mode <mode>", "Location mode: inferred, manual, or none").option("--start-date <date>", "Events on or after this date (ISO)").option("--end-date <date>", "Events on or before this date (ISO)").option("--page <n>", "Page number", "1").option("--size <n>", "Results per page", "10").option("--json", "Output raw JSON (for agents)", true).action(async (query, opts) => {
413
947
  try {
414
948
  const params = {
415
949
  query,
416
950
  city: opts.city,
417
951
  state: opts.state,
418
952
  category: opts.category,
953
+ league: opts.league,
954
+ categoryEventType: opts.eventType,
955
+ performerId: opts.performerId,
956
+ venueId: opts.venueId,
957
+ parkingFilter: opts.parking,
958
+ nearLat: opts.nearLat ? parseFloat(opts.nearLat) : void 0,
959
+ nearLng: opts.nearLng ? parseFloat(opts.nearLng) : void 0,
960
+ locationMode: opts.locationMode,
419
961
  startDate: opts.startDate,
420
962
  endDate: opts.endDate,
421
963
  page: parseInt(opts.page, 10),
422
964
  size: parseInt(opts.size, 10)
423
965
  };
424
966
  const result = await client.searchEvents(params);
425
- const isJson = opts.json === true || opts.json === "true";
967
+ const isJson = opts.json;
426
968
  if (isJson) {
427
969
  output(result, true);
428
970
  } else {
@@ -439,7 +981,7 @@ Found ${pagination.total} event(s) \u2014 page ${pagination.page}/${pagination.t
439
981
  handleError(err);
440
982
  }
441
983
  });
442
- program.command("browse").description("Browse upcoming events near a location").option("--city <city>", "Preferred city").option("--state <state>", "Preferred state (2-letter code)").option("--lat <lat>", "Latitude").option("--lng <lng>", "Longitude").option("--category <type>", "SPORT, CONCERT, THEATER, or ALL", "ALL").option("--size <n>", "Number of results", "10").option("--json", "Output raw JSON (for agents)", false).action(async (opts) => {
984
+ program.command("browse").description("Browse upcoming events near a location").option("--city <city>", "Preferred city").option("--state <state>", "Preferred state (2-letter code)").option("--lat <lat>", "Latitude").option("--lng <lng>", "Longitude").option("--category <type>", "SPORT, CONCERT, THEATER, or ALL", "ALL").option("--category-slug <slug>", "Filter by category slug").option("--query <query>", "Filter by search query").option("--league <league>", "Filter by league abbreviation").option("--date <date>", "Filter by event date (YYYY-MM-DD)").option("--page <n>", "Page number", "1").option("--context <context>", "homepage, events, or category", "homepage").option("--recommendation <kind>", "upcoming or trending", "upcoming").option("--parking <mode>", "Parking filter: exclude, only, or include").option("--location-mode <mode>", "Location mode: inferred, manual, or none").option("--size <n>", "Number of results", "10").option("--json", "Output raw JSON (for agents)", true).action(async (opts) => {
443
985
  try {
444
986
  const params = {
445
987
  city: opts.city,
@@ -447,10 +989,19 @@ program.command("browse").description("Browse upcoming events near a location").
447
989
  latitude: opts.lat ? parseFloat(opts.lat) : void 0,
448
990
  longitude: opts.lng ? parseFloat(opts.lng) : void 0,
449
991
  categoryEventType: opts.category ?? "ALL",
992
+ category: opts.categorySlug,
993
+ query: opts.query,
994
+ league: opts.league,
995
+ date: opts.date,
996
+ page: parseInt(opts.page, 10),
997
+ context: opts.context,
998
+ recommendation: opts.recommendation,
999
+ parkingFilter: opts.parking,
1000
+ locationMode: opts.locationMode,
450
1001
  size: parseInt(opts.size, 10)
451
1002
  };
452
1003
  const result = await client.browse(params);
453
- const isJson = opts.json === true || opts.json === "true";
1004
+ const isJson = opts.json;
454
1005
  if (isJson) {
455
1006
  output(result, true);
456
1007
  } else {
@@ -466,16 +1017,18 @@ ${result.events.length} upcoming event(s) near ${opts.city ?? "you"}
466
1017
  handleError(err);
467
1018
  }
468
1019
  });
469
- program.command("listings <eventId>").description("Get available ticket listings for an event").option("--size <n>", "Results per page", "20").option("--page <n>", "Page number", "1").option("--sort <dir>", "Price sort: asc or desc", "asc").option("--json", "Output raw JSON (for agents)", false).action(async (eventId, opts) => {
1020
+ program.command("listings <eventId>").description("Get available ticket listings for an event").option("--size <n>", "Results per page", "20").option("--page <n>", "Page number", "1").option("--sort <dir>", "Price sort: asc or desc", "asc").option("--all", "Return all available listings", false).option("--refresh", "Bypass the listings cache", false).option("--json", "Output raw JSON (for agents)", true).action(async (eventId, opts) => {
470
1021
  try {
471
1022
  const params = {
472
1023
  eventId,
473
1024
  size: parseInt(opts.size, 10),
474
1025
  page: parseInt(opts.page, 10),
475
- orderByDirection: opts.sort
1026
+ orderByDirection: opts.sort,
1027
+ includeAll: opts.all,
1028
+ refresh: opts.refresh
476
1029
  };
477
1030
  const result = await client.getListings(params);
478
- const isJson = opts.json === true || opts.json === "true";
1031
+ const isJson = opts.json;
479
1032
  if (isJson) {
480
1033
  output(result, true);
481
1034
  } else {
@@ -491,20 +1044,63 @@ ${result.listings.length} listing(s) for event ${eventId}
491
1044
  handleError(err);
492
1045
  }
493
1046
  });
494
- program.command("checkout <listingId>").description("Get a checkout link to buy tickets for a listing").requiredOption("--quantity <n>", "Number of tickets to buy").option("--json", "Output raw JSON", false).action(async (listingId, opts) => {
1047
+ program.command("quote <eventId>").description("Get refreshed live listings; not a reservation or offer").option("--page <n>", "Page number", "1").option("--size <n>", "Page size (max 100)", "100").option("--json", "Output JSON", true).action(async (eventId, opts) => {
495
1048
  try {
496
- const quantity = parseInt(opts.quantity, 10);
497
- if (!Number.isFinite(quantity) || quantity < 1) {
498
- process.stderr.write("Error: --quantity must be a positive integer\n");
499
- process.exit(1);
1049
+ output(await client.quoteListings({ eventId, page: positiveInteger(opts.page, "--page", 1e4), size: positiveInteger(opts.size, "--size", 100) }), true);
1050
+ } catch (error) {
1051
+ handleError(error);
1052
+ }
1053
+ });
1054
+ program.command("auth").description("Show public browser sign-in and wallet setup; does not create a CLI session").option("--json", "Output JSON", true).action(() => output(client.getAuthorizationInfo(), true));
1055
+ program.command("buy <listingId>").description("Quote or confirm Stripe Link checkout (4-12 alphanumeric listing IDs only)").requiredOption("--quantity <n>", "Number of tickets (1-8)").requiredOption("--max-price <total>", "Maximum TOTAL USD charge including fees").option("--email <email>", "Buyer delivery email (or TIXBIT_EMAIL)").option("--confirm", "Approve payment; return browser handoff if wallet authorization is missing", false).option("--json", "Output JSON", true).action(async (listingId, opts) => {
1056
+ try {
1057
+ output(await client.buyTickets({ listingId, quantity: positiveInteger(opts.quantity, "--quantity", 8), maxAmountCents: usdCents(opts.maxPrice), email: opts.email ?? process.env.TIXBIT_EMAIL ?? "", confirm: opts.confirm, sharedPaymentToken: process.env.TIXBIT_LINK_TOKEN }), true);
1058
+ } catch (error) {
1059
+ handleError(error);
1060
+ }
1061
+ });
1062
+ var sell = program.command("sell").description("Seller browser sign-in or optional authorized user integration");
1063
+ sell.command("list").option("--json", "Output JSON", true).action(async () => {
1064
+ try {
1065
+ output(await client.listSellerListings(process.env.TIXBIT_ACCESS_TOKEN ?? ""), true);
1066
+ } catch (error) {
1067
+ handleError(error);
1068
+ }
1069
+ });
1070
+ sell.command("create").option("--confirm", "Approve listing creation and seller terms", false).option("--json", "Output JSON", true).action(async (opts) => {
1071
+ try {
1072
+ if (!opts.confirm) throw new TypeError("sell create requires --confirm and approved listing JSON on stdin");
1073
+ if (!process.env.TIXBIT_ACCESS_TOKEN) {
1074
+ output(await client.listSellerListings(), true);
1075
+ return;
1076
+ }
1077
+ if (process.stdin.isTTY) throw new TypeError("Provide listing JSON on stdin");
1078
+ let input = "";
1079
+ for await (const chunk of process.stdin) {
1080
+ input += chunk.toString();
1081
+ if (Buffer.byteLength(input) > 65536) throw new TypeError("Listing JSON exceeds 64 KiB");
1082
+ }
1083
+ let body;
1084
+ try {
1085
+ body = JSON.parse(input);
1086
+ } catch {
1087
+ throw new TypeError("Invalid listing JSON on stdin");
500
1088
  }
501
- const isJson = opts.json === true || opts.json === "true";
1089
+ output(await client.createSellerListing(body, process.env.TIXBIT_ACCESS_TOKEN ?? "", opts.confirm), true);
1090
+ } catch (error) {
1091
+ handleError(error);
1092
+ }
1093
+ });
1094
+ program.command("checkout <listingId>").description("Get a checkout link to buy tickets for a listing").requiredOption("--quantity <n>", "Number of tickets to buy").option("--json", "Output raw JSON", true).action(async (listingId, opts) => {
1095
+ try {
1096
+ const quantity = Number(opts.quantity);
1097
+ if (!Number.isInteger(quantity) || quantity < 1 || quantity > 8) {
1098
+ throw new TypeError("--quantity must be an integer from 1 to 8");
1099
+ }
1100
+ const isJson = opts.json === true;
502
1101
  let listingInfo;
503
1102
  try {
504
- const result = await client.getListings({ eventId: listingId, size: 1 });
505
- if (result.listings.length > 0) {
506
- listingInfo = result.listings.find((l) => l.id === listingId);
507
- }
1103
+ listingInfo = (await client.getListing(listingId)).listing;
508
1104
  } catch {
509
1105
  }
510
1106
  const link = client.createCheckoutLink({ listingId, quantity });
@@ -540,10 +1136,85 @@ program.command("checkout <listingId>").description("Get a checkout link to buy
540
1136
  handleError(err);
541
1137
  }
542
1138
  });
543
- program.command("seatmap <eventId>").description("Show the seating chart / section map for an event's venue").option("--section <name>", "Highlight a specific section (case-insensitive)").option("--json", "Output raw JSON (for agents)", false).action(async (eventId, opts) => {
1139
+ program.command("purchase <listingId>").description("Buy tickets through MPP machine checkout").requiredOption("--quantity <n>", "Number of tickets to buy").option("--email <email>", "Buyer email for confirmation and delivery").option("--name <name>", "Optional buyer/recipient name").option(
1140
+ "--idempotency-key <uuid>",
1141
+ "Stable UUID v4 to reuse for retries and recovery"
1142
+ ).option("--account <name>", "mppx account name").requiredOption("--max-price <total>", "Maximum total USD payment").option("--confirm", "Approve this MPP payment", false).option("--json", "Output agent-readable JSON", true).action(async (listingId, opts) => {
1143
+ let publicErrorCode = "INVALID_PURCHASE_REQUEST";
1144
+ let publicErrorMessage = "Invalid purchase request.";
1145
+ try {
1146
+ if (!opts.confirm) throw new TypeError("purchase requires --confirm before loading the wallet");
1147
+ const maxAmountCents = usdCents(opts.maxPrice);
1148
+ if (!opts.idempotencyKey) throw new TypeError("purchase requires --idempotency-key for safe recovery");
1149
+ if (!opts.email?.trim()) {
1150
+ throw new TypeError("--email is required for machine purchases");
1151
+ }
1152
+ const purchase = normalizePurchaseTicketsParams({
1153
+ listingId,
1154
+ quantity: Number(opts.quantity),
1155
+ email: opts.email,
1156
+ name: opts.name,
1157
+ idempotencyKey: opts.idempotencyKey
1158
+ });
1159
+ const paymentEndpoint = process.env.TIXBIT_PAYMENT_URL;
1160
+ new TixBitClient({ paymentEndpoint });
1161
+ publicErrorCode = "MPP_CLIENT_SETUP_FAILED";
1162
+ publicErrorMessage = "MPP payment client setup failed. Check the selected mppx account and local wallet configuration.";
1163
+ const [{ Mppx, tempo }, { resolveAccount }] = await Promise.all([
1164
+ import("mppx/client"),
1165
+ import("mppx/cli")
1166
+ ]);
1167
+ const account = await resolveAccount(opts.account);
1168
+ const payments = Mppx.create({
1169
+ methods: [tempo({ account })],
1170
+ polyfill: false,
1171
+ maxPaymentRetries: 1,
1172
+ onChallenge: async (challenge) => {
1173
+ assertMppChallenge(challenge, maxAmountCents);
1174
+ return void 0;
1175
+ }
1176
+ });
1177
+ const purchaseClient = new TixBitClient({
1178
+ baseUrl: process.env.TIXBIT_BASE_URL,
1179
+ paymentEndpoint,
1180
+ paymentFetch: (input, init) => payments.fetch(input, init)
1181
+ });
1182
+ const result = await purchaseClient.purchaseTickets(purchase);
1183
+ if (opts.json) {
1184
+ output(result, true);
1185
+ } else {
1186
+ outputPurchaseResult(result);
1187
+ }
1188
+ if (!result.success) process.exitCode = 2;
1189
+ } catch (err) {
1190
+ const message = publicErrorCode === "INVALID_PURCHASE_REQUEST" && err instanceof Error ? err.message : publicErrorMessage;
1191
+ if (!opts.json) {
1192
+ process.stderr.write(`Error: ${message}
1193
+ `);
1194
+ process.exitCode = 1;
1195
+ return;
1196
+ }
1197
+ output(
1198
+ {
1199
+ success: false,
1200
+ status: "rejected",
1201
+ orderReference: null,
1202
+ receiptUrl: null,
1203
+ error: {
1204
+ code: publicErrorCode,
1205
+ message
1206
+ },
1207
+ action: publicErrorCode === "INVALID_PURCHASE_REQUEST" ? "Correct the request before attempting payment." : "Fix the local mppx account configuration, then retry with the same idempotency key."
1208
+ },
1209
+ true
1210
+ );
1211
+ process.exitCode = 1;
1212
+ }
1213
+ });
1214
+ program.command("seatmap <eventId>").description("Show the seating chart / section map for an event's venue").option("--section <name>", "Highlight a specific section (case-insensitive)").option("--json", "Output raw JSON (for agents)", true).action(async (eventId, opts) => {
544
1215
  try {
545
1216
  const result = await client.getSeatmap({ eventId });
546
- const isJson = opts.json === true || opts.json === "true";
1217
+ const isJson = opts.json;
547
1218
  if (isJson) {
548
1219
  output(result, true);
549
1220
  return;
@@ -616,8 +1287,8 @@ program.command("seatmap <eventId>").description("Show the seating chart / secti
616
1287
  handleError(err);
617
1288
  }
618
1289
  });
619
- program.command("url <slug>").description("Print the TixBit event page URL for a slug or ID").action((slug) => {
620
- process.stdout.write(client.eventUrl(slug) + "\n");
1290
+ program.command("url <slug>").description("Print the TixBit event page URL for a slug or ID").option("--json", "Output JSON", true).action((slug) => {
1291
+ output({ url: client.eventUrl(slug) }, true);
621
1292
  });
622
1293
  function categorizeSections(sections) {
623
1294
  const groups = {};
@@ -676,4 +1347,22 @@ function describePosition(section, allSections) {
676
1347
  }
677
1348
  return [vertical, horizontal].filter((p) => p !== "center").join(", ");
678
1349
  }
679
- program.parse();
1350
+ function configureCommands(command) {
1351
+ command.exitOverride();
1352
+ command.configureOutput({
1353
+ writeOut: (text) => output({ help: text }, true),
1354
+ writeErr: () => {
1355
+ }
1356
+ });
1357
+ for (const child of command.commands) configureCommands(child);
1358
+ }
1359
+ configureCommands(program);
1360
+ try {
1361
+ await program.parseAsync();
1362
+ } catch (error) {
1363
+ const code = error.code;
1364
+ if (code !== "commander.helpDisplayed" && code !== "commander.version") {
1365
+ output({ success: false, error: { code: "INVALID_COMMAND", message: "Invalid command or options. Use --help." } }, true);
1366
+ process.exitCode = 1;
1367
+ }
1368
+ }