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/README.md +206 -49
- package/dist/cli.js +798 -76
- package/dist/index.d.ts +256 -16
- package/dist/index.js +569 -47
- package/package.json +11 -6
- package/skills/tixbit/SKILL.md +64 -0
package/dist/cli.js
CHANGED
|
@@ -3,44 +3,127 @@
|
|
|
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";
|
|
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");
|
|
68
|
+
}
|
|
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;
|
|
78
|
+
}
|
|
79
|
+
function toAbsoluteUrl(baseUrl, value) {
|
|
80
|
+
if (!value) return null;
|
|
81
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
return `${baseUrl}${value.startsWith("/") ? "" : "/"}${value}`;
|
|
85
|
+
}
|
|
10
86
|
var TixBitClient = class {
|
|
11
87
|
baseUrl;
|
|
12
88
|
timeoutMs;
|
|
13
|
-
|
|
89
|
+
paymentEndpoint;
|
|
90
|
+
paymentFetch;
|
|
14
91
|
constructor(config = {}) {
|
|
15
92
|
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
16
93
|
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
17
|
-
this.
|
|
94
|
+
this.paymentEndpoint = normalizePaymentEndpoint(
|
|
95
|
+
config.paymentEndpoint ?? DEFAULT_PAYMENT_ENDPOINT
|
|
96
|
+
);
|
|
97
|
+
this.paymentFetch = config.paymentFetch ?? ((input, init) => fetch(input, init));
|
|
18
98
|
}
|
|
19
99
|
// ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
20
|
-
async request(path
|
|
100
|
+
async request(path) {
|
|
21
101
|
const url = `${this.baseUrl}${path}`;
|
|
22
102
|
const controller = new AbortController();
|
|
23
103
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
24
104
|
const headers = {
|
|
25
105
|
Accept: "application/json",
|
|
26
|
-
"User-Agent": USER_AGENT
|
|
27
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
106
|
+
"User-Agent": USER_AGENT
|
|
28
107
|
};
|
|
29
108
|
try {
|
|
30
109
|
const res = await fetch(url, {
|
|
31
|
-
|
|
32
|
-
headers
|
|
110
|
+
redirect: "error",
|
|
111
|
+
headers,
|
|
33
112
|
signal: controller.signal
|
|
34
113
|
});
|
|
35
114
|
if (!res.ok) {
|
|
36
|
-
const text = await res.text().catch(() => "");
|
|
37
115
|
throw new TixBitApiError(
|
|
38
|
-
|
|
116
|
+
`Request failed (HTTP ${res.status})`,
|
|
39
117
|
res.status,
|
|
40
118
|
url
|
|
41
119
|
);
|
|
42
120
|
}
|
|
43
121
|
return await res.json();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (controller.signal.aborted) {
|
|
124
|
+
throw new TixBitTimeoutError(url, this.timeoutMs);
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
44
127
|
} finally {
|
|
45
128
|
clearTimeout(timer);
|
|
46
129
|
}
|
|
@@ -64,15 +147,40 @@ var TixBitClient = class {
|
|
|
64
147
|
city: params.city,
|
|
65
148
|
state: params.state,
|
|
66
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,
|
|
67
158
|
startDate: params.startDate,
|
|
68
159
|
endDate: params.endDate,
|
|
69
160
|
page: params.page,
|
|
70
161
|
size: params.size ?? 25
|
|
71
162
|
});
|
|
72
163
|
const data = await this.request(`/api/events/search${query}`);
|
|
164
|
+
const pagination = data.pagination;
|
|
73
165
|
return {
|
|
74
166
|
events: (data.events ?? []).map(normalizeEvent),
|
|
75
|
-
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
|
|
76
184
|
};
|
|
77
185
|
}
|
|
78
186
|
// ── Browse (Location-Aware) ───────────────────────────────────────────────
|
|
@@ -86,19 +194,32 @@ var TixBitClient = class {
|
|
|
86
194
|
*/
|
|
87
195
|
async browse(params = {}) {
|
|
88
196
|
const query = this.qs({
|
|
197
|
+
page: params.page ?? 1,
|
|
89
198
|
size: params.size ?? 18,
|
|
90
|
-
|
|
91
|
-
|
|
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",
|
|
92
205
|
nearLat: params.latitude,
|
|
93
206
|
nearLng: params.longitude,
|
|
94
207
|
preferCity: params.city,
|
|
95
208
|
preferState: params.state,
|
|
96
|
-
categoryEventType: params.categoryEventType
|
|
209
|
+
categoryEventType: params.categoryEventType,
|
|
210
|
+
locationMode: params.locationMode,
|
|
211
|
+
searchScope: params.searchScope,
|
|
212
|
+
parkingFilter: params.parkingFilter
|
|
97
213
|
});
|
|
98
214
|
const data = await this.request(`/api/events${query}`);
|
|
99
215
|
return {
|
|
100
216
|
events: (data.events ?? []).map(normalizeEvent),
|
|
101
|
-
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 }
|
|
102
223
|
};
|
|
103
224
|
}
|
|
104
225
|
// ── Listings ──────────────────────────────────────────────────────────────
|
|
@@ -107,27 +228,138 @@ var TixBitClient = class {
|
|
|
107
228
|
*
|
|
108
229
|
* @example
|
|
109
230
|
* ```ts
|
|
110
|
-
* const listings = await client.getListings({ eventId: "
|
|
231
|
+
* const listings = await client.getListings({ eventId: "EVENT123" });
|
|
111
232
|
* ```
|
|
112
233
|
*/
|
|
113
234
|
async getListings(params) {
|
|
114
235
|
const query = this.qs({
|
|
115
236
|
size: params.size ?? 50,
|
|
116
237
|
page: params.page ?? 1,
|
|
117
|
-
order_by_direction: params.orderByDirection ?? "asc"
|
|
238
|
+
order_by_direction: params.orderByDirection ?? "asc",
|
|
239
|
+
includeAll: params.includeAll,
|
|
240
|
+
refresh: params.refresh
|
|
118
241
|
});
|
|
119
|
-
const
|
|
242
|
+
const normalizedEventId = normalizeExternalEventId(params.eventId);
|
|
243
|
+
const data = await this.request(`/api/events/${encodeURIComponent(normalizedEventId)}/listings${query}`);
|
|
120
244
|
const listings = (data.data ?? []).map(normalizeListing);
|
|
121
245
|
return {
|
|
122
246
|
listings,
|
|
123
247
|
meta: {
|
|
124
|
-
total: data.meta
|
|
125
|
-
page: data.meta
|
|
126
|
-
size: data.meta
|
|
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"]),
|
|
127
257
|
cacheSource: data.meta?.cacheSource
|
|
128
258
|
}
|
|
129
259
|
};
|
|
130
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
|
+
}
|
|
131
363
|
// ── Checkout Link ──────────────────────────────────────────────────────────
|
|
132
364
|
/**
|
|
133
365
|
* Create a checkout link for a listing.
|
|
@@ -138,29 +370,148 @@ var TixBitClient = class {
|
|
|
138
370
|
* @example
|
|
139
371
|
* ```ts
|
|
140
372
|
* const link = client.createCheckoutLink({
|
|
141
|
-
* listingId: "
|
|
373
|
+
* listingId: "LISTING123",
|
|
142
374
|
* quantity: 2,
|
|
143
375
|
* });
|
|
144
376
|
*
|
|
145
377
|
* console.log(link.url);
|
|
146
|
-
* // → "https://tixbit.com/checkout/process?listing=
|
|
378
|
+
* // → "https://www.tixbit.com/checkout/process?listing=LISTING123&quantity=2"
|
|
147
379
|
* ```
|
|
148
380
|
*/
|
|
149
381
|
createCheckoutLink(params) {
|
|
150
|
-
const
|
|
151
|
-
|
|
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}`;
|
|
152
392
|
return {
|
|
153
393
|
url,
|
|
154
|
-
listingId
|
|
155
|
-
quantity
|
|
394
|
+
listingId,
|
|
395
|
+
quantity: params.quantity
|
|
156
396
|
};
|
|
157
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
|
+
}
|
|
158
505
|
// ── Event URL helper ──────────────────────────────────────────────────────
|
|
159
506
|
/**
|
|
160
|
-
* Build the direct URL to an event page on tixbit.com.
|
|
507
|
+
* Build the direct URL to an event page on www.tixbit.com.
|
|
161
508
|
*/
|
|
162
509
|
eventUrl(slugOrId) {
|
|
163
|
-
|
|
510
|
+
if (slugOrId.includes("/")) {
|
|
511
|
+
return `${this.baseUrl}/events/${slugOrId}`;
|
|
512
|
+
}
|
|
513
|
+
const normalized = normalizeExternalEventId(slugOrId);
|
|
514
|
+
return `${this.baseUrl}/events/${normalized}`;
|
|
164
515
|
}
|
|
165
516
|
// ── Seatmap ───────────────────────────────────────────────────────────────
|
|
166
517
|
/**
|
|
@@ -172,33 +523,33 @@ var TixBitClient = class {
|
|
|
172
523
|
*
|
|
173
524
|
* @example
|
|
174
525
|
* ```ts
|
|
175
|
-
* const seatmap = await client.getSeatmap({ eventId: "
|
|
526
|
+
* const seatmap = await client.getSeatmap({ eventId: "EVENT123" });
|
|
176
527
|
* console.log(seatmap.venue_name); // "State Farm Arena"
|
|
177
528
|
* console.log(seatmap.section_names); // ["101", "102", ...]
|
|
178
529
|
* ```
|
|
179
530
|
*/
|
|
180
531
|
async getSeatmap(params) {
|
|
181
|
-
const
|
|
532
|
+
const normalizedEventId = normalizeExternalEventId(params.eventId);
|
|
533
|
+
const data = await this.request(`/api/events/${encodeURIComponent(normalizedEventId)}/seating-chart`);
|
|
182
534
|
let zones = [];
|
|
183
535
|
let sectionNames = [];
|
|
184
|
-
|
|
536
|
+
const coordinatesUrl = data.coordinates_url ?? data.coordinates ?? null;
|
|
537
|
+
if (data.has_coordinates && coordinatesUrl) {
|
|
185
538
|
try {
|
|
186
|
-
zones = await this.fetchAndParseCoordinates(
|
|
187
|
-
sectionNames = zones.flatMap(
|
|
188
|
-
(z) => z.sections.map((s) => s.name)
|
|
189
|
-
);
|
|
539
|
+
zones = await this.fetchAndParseCoordinates(coordinatesUrl);
|
|
540
|
+
sectionNames = zones.flatMap((z) => z.sections.map((s) => s.name));
|
|
190
541
|
} catch {
|
|
191
542
|
}
|
|
192
543
|
}
|
|
193
544
|
return {
|
|
194
545
|
success: data.success,
|
|
195
|
-
event_id: data.event_id,
|
|
546
|
+
event_id: normalizeExternalEventId(data.event_id),
|
|
196
547
|
venue_id: data.venue_id,
|
|
197
548
|
venue_name: data.venue_name,
|
|
198
549
|
configuration_id: data.configuration_id,
|
|
199
550
|
configuration_name: data.configuration_name,
|
|
200
|
-
background_image: data.background_image ?? null,
|
|
201
|
-
coordinates_url:
|
|
551
|
+
background_image: toAbsoluteUrl(this.baseUrl, data.background_image ?? null),
|
|
552
|
+
coordinates_url: toAbsoluteUrl(this.baseUrl, coordinatesUrl),
|
|
202
553
|
has_coordinates: data.has_coordinates,
|
|
203
554
|
capacity: data.capacity ?? null,
|
|
204
555
|
venue: data.venue_data,
|
|
@@ -228,12 +579,22 @@ var TixBitClient = class {
|
|
|
228
579
|
id: zone.id,
|
|
229
580
|
name: zone.name,
|
|
230
581
|
sections: (zone.sections ?? []).map((section) => {
|
|
231
|
-
const
|
|
582
|
+
const labels = Array.isArray(section.labels) ? section.labels.filter((label) => label && typeof label.text === "string").map((label) => ({
|
|
583
|
+
text: label.text,
|
|
584
|
+
x: typeof label.x === "number" ? label.x : 0,
|
|
585
|
+
y: typeof label.y === "number" ? label.y : 0,
|
|
586
|
+
size: typeof label.size === "number" ? label.size : void 0,
|
|
587
|
+
angle: typeof label.angle === "number" ? label.angle : void 0
|
|
588
|
+
})) : [];
|
|
589
|
+
const primaryLabel = labels.find((label) => label.text.toUpperCase() === section.name.toUpperCase()) ?? labels[0];
|
|
590
|
+
const shapePath = section.shape && typeof section.shape.path === "string" ? section.shape.path : null;
|
|
232
591
|
return {
|
|
233
592
|
id: section.id,
|
|
234
593
|
name: section.name,
|
|
235
|
-
x:
|
|
236
|
-
y:
|
|
594
|
+
x: primaryLabel?.x ?? 0,
|
|
595
|
+
y: primaryLabel?.y ?? 0,
|
|
596
|
+
labels,
|
|
597
|
+
shape_path: shapePath
|
|
237
598
|
};
|
|
238
599
|
})
|
|
239
600
|
}));
|
|
@@ -242,20 +603,148 @@ var TixBitClient = class {
|
|
|
242
603
|
}
|
|
243
604
|
}
|
|
244
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
|
+
}
|
|
245
729
|
function normalizeEvent(raw) {
|
|
246
730
|
const e = raw;
|
|
731
|
+
const rawId = str(e.external_event_id) ?? str(e.externalEventId) ?? str(e.id) ?? "";
|
|
732
|
+
const externalEventId = normalizeExternalEventId(rawId);
|
|
247
733
|
return {
|
|
248
|
-
id:
|
|
249
|
-
external_event_id:
|
|
250
|
-
slug: str(e.slug) ??
|
|
734
|
+
id: externalEventId,
|
|
735
|
+
external_event_id: externalEventId,
|
|
736
|
+
slug: str(e.slug) ?? externalEventId,
|
|
251
737
|
name: str(e.name) ?? str(e.performer) ?? "",
|
|
252
738
|
date: resolveDate(e),
|
|
253
739
|
venue_name: str(e.venue_name) ?? str(e.venueName) ?? null,
|
|
254
740
|
venue_city: str(e.venue_city) ?? str(e.venueCity) ?? null,
|
|
255
741
|
venue_state: str(e.venue_state) ?? str(e.venueState) ?? null,
|
|
742
|
+
venue_country: str(e.venue_country) ?? str(e.venueCountry) ?? null,
|
|
256
743
|
image_url: str(e.image_url) ?? str(e.imageUrl) ?? null,
|
|
744
|
+
category_slug: str(e.category_slug) ?? str(e.categorySlug) ?? null,
|
|
257
745
|
category_name: str(e.category_name) ?? str(e.categoryName) ?? null,
|
|
258
746
|
category_event_type: str(e.category_event_type) ?? str(e.categoryEventType) ?? null,
|
|
747
|
+
metadata: e.metadata && typeof e.metadata === "object" ? e.metadata : null,
|
|
259
748
|
has_listings: Boolean(e.has_listings),
|
|
260
749
|
inventory: normalizeInventory(e.inventory)
|
|
261
750
|
};
|
|
@@ -265,10 +754,17 @@ function normalizeInventory(raw) {
|
|
|
265
754
|
return { total_available: 0, min_price: 0, max_price: 0 };
|
|
266
755
|
}
|
|
267
756
|
const inv = raw;
|
|
757
|
+
const sources = inv.sources;
|
|
268
758
|
return {
|
|
269
759
|
total_available: num(inv.total_available) ?? 0,
|
|
270
760
|
min_price: num(inv.min_price) ?? 0,
|
|
271
|
-
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
|
+
} : {}
|
|
272
768
|
};
|
|
273
769
|
}
|
|
274
770
|
function normalizeListing(raw) {
|
|
@@ -276,16 +772,30 @@ function normalizeListing(raw) {
|
|
|
276
772
|
const attrs = outer.attributes ?? outer;
|
|
277
773
|
return {
|
|
278
774
|
id: str(outer.id) ?? str(attrs.id) ?? "",
|
|
775
|
+
event_id: str(attrs.event_id) ?? null,
|
|
279
776
|
// price_per_ticket is the fee-inclusive per-ticket price (in dollars, not cents)
|
|
280
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",
|
|
281
786
|
quantity: num(attrs.quantity) ?? num(attrs.available_quantity) ?? 0,
|
|
282
787
|
quantities_list: Array.isArray(attrs.quantities_list) ? attrs.quantities_list : [],
|
|
283
788
|
section: str(attrs.section) ?? null,
|
|
284
789
|
row: str(attrs.row) ?? null,
|
|
790
|
+
seat_from: str(attrs.seat_from) ?? null,
|
|
791
|
+
seat_to: str(attrs.seat_to) ?? null,
|
|
285
792
|
seat_numbers: str(attrs.seat_numbers) ?? null,
|
|
286
793
|
listing_hash: str(attrs.listing_hash) ?? "",
|
|
287
794
|
notes: str(attrs.notes) ?? null,
|
|
288
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") : [],
|
|
289
799
|
splits: Array.isArray(attrs.splits) ? attrs.splits : [],
|
|
290
800
|
raw: outer
|
|
291
801
|
};
|
|
@@ -308,6 +818,23 @@ function str(v) {
|
|
|
308
818
|
function num(v) {
|
|
309
819
|
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
310
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
|
+
}
|
|
311
838
|
var TixBitApiError = class extends Error {
|
|
312
839
|
constructor(message, status, url) {
|
|
313
840
|
super(message);
|
|
@@ -316,13 +843,30 @@ var TixBitApiError = class extends Error {
|
|
|
316
843
|
this.name = "TixBitApiError";
|
|
317
844
|
}
|
|
318
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
|
+
};
|
|
319
854
|
|
|
320
855
|
// src/cli.ts
|
|
321
|
-
var client =
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
})
|
|
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");
|
|
325
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
|
+
}
|
|
326
870
|
if (json) {
|
|
327
871
|
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
328
872
|
return;
|
|
@@ -335,6 +879,30 @@ function output(data, json) {
|
|
|
335
879
|
}
|
|
336
880
|
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
337
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
|
+
}
|
|
338
906
|
function formatItem(item) {
|
|
339
907
|
if (!item || typeof item !== "object") return String(item);
|
|
340
908
|
const e = item;
|
|
@@ -370,26 +938,33 @@ function formatItem(item) {
|
|
|
370
938
|
return JSON.stringify(item, null, 2);
|
|
371
939
|
}
|
|
372
940
|
function handleError(err) {
|
|
373
|
-
const msg = err instanceof
|
|
374
|
-
|
|
375
|
-
`);
|
|
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);
|
|
376
943
|
process.exit(1);
|
|
377
944
|
}
|
|
378
|
-
var program = new Command().name("tixbit").description("Search events, browse listings, and buy tickets
|
|
379
|
-
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)",
|
|
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) => {
|
|
380
947
|
try {
|
|
381
948
|
const params = {
|
|
382
949
|
query,
|
|
383
950
|
city: opts.city,
|
|
384
951
|
state: opts.state,
|
|
385
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,
|
|
386
961
|
startDate: opts.startDate,
|
|
387
962
|
endDate: opts.endDate,
|
|
388
963
|
page: parseInt(opts.page, 10),
|
|
389
964
|
size: parseInt(opts.size, 10)
|
|
390
965
|
};
|
|
391
966
|
const result = await client.searchEvents(params);
|
|
392
|
-
const isJson = opts.json
|
|
967
|
+
const isJson = opts.json;
|
|
393
968
|
if (isJson) {
|
|
394
969
|
output(result, true);
|
|
395
970
|
} else {
|
|
@@ -406,7 +981,7 @@ Found ${pagination.total} event(s) \u2014 page ${pagination.page}/${pagination.t
|
|
|
406
981
|
handleError(err);
|
|
407
982
|
}
|
|
408
983
|
});
|
|
409
|
-
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)",
|
|
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) => {
|
|
410
985
|
try {
|
|
411
986
|
const params = {
|
|
412
987
|
city: opts.city,
|
|
@@ -414,10 +989,19 @@ program.command("browse").description("Browse upcoming events near a location").
|
|
|
414
989
|
latitude: opts.lat ? parseFloat(opts.lat) : void 0,
|
|
415
990
|
longitude: opts.lng ? parseFloat(opts.lng) : void 0,
|
|
416
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,
|
|
417
1001
|
size: parseInt(opts.size, 10)
|
|
418
1002
|
};
|
|
419
1003
|
const result = await client.browse(params);
|
|
420
|
-
const isJson = opts.json
|
|
1004
|
+
const isJson = opts.json;
|
|
421
1005
|
if (isJson) {
|
|
422
1006
|
output(result, true);
|
|
423
1007
|
} else {
|
|
@@ -433,16 +1017,18 @@ ${result.events.length} upcoming event(s) near ${opts.city ?? "you"}
|
|
|
433
1017
|
handleError(err);
|
|
434
1018
|
}
|
|
435
1019
|
});
|
|
436
|
-
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)",
|
|
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) => {
|
|
437
1021
|
try {
|
|
438
1022
|
const params = {
|
|
439
1023
|
eventId,
|
|
440
1024
|
size: parseInt(opts.size, 10),
|
|
441
1025
|
page: parseInt(opts.page, 10),
|
|
442
|
-
orderByDirection: opts.sort
|
|
1026
|
+
orderByDirection: opts.sort,
|
|
1027
|
+
includeAll: opts.all,
|
|
1028
|
+
refresh: opts.refresh
|
|
443
1029
|
};
|
|
444
1030
|
const result = await client.getListings(params);
|
|
445
|
-
const isJson = opts.json
|
|
1031
|
+
const isJson = opts.json;
|
|
446
1032
|
if (isJson) {
|
|
447
1033
|
output(result, true);
|
|
448
1034
|
} else {
|
|
@@ -458,20 +1044,63 @@ ${result.listings.length} listing(s) for event ${eventId}
|
|
|
458
1044
|
handleError(err);
|
|
459
1045
|
}
|
|
460
1046
|
});
|
|
461
|
-
program.command("
|
|
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) => {
|
|
462
1048
|
try {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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");
|
|
467
1088
|
}
|
|
468
|
-
|
|
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;
|
|
469
1101
|
let listingInfo;
|
|
470
1102
|
try {
|
|
471
|
-
|
|
472
|
-
if (result.listings.length > 0) {
|
|
473
|
-
listingInfo = result.listings.find((l) => l.id === listingId);
|
|
474
|
-
}
|
|
1103
|
+
listingInfo = (await client.getListing(listingId)).listing;
|
|
475
1104
|
} catch {
|
|
476
1105
|
}
|
|
477
1106
|
const link = client.createCheckoutLink({ listingId, quantity });
|
|
@@ -507,10 +1136,85 @@ program.command("checkout <listingId>").description("Get a checkout link to buy
|
|
|
507
1136
|
handleError(err);
|
|
508
1137
|
}
|
|
509
1138
|
});
|
|
510
|
-
program.command("
|
|
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) => {
|
|
511
1215
|
try {
|
|
512
1216
|
const result = await client.getSeatmap({ eventId });
|
|
513
|
-
const isJson = opts.json
|
|
1217
|
+
const isJson = opts.json;
|
|
514
1218
|
if (isJson) {
|
|
515
1219
|
output(result, true);
|
|
516
1220
|
return;
|
|
@@ -583,8 +1287,8 @@ program.command("seatmap <eventId>").description("Show the seating chart / secti
|
|
|
583
1287
|
handleError(err);
|
|
584
1288
|
}
|
|
585
1289
|
});
|
|
586
|
-
program.command("url <slug>").description("Print the TixBit event page URL for a slug or ID").action((slug) => {
|
|
587
|
-
|
|
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);
|
|
588
1292
|
});
|
|
589
1293
|
function categorizeSections(sections) {
|
|
590
1294
|
const groups = {};
|
|
@@ -643,4 +1347,22 @@ function describePosition(section, allSections) {
|
|
|
643
1347
|
}
|
|
644
1348
|
return [vertical, horizontal].filter((p) => p !== "center").join(", ");
|
|
645
1349
|
}
|
|
646
|
-
|
|
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
|
+
}
|