tixbit 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
+ import { readFile as readFile3 } from "fs/promises";
6
+ import { randomUUID as randomUUID3 } from "crypto";
5
7
 
6
8
  // src/safety.ts
7
9
  function usdCents(raw) {
@@ -41,6 +43,235 @@ function redact(value, secrets = []) {
41
43
  return value;
42
44
  }
43
45
 
46
+ // src/seller.ts
47
+ import { createHash, randomBytes, randomUUID } from "crypto";
48
+ import { mkdir, readFile, writeFile, rename, unlink, lstat, chmod } from "fs/promises";
49
+ import { homedir } from "os";
50
+ import { join } from "path";
51
+ var SELLER_ORIGIN = "https://mcp.tixbit.com";
52
+ var SELLER_CALLBACK = `${SELLER_ORIGIN}/cli/callback`;
53
+ var DEFAULT_SELLER_CLIENT_ID = "tixbit-client.eyJ0eXAiOiJjbGllbnQiLCJpYXQiOjE3OTIxOTI4MDAsImNsaWVudE5hbWUiOiJUaXhCaXQgQ0xJIiwicmVkaXJlY3RVcmlzIjpbImh0dHBzOi8vbWNwLnRpeGJpdC5jb20vY2xpL2NhbGxiYWNrIl0sInNjb3BlIjoibWNwOnJlYWQgaW52ZW50b3J5OnJlYWQgc2VsbGVyOndyaXRlIn0.C9ijXrH07kd2p15Dd1z5yzfVsz4XoBUJI90GP9Rbg7k";
54
+ var SELLER_SCOPES = "mcp:read inventory:read seller:write";
55
+ var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
56
+ var disclosures = ["ada", "twenty_one_plus", "obstructed_view", "limited_view", "standing_room", "aisle_seat"];
57
+ var SellerError = class extends Error {
58
+ constructor(code, message, action, retryable = false) {
59
+ super(message);
60
+ this.code = code;
61
+ this.action = action;
62
+ this.retryable = retryable;
63
+ }
64
+ };
65
+ function sellerId(value) {
66
+ if (!uuid.test(value)) throw new TypeError("Review and attempt IDs must be UUID v4 values");
67
+ return value;
68
+ }
69
+ function text(value, label, max) {
70
+ if (typeof value !== "string" || !value.trim() || value.length > max || /[\x00-\x1f\x7f]/.test(value)) throw new TypeError(`${label} must be nonempty text (max ${max} characters)`);
71
+ return value.trim();
72
+ }
73
+ function normalizeSellerOperation(operation, value) {
74
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Expected a JSON object");
75
+ const b = value;
76
+ const allowed = operation === "seller.create" ? ["externalEventId", "section", "quantity", "priceCents", "termsAccepted", "row", "seatNumbers", "generalAdmission", "faceValueCents", "disclosures", "notes", "ownedImportId"] : operation === "seller.reprice" ? ["listingId", "priceCents"] : ["listingId"];
77
+ for (const key of Object.keys(b)) if (!allowed.includes(key)) throw new TypeError(`Unsupported seller field: ${key}. Quantity edits and fulfillment/payment changes are unavailable.`);
78
+ const result = {};
79
+ if (operation !== "seller.create") result.listingId = text(b.listingId, "listingId", 128);
80
+ if (operation !== "seller.delist") {
81
+ if (!Number.isSafeInteger(b.priceCents) || b.priceCents < 100 || b.priceCents > 5e6) throw new TypeError("priceCents must be an integer from 100 to 5000000 (USD per ticket)");
82
+ result.priceCents = b.priceCents;
83
+ }
84
+ if (operation === "seller.create") {
85
+ result.externalEventId = text(b.externalEventId, "externalEventId", 128);
86
+ result.section = text(b.section, "section", 128);
87
+ if (!Number.isInteger(b.quantity) || b.quantity < 1 || b.quantity > 20) throw new TypeError("quantity must be an integer from 1 to 20");
88
+ if (b.termsAccepted !== true) throw new TypeError("Seller approval requires termsAccepted: true");
89
+ result.quantity = b.quantity;
90
+ result.termsAccepted = true;
91
+ for (const key of ["row", "notes", "ownedImportId"]) if (b[key] !== void 0) result[key] = text(b[key], key, key === "notes" ? 280 : key === "row" ? 64 : 100);
92
+ if (b.generalAdmission !== void 0) {
93
+ if (typeof b.generalAdmission !== "boolean") throw new TypeError("generalAdmission must be a boolean");
94
+ result.generalAdmission = b.generalAdmission;
95
+ }
96
+ if (b.seatNumbers !== void 0) {
97
+ if (!Array.isArray(b.seatNumbers) || b.seatNumbers.length > 0 && b.seatNumbers.length !== b.quantity || new Set(b.seatNumbers).size !== b.seatNumbers.length) throw new TypeError("seatNumbers must contain one distinct seat per ticket");
98
+ result.seatNumbers = b.seatNumbers.map((s) => text(s, "seatNumber", 16));
99
+ }
100
+ if (b.faceValueCents !== void 0) {
101
+ if (!Number.isSafeInteger(b.faceValueCents) || b.faceValueCents < 0 || b.faceValueCents > 5e6) throw new TypeError("faceValueCents must be a nonnegative integer");
102
+ result.faceValueCents = b.faceValueCents;
103
+ }
104
+ if (b.ownedImportId !== void 0 && !/^ori_[0-9a-f-]{36}$/i.test(String(b.ownedImportId))) throw new TypeError("ownedImportId must be a TixBit-owned resale import ID");
105
+ if (b.generalAdmission === true && (b.row || Array.isArray(b.seatNumbers) && b.seatNumbers.length)) throw new TypeError("General admission cannot include a row or seat numbers");
106
+ if (b.generalAdmission !== true && (!b.row || !b.ownedImportId && (!Array.isArray(b.seatNumbers) || !b.seatNumbers.length))) throw new TypeError("Reserved seating requires row and exact seatNumbers matching quantity; use generalAdmission: true only for GA tickets");
107
+ if (Array.isArray(b.seatNumbers) && b.seatNumbers.some((s) => /^(tbd|unknown|n\/?a|pending)$/i.test(s.trim()))) throw new TypeError("Exact seat numbers are required; placeholder seats are not accepted");
108
+ if (b.disclosures !== void 0) {
109
+ if (!Array.isArray(b.disclosures) || b.disclosures.some((d) => !disclosures.includes(d))) throw new TypeError(`disclosures must use: ${disclosures.join(", ")}`);
110
+ result.disclosures = [...new Set(b.disclosures)];
111
+ }
112
+ }
113
+ return result;
114
+ }
115
+ var SellerStore = class {
116
+ constructor(directory = join(homedir(), ".config", "tixbit", "seller")) {
117
+ this.directory = directory;
118
+ }
119
+ async save(name, value) {
120
+ await mkdir(this.directory, { recursive: true, mode: 448 });
121
+ const info = await lstat(this.directory);
122
+ if (info.isSymbolicLink() || !info.isDirectory()) throw new TypeError("Seller state directory must be a real directory");
123
+ await chmod(this.directory, 448);
124
+ const temporary = join(this.directory, `.${randomUUID()}.tmp`);
125
+ await writeFile(temporary, JSON.stringify(value), { mode: 384, flag: "wx" });
126
+ await rename(temporary, join(this.directory, name));
127
+ }
128
+ async load(name) {
129
+ try {
130
+ const path = join(this.directory, name);
131
+ const info = await lstat(path);
132
+ if (!info.isFile() || info.isSymbolicLink() || (info.mode & 63) !== 0) throw new Error();
133
+ return JSON.parse(await readFile(path, "utf8"));
134
+ } catch {
135
+ throw new SellerError("LOCAL_STATE_MISSING", "Private seller session or review is missing or unreadable.", "Run sell login, or prepare a new review after checking inventory.");
136
+ }
137
+ }
138
+ async remove(name) {
139
+ await unlink(join(this.directory, name)).catch(() => {
140
+ });
141
+ }
142
+ };
143
+ var SellerClient = class {
144
+ constructor(store = new SellerStore(), fetcher = fetch) {
145
+ this.store = store;
146
+ this.fetcher = fetcher;
147
+ }
148
+ async request(path, body, token) {
149
+ let response;
150
+ try {
151
+ response = await this.fetcher(`${SELLER_ORIGIN}${path}`, { method: body === void 0 ? "GET" : "POST", headers: { ...body === void 0 ? {} : { "Content-Type": "application/json" }, ...token ? { Authorization: `Bearer ${token}` } : {} }, ...body === void 0 ? {} : { body: JSON.stringify(body) }, redirect: "error", signal: AbortSignal.timeout(3e4) });
152
+ } catch {
153
+ throw new SellerError("SELLER_OUTCOME_UNKNOWN", "The seller service response was not received.", "For a confirm, retry the SAME review ID. For prepare, check inventory before preparing another attempt.", true);
154
+ }
155
+ const data = await response.json().catch(() => null);
156
+ if (!response.ok) {
157
+ const errors = {
158
+ authentication_required: { message: "Seller sign-in is required.", action: "Run tixbit sell login to authorize this CLI." },
159
+ seller_unavailable: { message: "Seller account support is not configured.", action: "Ask the operator to configure the seller account bridge and approved OAuth client." },
160
+ permission_required: { message: "Current seller account permission is required.", action: "Check seller ownership, sign in to the correct account, and reconnect to grant inventory:read and seller:write." },
161
+ invalid_request: { message: "The seller request is invalid.", action: "Check required fields, exact event and seat IDs, quantity, price in cents and UUID attempt. Correct the request before preparing again." },
162
+ invalid_content_type: { message: "The seller request requires JSON.", action: "Use the documented CLI JSON input or send application/json to the API." },
163
+ account_unavailable: { message: "The seller account service is temporarily unavailable.", action: path === "/api/seller/confirm" ? "Read review status and inventory before retrying. Retain the SAME review ID and confirmation; do not prepare a new mutation attempt." : "Retry the read when service recovers. For preparation, reconcile inventory and the existing attempt before preparing again." },
164
+ review_or_listing_unavailable: { message: "The review or owned listing is unavailable.", action: "Run sell list to refresh owned inventory. Use an owned listing or review ID and check that the review has not expired." },
165
+ review_conflict: { message: "The seller review changed, expired or requires reconciliation.", action: "Read sell review and sell list. Retain the existing confirmation; never repeat an uncertain mutation with a new attempt." }
166
+ };
167
+ const code = typeof data?.error?.code === "string" && Object.hasOwn(errors, data.error.code) ? data.error.code : null;
168
+ if (code) {
169
+ const known = errors[code];
170
+ throw new SellerError(code, known.message, known.action, data.error.retryable === true);
171
+ }
172
+ throw new SellerError(`SELLER_HTTP_${response.status}`, `Seller request failed (HTTP ${response.status}).`, "Check command input and service status. Reconcile review status and inventory before retrying any mutation.", false);
173
+ }
174
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw new SellerError("INVALID_SELLER_RESPONSE", "Seller service returned an invalid response.", "Check review status and inventory before another attempt.");
175
+ return redact(data, [token ?? ""]);
176
+ }
177
+ async token() {
178
+ const session = await this.store.load("session.json");
179
+ if (!session.accessToken || !Number.isFinite(session.expiresAt) || session.expiresAt <= Date.now()) throw new SellerError("SELLER_LOGIN_EXPIRED", "Seller sign-in has expired.", "Run sell login again. Saved reviews can be checked after sign-in.");
180
+ return session.accessToken;
181
+ }
182
+ capabilities() {
183
+ return this.request("/api/seller/capabilities");
184
+ }
185
+ async inventory() {
186
+ return this.request("/api/seller/inventory", void 0, await this.token());
187
+ }
188
+ async status(id) {
189
+ const inventory = await this.inventory();
190
+ if (!id) return inventory;
191
+ const listing = inventory.listings?.find((l) => l.listingId === id);
192
+ if (!listing) throw new SellerError("LISTING_NOT_FOUND", "Listing not found in this seller account.", "Run sell list and choose an owned listing ID.");
193
+ return { listing };
194
+ }
195
+ async prepare(operation, body, attempt = randomUUID()) {
196
+ sellerId(attempt);
197
+ const normalized = normalizeSellerOperation(operation, body);
198
+ const token = await this.token();
199
+ try {
200
+ await this.store.load(`attempt-${attempt}.json`);
201
+ throw new SellerError("ATTEMPT_ALREADY_SENT", "This preparation attempt has already been sent.", "Check sell list and sell review before starting a new attempt.");
202
+ } catch (error) {
203
+ if (!(error instanceof SellerError) || error.code !== "LOCAL_STATE_MISSING") throw error;
204
+ }
205
+ await this.store.save(`attempt-${attempt}.json`, { operation, body: normalized, attempt });
206
+ let result;
207
+ try {
208
+ result = await this.request("/api/seller/prepare", { operation, body: normalized, attempt }, token);
209
+ } catch (error) {
210
+ if (error instanceof SellerError) error.action += ` Preparation attempt: ${attempt}.`;
211
+ throw error;
212
+ }
213
+ if (!result.review || !uuid.test(result.review.id) || result.confirmation?.reviewId !== result.review.id || !/^[A-Za-z0-9_-]{43}$/.test(result.confirmation.nonce)) throw new SellerError("INVALID_SELLER_REVIEW", "Invalid server review.", "Check inventory before another attempt.");
214
+ await this.store.save(`review-${result.review.id}.json`, result);
215
+ return { review: result.review, attempt, action: `Review the operation and seller terms. To approve: tixbit sell confirm ${result.review.id} --confirm. To abandon: tixbit sell cancel ${result.review.id}. Submission does not guarantee a live marketplace listing.` };
216
+ }
217
+ async review(id) {
218
+ return this.request(`/api/seller/reviews/${sellerId(id)}`, void 0, await this.token());
219
+ }
220
+ async confirm(id, approved) {
221
+ if (approved !== true) throw new TypeError("Explicit approval requires --confirm after reviewing the prepared operation");
222
+ const saved = await this.store.load(`review-${sellerId(id)}.json`);
223
+ if (saved.confirmation?.reviewId !== id) throw new TypeError("Review identity mismatch");
224
+ return this.request("/api/seller/confirm", saved.confirmation, await this.token());
225
+ }
226
+ async cancel(id) {
227
+ const saved = await this.store.load(`review-${sellerId(id)}.json`);
228
+ return this.request("/api/seller/cancel", saved.confirmation, await this.token());
229
+ }
230
+ async logout() {
231
+ await this.store.remove("session.json");
232
+ await this.store.remove("login.json");
233
+ return { signedIn: false, action: "Local credentials removed. To revoke an existing grant, use your TixBit account connections." };
234
+ }
235
+ async authStatus() {
236
+ try {
237
+ const session = await this.store.load("session.json");
238
+ return { signedIn: session.expiresAt > Date.now(), expiresAt: session.expiresAt, action: "Run sell list to validate current server access. Local status does not prove the grant is still active." };
239
+ } catch {
240
+ return { signedIn: false, action: "Run tixbit sell login to sign in with the built-in public CLI client." };
241
+ }
242
+ }
243
+ async startLogin(clientId = DEFAULT_SELLER_CLIENT_ID) {
244
+ text(clientId, "clientId", 8192);
245
+ const pending = { clientId, state: randomBytes(32).toString("base64url"), verifier: randomBytes(32).toString("base64url"), expiresAt: Date.now() + 6e5 };
246
+ await this.store.save("login.json", pending);
247
+ const url = new URL(`${SELLER_ORIGIN}/api/oauth/authorize`);
248
+ for (const [key, value] of Object.entries({ response_type: "code", client_id: clientId, redirect_uri: SELLER_CALLBACK, scope: SELLER_SCOPES, resource: `${SELLER_ORIGIN}/api/mcp`, state: pending.state, code_challenge: createHash("sha256").update(pending.verifier).digest("base64url"), code_challenge_method: "S256" })) url.searchParams.set(key, value);
249
+ return { signInUrl: url.toString(), action: "Open this URL in your browser, sign in and approve seller access. Then run tixbit sell login --complete and paste the full callback URL on stdin. Never paste tokens or browser cookies.", expiresAt: pending.expiresAt };
250
+ }
251
+ async completeLogin(callback) {
252
+ const pending = await this.store.load("login.json");
253
+ let url;
254
+ try {
255
+ url = new URL(callback.trim());
256
+ } catch {
257
+ throw new TypeError("Paste the full TixBit callback URL from your browser");
258
+ }
259
+ if (url.origin + url.pathname !== SELLER_CALLBACK || url.hash || url.username || url.password || pending.expiresAt <= Date.now() || url.searchParams.get("state") !== pending.state || url.searchParams.getAll("state").length !== 1 || url.searchParams.getAll("code").length !== 1 || !url.searchParams.get("code") || url.searchParams.has("error")) throw new TypeError("Callback URL, state, or login expiry does not match. Run sell login again.");
260
+ await this.store.remove("login.json");
261
+ let response;
262
+ try {
263
+ response = await this.fetcher(`${SELLER_ORIGIN}/api/oauth/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: pending.clientId, redirect_uri: SELLER_CALLBACK, resource: `${SELLER_ORIGIN}/api/mcp`, code: url.searchParams.get("code"), code_verifier: pending.verifier }), redirect: "error", signal: AbortSignal.timeout(3e4) });
264
+ } catch {
265
+ throw new SellerError("LOGIN_EXCHANGE_FAILED", "Could not complete login.", "Run sell login again; do not reuse an uncertain authorization code.");
266
+ }
267
+ const result = await response.json().catch(() => null);
268
+ await this.store.remove("login.json");
269
+ if (!response.ok || typeof result?.access_token !== "string" || !result.access_token || result.token_type?.toLowerCase() !== "bearer" || !Number.isFinite(result.expires_in) || result.expires_in <= 0) throw new SellerError("LOGIN_EXCHANGE_FAILED", "Seller authorization was not granted.", "Run sell login again. Ask the operator to approve the CLI client and seller scopes if unavailable.");
270
+ await this.store.save("session.json", { accessToken: result.access_token, clientId: pending.clientId, expiresAt: Date.now() + result.expires_in * 1e3 });
271
+ return { signedIn: true, expiresIn: result.expires_in, action: "Run tixbit sell list to inspect your inventory." };
272
+ }
273
+ };
274
+
44
275
  // src/cli.ts
45
276
  import { createRequire as createRequire2 } from "module";
46
277
 
@@ -425,7 +656,8 @@ var TixBitClient = class {
425
656
  }),
426
657
  signal: controller.signal
427
658
  });
428
- const body = await response.json().catch(() => ({}));
659
+ const value = await response.json().catch(() => void 0);
660
+ const body = value && typeof value === "object" && !Array.isArray(value) ? value : {};
429
661
  const orderReference = normalizeOrderReference(body.orderReference);
430
662
  const receiptUrl = normalizeReceiptUrl(body.receiptUrl);
431
663
  if (response.status >= 500 && body.status !== "manual_review_required") {
@@ -458,21 +690,21 @@ var TixBitClient = class {
458
690
  action: "Configure the official mppx payment client, then retry with the same idempotency key."
459
691
  };
460
692
  }
693
+ if (response.ok && (!isPurchaseStatus(body.status) || body.status === "fulfilled" && (body.success !== true || !orderReference))) {
694
+ return {
695
+ success: false,
696
+ status: "pending",
697
+ idempotencyKey,
698
+ orderReference,
699
+ receiptUrl,
700
+ error: {
701
+ code: "PURCHASE_OUTCOME_AMBIGUOUS",
702
+ message: "The server did not return a confirmed purchase outcome."
703
+ },
704
+ action: "Check the order with the same idempotency key. Do not create a new payment or purchase attempt."
705
+ };
706
+ }
461
707
  if (isPurchaseStatus(body.status)) {
462
- if (body.status === "fulfilled" && response.ok && body.success === true && !orderReference) {
463
- return {
464
- success: false,
465
- status: "pending",
466
- idempotencyKey,
467
- orderReference: null,
468
- receiptUrl,
469
- error: {
470
- code: "PURCHASE_OUTCOME_AMBIGUOUS",
471
- message: "The purchase response did not include an order reference."
472
- },
473
- action: "Retry with the same idempotency key. Do not create a new payment or purchase attempt."
474
- };
475
- }
476
708
  if (body.status === "fulfilled" && (!response.ok || body.success !== true)) {
477
709
  return rejectedPurchaseResult(
478
710
  idempotencyKey,
@@ -868,10 +1100,10 @@ var TixBitTimeoutError = class extends Error {
868
1100
 
869
1101
  // src/link.ts
870
1102
  import { execFile } from "child_process";
871
- import { randomUUID } from "crypto";
872
- import { mkdir, readFile, rename, writeFile } from "fs/promises";
873
- import { homedir } from "os";
874
- import { dirname, join } from "path";
1103
+ import { randomUUID as randomUUID2 } from "crypto";
1104
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1105
+ import { homedir as homedir2 } from "os";
1106
+ import { dirname, join as join2 } from "path";
875
1107
  import { createRequire } from "module";
876
1108
  import { promisify } from "util";
877
1109
  import { Challenge } from "mppx";
@@ -887,7 +1119,7 @@ function linkCliEntry() {
887
1119
  throw new LinkCheckoutError("LINK_NODE_UNSUPPORTED", "TixBit needs Node.js 22 or later.");
888
1120
  }
889
1121
  try {
890
- return join(dirname(require2.resolve("@stripe/link-cli/package.json")), "dist", "cli.js");
1122
+ return join2(dirname(require2.resolve("@stripe/link-cli/package.json")), "dist", "cli.js");
891
1123
  } catch {
892
1124
  throw new LinkCheckoutError("LINK_CLI_MISSING", "The bundled Link CLI is missing. Reinstall TixBit, then retry.");
893
1125
  }
@@ -905,20 +1137,20 @@ function paymentEndpoint() {
905
1137
  }
906
1138
  function statePath(orderReference) {
907
1139
  if (!ORDER_REFERENCE.test(orderReference)) throw new LinkCheckoutError("INVALID_ORDER_REFERENCE", "Enter the order reference shown by link start.");
908
- const base = process.env.XDG_STATE_HOME || join(homedir(), ".local", "state");
909
- return join(base, "tixbit", "link", `${orderReference}.json`);
1140
+ const base = process.env.XDG_STATE_HOME || join2(homedir2(), ".local", "state");
1141
+ return join2(base, "tixbit", "link", `${orderReference}.json`);
910
1142
  }
911
1143
  async function saveState(state) {
912
1144
  const path = statePath(state.orderReference);
913
- await mkdir(dirname(path), { recursive: true, mode: 448 });
914
- const temporary = `${path}.${randomUUID()}.tmp`;
915
- await writeFile(temporary, JSON.stringify(state), { mode: 384, flag: "wx" });
916
- await rename(temporary, path);
1145
+ await mkdir2(dirname(path), { recursive: true, mode: 448 });
1146
+ const temporary = `${path}.${randomUUID2()}.tmp`;
1147
+ await writeFile2(temporary, JSON.stringify(state), { mode: 384, flag: "wx" });
1148
+ await rename2(temporary, path);
917
1149
  }
918
1150
  async function loadState(orderReference) {
919
1151
  let value;
920
1152
  try {
921
- value = JSON.parse(await readFile(statePath(orderReference), "utf8"));
1153
+ value = JSON.parse(await readFile2(statePath(orderReference), "utf8"));
922
1154
  } catch {
923
1155
  throw new LinkCheckoutError("CHECKOUT_NOT_FOUND", "This Link checkout was not found on this device.");
924
1156
  }
@@ -1002,7 +1234,7 @@ function approvalUrl(value) {
1002
1234
  }
1003
1235
  async function startLinkPurchase(input) {
1004
1236
  linkCliEntry();
1005
- const purchase = normalizePurchaseTicketsParams({ ...input, idempotencyKey: randomUUID() });
1237
+ const purchase = normalizePurchaseTicketsParams({ ...input, idempotencyKey: randomUUID2() });
1006
1238
  if (!Number.isSafeInteger(input.maxAmountCents) || input.maxAmountCents <= 0) {
1007
1239
  throw new LinkCheckoutError("INVALID_MAX_PRICE", "Set a positive total price limit.");
1008
1240
  }
@@ -1061,35 +1293,39 @@ async function startLinkPurchase(input) {
1061
1293
  action: `Approve in Link, then run: tixbit link complete ${body.orderReference}`
1062
1294
  };
1063
1295
  }
1296
+ function purchaseOutcome(state, result) {
1297
+ if (result.orderReference && result.orderReference !== state.orderReference) {
1298
+ throw new LinkCheckoutError("ORDER_REFERENCE_CHANGED", "TixBit returned a different order reference. Check the order before another payment.");
1299
+ }
1300
+ const sameOrder = result.orderReference === state.orderReference;
1301
+ const terminalFailure = sameOrder && (result.status === "payment_failed" || result.status === "rejected");
1302
+ const manualReview = sameOrder && result.status === "manual_review_required";
1303
+ const fulfilled = sameOrder && result.success && result.status === "fulfilled";
1304
+ return {
1305
+ success: fulfilled,
1306
+ status: fulfilled || manualReview || terminalFailure ? result.status : "pending",
1307
+ orderReference: state.orderReference,
1308
+ amountCents: state.amountCents,
1309
+ ...sameOrder && result.order ? { order: result.order } : {},
1310
+ ...sameOrder && result.receiptUrl ? { receiptUrl: result.receiptUrl } : {},
1311
+ ...!fulfilled ? {
1312
+ error: sameOrder && (manualReview || terminalFailure) && result.error ? result.error : { code: "PURCHASE_OUTCOME_AMBIGUOUS", message: "The order is still being checked." },
1313
+ action: terminalFailure || manualReview ? result.action ?? "Contact TixBit support with this order reference. Do not submit another payment." : "Run the same complete command again later. Do not start another payment or purchase."
1314
+ } : {}
1315
+ };
1316
+ }
1064
1317
  async function completeLinkPurchase(orderReference) {
1065
1318
  const state = await loadState(orderReference);
1066
- if (state.result && ["fulfilled", "payment_failed", "rejected"].includes(state.result.status)) return state.result;
1319
+ const ambiguousLegacyResult = state.paymentAttempted && state.result?.status === "rejected" && /^HTTP_2\d\d$/.test(state.result.error?.code ?? "");
1320
+ if (state.result && !ambiguousLegacyResult && ["fulfilled", "payment_failed", "rejected"].includes(state.result.status)) return state.result;
1067
1321
  if (state.paymentAttempted) {
1068
1322
  const request2 = JSON.parse(state.requestBody);
1069
1323
  const result2 = await new TixBitClient({ paymentEndpoint: paymentEndpoint(), timeoutMs: 3e4 }).purchaseTickets(request2);
1070
- if (result2.orderReference && result2.orderReference !== orderReference) {
1071
- throw new LinkCheckoutError("ORDER_REFERENCE_CHANGED", "TixBit returned a different order reference. Check the order before another payment.");
1072
- }
1073
- if (state.result?.status === "manual_review_required" && result2.status !== "fulfilled") {
1324
+ const output3 = purchaseOutcome(state, result2);
1325
+ if (state.result?.status === "manual_review_required" && !output3.success) {
1074
1326
  return state.result;
1075
1327
  }
1076
- const sameOrder = result2.orderReference === orderReference;
1077
- const terminalFailure = sameOrder && (result2.status === "payment_failed" || result2.status === "rejected");
1078
- const manualReview = sameOrder && result2.status === "manual_review_required";
1079
- const fulfilled = sameOrder && result2.success && result2.status === "fulfilled";
1080
- const output3 = {
1081
- success: fulfilled,
1082
- status: fulfilled || manualReview || terminalFailure ? result2.status : "pending",
1083
- orderReference,
1084
- amountCents: state.amountCents,
1085
- ...sameOrder && result2.order ? { order: result2.order } : {},
1086
- ...sameOrder && result2.receiptUrl ? { receiptUrl: result2.receiptUrl } : {},
1087
- ...!fulfilled ? {
1088
- error: sameOrder && (manualReview || terminalFailure) && result2.error ? result2.error : { code: "PURCHASE_OUTCOME_AMBIGUOUS", message: "The order is still being checked." },
1089
- action: terminalFailure || manualReview ? result2.action ?? "Contact TixBit support with this order reference. Do not submit another payment." : "Run the same complete command again later. Do not start another payment or purchase."
1090
- } : {}
1091
- };
1092
- if (output3.success || terminalFailure || output3.status === "manual_review_required") {
1328
+ if (ambiguousLegacyResult || output3.success || ["payment_failed", "rejected", "manual_review_required"].includes(output3.status)) {
1093
1329
  state.result = output3;
1094
1330
  await saveState(state);
1095
1331
  }
@@ -1160,21 +1396,15 @@ async function completeLinkPurchase(orderReference) {
1160
1396
  const request = JSON.parse(state.requestBody);
1161
1397
  const result = await client2.purchaseTickets(request);
1162
1398
  if (challengeChanged) throw new LinkCheckoutError("CHALLENGE_CHANGED", "The order or payment amount changed. No payment was submitted.");
1163
- if (result.orderReference && result.orderReference !== orderReference) {
1164
- throw new LinkCheckoutError("ORDER_REFERENCE_CHANGED", "TixBit returned a different order reference. Check the order before another payment.");
1165
- }
1166
- const output2 = {
1167
- success: result.success,
1168
- status: decline ? "payment_failed" : result.status,
1399
+ const outcome = purchaseOutcome(state, result);
1400
+ const output2 = decline ? {
1401
+ success: false,
1402
+ status: "payment_failed",
1169
1403
  orderReference,
1170
1404
  amountCents: state.amountCents,
1171
- ...result.order && !decline ? { order: result.order } : {},
1172
- ...result.receiptUrl && !decline ? { receiptUrl: result.receiptUrl } : {},
1173
- ...!result.success ? {
1174
- error: decline ? { code: "CARD_DECLINED", message: "Stripe declined the card selected in Link." } : result.error,
1175
- action: decline ? "Select another card in Link and start a new spend approval. No ticket was issued." : result.status === "pending" ? "Payment may have succeeded. Do not retry or start another checkout. Contact TixBit support with this order reference." : result.action
1176
- } : {}
1177
- };
1405
+ error: { code: "CARD_DECLINED", message: "Stripe declined the card selected in Link." },
1406
+ action: "Select another card in Link and start a new spend approval. No ticket was issued."
1407
+ } : outcome;
1178
1408
  state.result = output2;
1179
1409
  await saveState(state);
1180
1410
  return output2;
@@ -1190,6 +1420,7 @@ var client = (() => {
1190
1420
  })();
1191
1421
  var packageJson = createRequire2(import.meta.url)("../package.json");
1192
1422
  function output(data, json) {
1423
+ data = JSON.parse(JSON.stringify(data, (key, value) => /nonce|verifier/i.test(key) ? "[REDACTED]" : value));
1193
1424
  data = redact(data, [process.env.TIXBIT_LINK_TOKEN ?? "", process.env.TIXBIT_ACCESS_TOKEN ?? "", process.env.MPPX_PRIVATE_KEY ?? ""]);
1194
1425
  if (data && typeof data === "object" && !Array.isArray(data)) {
1195
1426
  data = { success: true, ...data };
@@ -1266,11 +1497,15 @@ function formatItem(item) {
1266
1497
  return JSON.stringify(item, null, 2);
1267
1498
  }
1268
1499
  function handleError(err) {
1500
+ if (err instanceof SellerError) {
1501
+ output({ success: false, error: { code: err.code, message: err.message, retryable: err.retryable }, action: err.action }, true);
1502
+ process.exit(1);
1503
+ }
1269
1504
  const msg = err instanceof TypeError || err instanceof RangeError ? err.message : "Request failed. Check input and service availability; do not retry an uncertain write.";
1270
1505
  output({ success: false, error: { code: "COMMAND_FAILED", message: msg } }, true);
1271
1506
  process.exit(1);
1272
1507
  }
1273
- var program = new Command().name("tixbit").description("Search events, browse listings, and buy TixBit tickets").version(packageJson.version);
1508
+ var program = new Command().name("tixbit").description("Search events, buy tickets, and manage your own seller listings").version(packageJson.version);
1274
1509
  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) => {
1275
1510
  try {
1276
1511
  const params = {
@@ -1387,38 +1622,57 @@ program.command("buy <listingId>").description("Quote or confirm Stripe Link che
1387
1622
  handleError(error);
1388
1623
  }
1389
1624
  });
1390
- var sell = program.command("sell").description("Seller browser sign-in or optional authorized user integration");
1391
- sell.command("list").option("--json", "Output JSON", true).action(async () => {
1625
+ var seller = new SellerClient();
1626
+ var sell = program.command("sell").description("Sign in, review and confirm seller inventory changes");
1627
+ var sellerAction = (fn) => async (...args) => {
1392
1628
  try {
1393
- output(await client.listSellerListings(process.env.TIXBIT_ACCESS_TOKEN ?? ""), true);
1629
+ output(await fn(...args), true);
1394
1630
  } catch (error) {
1395
1631
  handleError(error);
1396
1632
  }
1397
- });
1398
- sell.command("create").option("--confirm", "Approve listing creation and seller terms", false).option("--json", "Output JSON", true).action(async (opts) => {
1399
- try {
1400
- if (!opts.confirm) throw new TypeError("sell create requires --confirm and approved listing JSON on stdin");
1401
- if (!process.env.TIXBIT_ACCESS_TOKEN) {
1402
- output(await client.listSellerListings(), true);
1403
- return;
1404
- }
1405
- if (process.stdin.isTTY) throw new TypeError("Provide listing JSON on stdin");
1406
- let input = "";
1407
- for await (const chunk of process.stdin) {
1408
- input += chunk.toString();
1409
- if (Buffer.byteLength(input) > 65536) throw new TypeError("Listing JSON exceeds 64 KiB");
1410
- }
1633
+ };
1634
+ async function readSellerInput(file) {
1635
+ if (file) {
1636
+ const input2 = await readFile3(file, "utf8");
1637
+ if (Buffer.byteLength(input2) > 65536) throw new TypeError("Input exceeds 64 KiB");
1638
+ return input2;
1639
+ }
1640
+ if (process.stdin.isTTY) process.stderr.write("Paste input, then press Ctrl-D to finish.\n");
1641
+ let input = "";
1642
+ for await (const chunk of process.stdin) {
1643
+ input += chunk.toString();
1644
+ if (Buffer.byteLength(input) > 65536) throw new TypeError("Input exceeds 64 KiB");
1645
+ }
1646
+ return input;
1647
+ }
1648
+ sell.command("capabilities").description("Discover public seller support, limits and configuration readiness").action(sellerAction(() => seller.capabilities()));
1649
+ sell.command("login").description("Authorize the CLI through browser OAuth with PKCE; no browser cookies or manual tokens").option("--client-id <id>", "Override the built-in public OAuth client ID (or TIXBIT_SELLER_CLIENT_ID)").option("--complete", "Read the full callback URL from stdin and securely finish authorization").action(sellerAction(async (opts) => {
1650
+ if (opts.complete) return seller.completeLogin(await readSellerInput());
1651
+ const clientId = opts.clientId ?? process.env.TIXBIT_SELLER_CLIENT_ID ?? DEFAULT_SELLER_CLIENT_ID;
1652
+ return seller.startLogin(clientId);
1653
+ }));
1654
+ sell.command("auth-status").description("Check local login expiry without exposing credentials").action(sellerAction(() => seller.authStatus()));
1655
+ sell.command("logout").description("Remove local CLI credentials; account grants can be revoked in TixBit").action(sellerAction(() => seller.logout()));
1656
+ sell.command("list").description("Read inventory owned by the signed-in seller").option("--json", "Output JSON", true).action(sellerAction(() => seller.inventory()));
1657
+ sell.command("status [listingId]").description("Read current owned listing status (or all inventory)").action(sellerAction((id) => seller.status(id)));
1658
+ sell.command("review <reviewId>").description("Reconcile a saved review and any completed result").action(sellerAction((id) => seller.review(id)));
1659
+ sell.command("confirm <reviewId>").description("Submit exactly the saved preview; reuse the same review ID after an uncertain response").requiredOption("--confirm", "Approve this previously reviewed operation and seller terms").action(sellerAction((id, opts) => seller.confirm(id, opts.confirm)));
1660
+ sell.command("cancel <reviewId>").description("Abandon a prepared review without changing a listing").action(sellerAction((id) => seller.cancel(id)));
1661
+ for (const name of ["create", "reprice", "delist"]) {
1662
+ sell.command(name).description(`${name === "create" ? "Prepare a listing" : name === "reprice" ? "Prepare a per-ticket price change" : "Prepare removal of an owned listing"}; JSON file or stdin, review before confirm`).option("--file <path>", "Read approved listing/operation JSON from a file (default stdin)").option("--attempt <uuid>", "Stable UUID v4 preparation attempt ID").option("--dry-run", "Validate locally and print preview; no authenticated requests").option("--json", "Output JSON", true).action(sellerAction(async (opts) => {
1411
1663
  let body;
1412
1664
  try {
1413
- body = JSON.parse(input);
1414
- } catch {
1415
- throw new TypeError("Invalid listing JSON on stdin");
1665
+ body = JSON.parse(await readSellerInput(opts.file));
1666
+ } catch (error) {
1667
+ if (error instanceof SyntaxError) throw new TypeError("Invalid seller JSON. Use --help for input options.");
1668
+ throw error;
1416
1669
  }
1417
- output(await client.createSellerListing(body, process.env.TIXBIT_ACCESS_TOKEN ?? "", opts.confirm), true);
1418
- } catch (error) {
1419
- handleError(error);
1420
- }
1421
- });
1670
+ const operation = `seller.${name}`;
1671
+ const normalized = normalizeSellerOperation(operation, body);
1672
+ if (opts.dryRun) return { dryRun: true, operation, body: normalized, action: "Validate event, date, section, seats, per-ticket USD price and terms with the seller. Omit --dry-run to obtain a server review; no listing was changed." };
1673
+ return seller.prepare(operation, normalized, opts.attempt ?? randomUUID3());
1674
+ }));
1675
+ }
1422
1676
  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) => {
1423
1677
  try {
1424
1678
  const quantity = Number(opts.quantity);
@@ -1713,7 +1967,7 @@ function describePosition(section, allSections) {
1713
1967
  function configureCommands(command) {
1714
1968
  command.exitOverride();
1715
1969
  command.configureOutput({
1716
- writeOut: (text) => output({ help: text }, true),
1970
+ writeOut: (text2) => output({ help: text2 }, true),
1717
1971
  writeErr: () => {
1718
1972
  }
1719
1973
  });
package/dist/index.d.ts CHANGED
@@ -504,4 +504,71 @@ declare function assertMppChallenge(challenge: {
504
504
  request: Record<string, unknown>;
505
505
  }, maxAmountCents: number): void;
506
506
 
507
- export { type AgentResult, type BrowseEventsParams, type BrowseEventsResult, type BuyTicketsParams, type CheckoutLink, type CheckoutParams, type GetListingResult, type GetListingsParams, type GetListingsResult, type GetSeatmapParams, type LocationMode, type ParkingFilter, type PurchaseOrder, type PurchaseStatus, type PurchaseTicketsParams, type PurchaseTicketsResult, type SearchEventsParams, type SearchEventsResult, type SeatmapLabel, type SeatmapResult, type SeatmapSection, type SeatmapVenue, type SeatmapZone, TixBitApiError, TixBitClient, type TixBitConfig, type TixBitEvent, type TixBitEventDetail, type TixBitListing, type TixBitListingDisclosure, TixBitTimeoutError, assertMppChallenge };
507
+ declare const DEFAULT_SELLER_CLIENT_ID = "tixbit-client.eyJ0eXAiOiJjbGllbnQiLCJpYXQiOjE3OTIxOTI4MDAsImNsaWVudE5hbWUiOiJUaXhCaXQgQ0xJIiwicmVkaXJlY3RVcmlzIjpbImh0dHBzOi8vbWNwLnRpeGJpdC5jb20vY2xpL2NhbGxiYWNrIl0sInNjb3BlIjoibWNwOnJlYWQgaW52ZW50b3J5OnJlYWQgc2VsbGVyOndyaXRlIn0.C9ijXrH07kd2p15Dd1z5yzfVsz4XoBUJI90GP9Rbg7k";
508
+ type SellerOperation = "seller.create" | "seller.reprice" | "seller.delist";
509
+ declare class SellerError extends Error {
510
+ code: string;
511
+ action: string;
512
+ retryable: boolean;
513
+ constructor(code: string, message: string, action: string, retryable?: boolean);
514
+ }
515
+ /** Strictly validate the actual seller API contract; no payment or fulfillment fields. */
516
+ declare function normalizeSellerOperation(operation: SellerOperation, value: unknown): Record<string, unknown>;
517
+ /** Credentials and confirmation capabilities stay in private files, never stdout or argv. */
518
+ declare class SellerStore {
519
+ directory: string;
520
+ constructor(directory?: string);
521
+ save(name: string, value: unknown): Promise<void>;
522
+ load<T>(name: string): Promise<T>;
523
+ remove(name: string): Promise<void>;
524
+ }
525
+ declare class SellerClient {
526
+ store: SellerStore;
527
+ private fetcher;
528
+ constructor(store?: SellerStore, fetcher?: typeof fetch);
529
+ private request;
530
+ token(): Promise<string>;
531
+ capabilities(): Promise<any>;
532
+ inventory(): Promise<any>;
533
+ status(id?: string): Promise<any>;
534
+ prepare(operation: SellerOperation, body: unknown, attempt?: string): Promise<{
535
+ review: {
536
+ id: string;
537
+ operation: {
538
+ name: SellerOperation;
539
+ body: Record<string, unknown>;
540
+ };
541
+ expiresAt: number;
542
+ };
543
+ attempt: string;
544
+ action: string;
545
+ }>;
546
+ review(id: string): Promise<any>;
547
+ confirm(id: string, approved: boolean): Promise<any>;
548
+ cancel(id: string): Promise<any>;
549
+ logout(): Promise<{
550
+ signedIn: boolean;
551
+ action: string;
552
+ }>;
553
+ authStatus(): Promise<{
554
+ signedIn: boolean;
555
+ expiresAt: number;
556
+ action: string;
557
+ } | {
558
+ signedIn: boolean;
559
+ action: string;
560
+ expiresAt?: undefined;
561
+ }>;
562
+ startLogin(clientId?: string): Promise<{
563
+ signInUrl: string;
564
+ action: string;
565
+ expiresAt: number;
566
+ }>;
567
+ completeLogin(callback: string): Promise<{
568
+ signedIn: boolean;
569
+ expiresIn: any;
570
+ action: string;
571
+ }>;
572
+ }
573
+
574
+ export { type AgentResult, type BrowseEventsParams, type BrowseEventsResult, type BuyTicketsParams, type CheckoutLink, type CheckoutParams, DEFAULT_SELLER_CLIENT_ID, type GetListingResult, type GetListingsParams, type GetListingsResult, type GetSeatmapParams, type LocationMode, type ParkingFilter, type PurchaseOrder, type PurchaseStatus, type PurchaseTicketsParams, type PurchaseTicketsResult, type SearchEventsParams, type SearchEventsResult, type SeatmapLabel, type SeatmapResult, type SeatmapSection, type SeatmapVenue, type SeatmapZone, SellerClient, SellerError, type SellerOperation, SellerStore, TixBitApiError, TixBitClient, type TixBitConfig, type TixBitEvent, type TixBitEventDetail, type TixBitListing, type TixBitListingDisclosure, TixBitTimeoutError, assertMppChallenge, normalizeSellerOperation };