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