tixbit 0.1.2 → 0.2.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/README.md CHANGED
@@ -8,9 +8,9 @@ No command requires a TixBit developer API key. Public discovery and links need
8
8
 
9
9
  Run `tixbit auth` for public browser sign-in and wallet setup links. This command does not create a CLI session. Open the returned TixBit sign-in page, then complete checkout or selling in your browser. Do not copy browser cookies or bearer tokens.
10
10
 
11
- The public service currently has no supported browser-to-CLI seller session exchange. Website sign-in therefore does not unlock seller API calls from this CLI. Checkout discovery also does not expose the merchant identity needed for automatic Link shared-payment-token authorization. The CLI does not guess it or invent a login endpoint.
11
+ The public service currently has no supported browser-to-CLI seller session exchange. Website sign-in therefore does not unlock seller API calls from this CLI. The `buy` web checkout route does not issue Link spend requests; it expects a token from an authorized integration.
12
12
 
13
- For wallet setup, use [official Link onboarding](https://link.com/agents): install `@stripe/link-cli` and run `link-cli onboard`. This requires your Link account, not a developer API key. It does not by itself authorize this CLI to charge TixBit. Browser checkout is the normal path until automatic authorization is supported; MPP remains available with your wallet.
13
+ For wallet setup, use [official Link onboarding](https://link.com/agents): install `@stripe/link-cli` and run `link-cli onboard`. This requires your Link account, not a developer API key. A Link spend request is a separate approval for a selected purchase. See [Link agent checkout](#link-agent-checkout).
14
14
 
15
15
  | Operation | Required user authorization |
16
16
  |---|---|
@@ -78,11 +78,51 @@ tixbit purchase <listing-id> \
78
78
 
79
79
  `checkout` remains link-only: it creates a `https://www.tixbit.com/checkout/process` URL for browser completion. `purchase` is the separate public MPP machine surface. It uses the official `mppx` client to answer the server's HTTP 402 challenge and currently selects the Tempo one-time charge rail configured by the server.
80
80
 
81
+ ### Stripe Link machine checkout
82
+
83
+ Sign in to Link once, then start a purchase with a total price limit:
84
+
85
+ TixBit 0.2.0 needs Node.js 22 or later. The included Link CLI uses your local Link sign-in; TixBit does not ask for a developer API key.
86
+
87
+ ```sh
88
+ npx @stripe/link-cli auth login
89
+ tixbit link start <listing-id> --quantity 1 --email buyer@example.com --max-price 25.00
90
+ ```
91
+
92
+ `link start` returns the exact server total and an `approvalUrl`. Open that URL and approve the spend request in Link. Then use the returned order reference:
93
+
94
+ ```sh
95
+ tixbit link complete <order-reference>
96
+ ```
97
+
98
+ The second command checks approval, sends one Stripe MPP credential, and reports whether the ticket was issued. If approval is still pending, it returns the same URL without sending payment. If the payment result is uncertain, use `link complete` with the same order reference. The CLI stores the purchase key in a private file under `~/.local/state/tixbit/link` (or `XDG_STATE_HOME`) and never prints or saves the Link payment token. Keep that file until the order is settled. Each command returns JSON; `approval_required` and payment failures use exit code 2.
99
+
100
+ Each spend request includes its TixBit order reference in the approval details. Link can still rate limit a matching amount and merchant for a short time, even after a request is canceled. If Link rejects a request, the CLI reports a safe error code such as `LINK_DUPLICATE_REQUEST` or `LINK_NETWORK_UNAVAILABLE` without printing Link credentials.
101
+
81
102
  `--email` is always required and is never inferred from git or local account state. If a request times out or returns `pending` or `manual_review_required`, reuse the same idempotency key and follow the returned `action`; do not start another payment.
82
103
 
104
+ ### Link agent checkout
105
+
106
+ Stripe's [Link CLI](https://github.com/stripe/link-cli) provides the spend-request and MPP payment tools. An agent host that supports local MCP servers can add it alongside TixBit:
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "link": {
112
+ "command": "npx",
113
+ "args": ["@stripe/link-cli", "--mcp"]
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ The buyer signs in with `link-cli auth login` and approves each spend request in Link. For a TixBit purchase, first POST the selected listing, quantity, buyer email, and a new UUID v4 idempotency key to `https://mcp.tixbit.com/api/purchase`. Inspect the `402` order total and Stripe challenge. Create a `shared_payment_token` spend request for that challenge's `network_id` and total in cents. After Link reports approval, use `link-cli mpp pay` with the same URL and identical POST body, including the original idempotency key. Link CLI handles the one-time credential; do not print or copy an SPT. Treat `pending` or `manual_review_required` as non-final and reconcile by the original idempotency key.
120
+
121
+ This flow requires the TixBit MCP server to advertise Stripe in its `402` challenge. The published `tixbit purchase` command still selects Tempo. The `tixbit buy --confirm` command still uses the separate web checkout integration and cannot create a Link spend request. An agent host must expose Link's spend-request and MPP tools to its agent; this package cannot add tools to a third-party host such as Muse.
122
+
83
123
  ### Live inventory, Stripe Link, and seller commands
84
124
 
85
- These additions require a build of this PR or a later authorized package release. The existing registry package is still named `tixbit`; this PR does not publish it.
125
+ These commands are in `tixbit@0.1.2`.
86
126
 
87
127
  ```sh
88
128
  # Refreshed listings, not a reservation. Fails if live freshness is unavailable.
@@ -150,6 +190,7 @@ tixbit purchase <listing-id> --quantity 2 --email buyer@example.com --confirm --
150
190
  | `listings <eventId>` | Get available ticket listings for an event |
151
191
  | `checkout <listingId>` | Get a checkout link to buy tickets |
152
192
  | `purchase <listingId>` | Buy a selected listing through MPP with explicit confirmation and total cap |
193
+ | `link start <listingId>` / `link complete <orderReference>` | Approve and pay for a ticket with Stripe Link |
153
194
  | `quote <eventId>` | Refresh live listings, with freshness metadata |
154
195
  | `buy <listingId>` | Quote or confirm a capped Stripe Link purchase |
155
196
  | `sell list` / `sell create` | Use the existing gated seller API |
package/dist/cli.js CHANGED
@@ -42,7 +42,7 @@ function redact(value, secrets = []) {
42
42
  }
43
43
 
44
44
  // src/cli.ts
45
- import { createRequire } from "module";
45
+ import { createRequire as createRequire2 } from "module";
46
46
 
47
47
  // src/client.ts
48
48
  var DEFAULT_BASE_URL = "https://www.tixbit.com";
@@ -852,6 +852,287 @@ var TixBitTimeoutError = class extends Error {
852
852
  }
853
853
  };
854
854
 
855
+ // src/link.ts
856
+ import { execFile } from "child_process";
857
+ import { randomUUID } from "crypto";
858
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
859
+ import { homedir } from "os";
860
+ import { dirname, join } from "path";
861
+ import { createRequire } from "module";
862
+ import { promisify } from "util";
863
+ import { Challenge } from "mppx";
864
+ import { Mppx, stripe } from "mppx/client";
865
+ var execFileAsync = promisify(execFile);
866
+ var PAYMENT_URL = "https://mcp.tixbit.com/api/purchase";
867
+ var ORDER_REFERENCE = /^TBM-[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{10}$/;
868
+ var NETWORK_ID = /^profile_[A-Za-z0-9]+$/;
869
+ var SPEND_REQUEST_ID = /^lsrq_[A-Za-z0-9]+$/;
870
+ var require2 = createRequire(import.meta.url);
871
+ function linkCliEntry() {
872
+ if (Number(process.versions.node.split(".")[0]) < 22) {
873
+ throw new LinkCheckoutError("LINK_NODE_UNSUPPORTED", "TixBit 0.2.0 needs Node.js 22 or later.");
874
+ }
875
+ try {
876
+ return join(dirname(require2.resolve("@stripe/link-cli/package.json")), "dist", "cli.js");
877
+ } catch {
878
+ throw new LinkCheckoutError("LINK_CLI_MISSING", "The bundled Link CLI is missing. Reinstall TixBit, then retry.");
879
+ }
880
+ }
881
+ var LinkCheckoutError = class extends Error {
882
+ constructor(code, message) {
883
+ super(message);
884
+ this.code = code;
885
+ }
886
+ };
887
+ function paymentEndpoint() {
888
+ const endpoint = process.env.TIXBIT_PAYMENT_URL ?? PAYMENT_URL;
889
+ new TixBitClient({ paymentEndpoint: endpoint });
890
+ return endpoint;
891
+ }
892
+ function statePath(orderReference) {
893
+ if (!ORDER_REFERENCE.test(orderReference)) throw new LinkCheckoutError("INVALID_ORDER_REFERENCE", "Enter the order reference shown by link start.");
894
+ const base = process.env.XDG_STATE_HOME || join(homedir(), ".local", "state");
895
+ return join(base, "tixbit", "link", `${orderReference}.json`);
896
+ }
897
+ async function saveState(state) {
898
+ const path = statePath(state.orderReference);
899
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
900
+ const temporary = `${path}.${randomUUID()}.tmp`;
901
+ await writeFile(temporary, JSON.stringify(state), { mode: 384, flag: "wx" });
902
+ await rename(temporary, path);
903
+ }
904
+ async function loadState(orderReference) {
905
+ let value;
906
+ try {
907
+ value = JSON.parse(await readFile(statePath(orderReference), "utf8"));
908
+ } catch {
909
+ throw new LinkCheckoutError("CHECKOUT_NOT_FOUND", "This Link checkout was not found on this device.");
910
+ }
911
+ if (value.version !== 1 || value.orderReference !== orderReference || !SPEND_REQUEST_ID.test(value.spendRequestId) || !NETWORK_ID.test(value.networkId) || !Number.isSafeInteger(value.amountCents) || value.amountCents <= 0 || typeof value.externalId !== "string" || !value.externalId || typeof value.requestBody !== "string") {
912
+ throw new LinkCheckoutError("INVALID_CHECKOUT_STATE", "The saved Link checkout is invalid. Contact TixBit support before starting another purchase.");
913
+ }
914
+ return value;
915
+ }
916
+ async function linkCli(args) {
917
+ let stdout;
918
+ const entry = linkCliEntry();
919
+ try {
920
+ const result = await execFileAsync(process.execPath, [entry, ...args, "--format", "json"], {
921
+ timeout: 3e4,
922
+ maxBuffer: 1024 * 1024,
923
+ windowsHide: true
924
+ });
925
+ stdout = typeof result === "string" ? result : result.stdout;
926
+ } catch (error) {
927
+ const output2 = error && typeof error === "object" && "stdout" in error ? String(error.stdout) : "";
928
+ let message = "";
929
+ try {
930
+ const parsed = JSON.parse(output2);
931
+ const data = parsed && typeof parsed === "object" && !Array.isArray(parsed) && "data" in parsed ? parsed.data : parsed;
932
+ const item = Array.isArray(data) ? data[0] : data;
933
+ if (item && typeof item === "object" && "message" in item) {
934
+ message = String(item.message);
935
+ } else {
936
+ const nested = parsed && typeof parsed === "object" && !Array.isArray(parsed) && "error" in parsed ? parsed.error : void 0;
937
+ if (nested && typeof nested === "object" && "message" in nested) {
938
+ message = String(nested.message);
939
+ }
940
+ }
941
+ } catch {
942
+ }
943
+ if (/duplicate spend requests|matching spend request/i.test(message)) {
944
+ throw new LinkCheckoutError("LINK_DUPLICATE_REQUEST", "Link blocked a matching spend request. Wait a few minutes, then start a new checkout.");
945
+ }
946
+ if (/Invalid network_id|could not retrieve merchant information/i.test(message)) {
947
+ throw new LinkCheckoutError("LINK_NETWORK_UNAVAILABLE", "Link could not resolve the merchant profile in this account and mode. No payment was sent.");
948
+ }
949
+ throw new LinkCheckoutError("LINK_CLI_FAILED", "Link could not complete this step. Check Link sign-in and payment methods, then retry.");
950
+ }
951
+ try {
952
+ const parsed = JSON.parse(stdout);
953
+ const data = parsed && typeof parsed === "object" && !Array.isArray(parsed) && "data" in parsed ? parsed.data : parsed;
954
+ const item = Array.isArray(data) ? data[0] : data;
955
+ if (item && typeof item === "object") return item;
956
+ } catch {
957
+ }
958
+ throw new LinkCheckoutError("LINK_CLI_RESPONSE_INVALID", "Link returned an invalid response. No payment was submitted.");
959
+ }
960
+ function stripeChallenge(response, maxAmountCents) {
961
+ let challenges;
962
+ try {
963
+ challenges = Challenge.fromResponseList(response);
964
+ } catch {
965
+ throw new LinkCheckoutError("STRIPE_CHALLENGE_MISSING", "TixBit did not offer a valid Stripe Link payment challenge.");
966
+ }
967
+ const challenge = challenges.find((item) => item.method === "stripe" && item.intent === "charge");
968
+ const request = challenge?.request;
969
+ const amount = Number(request?.amount);
970
+ const details = request?.methodDetails;
971
+ if (!challenge || request?.currency !== "usd" || !Number.isSafeInteger(amount) || amount <= 0 || amount > maxAmountCents || typeof details?.networkId !== "string" || !NETWORK_ID.test(details.networkId) || typeof request.externalId !== "string" || !request.externalId) {
972
+ throw new LinkCheckoutError("STRIPE_CHALLENGE_UNSAFE", "The Stripe challenge is missing order details or exceeds your total price limit.");
973
+ }
974
+ return { amountCents: amount, networkId: details.networkId, externalId: request.externalId };
975
+ }
976
+ function approvalUrl(value) {
977
+ if (typeof value !== "string") throw new LinkCheckoutError("LINK_APPROVAL_MISSING", "Link did not return an approval link.");
978
+ let url;
979
+ try {
980
+ url = new URL(value);
981
+ } catch {
982
+ throw new LinkCheckoutError("LINK_APPROVAL_INVALID", "Link returned an unexpected approval link.");
983
+ }
984
+ if (url.origin !== "https://app.link.com" || !url.pathname.startsWith("/activity/approve/")) {
985
+ throw new LinkCheckoutError("LINK_APPROVAL_INVALID", "Link returned an unexpected approval link.");
986
+ }
987
+ return url.toString();
988
+ }
989
+ async function startLinkPurchase(input) {
990
+ linkCliEntry();
991
+ const purchase = normalizePurchaseTicketsParams({ ...input, idempotencyKey: randomUUID() });
992
+ if (!Number.isSafeInteger(input.maxAmountCents) || input.maxAmountCents <= 0) {
993
+ throw new LinkCheckoutError("INVALID_MAX_PRICE", "Set a positive total price limit.");
994
+ }
995
+ const requestBody = JSON.stringify(purchase);
996
+ const response = await fetch(paymentEndpoint(), {
997
+ method: "POST",
998
+ redirect: "error",
999
+ headers: { "content-type": "application/json", "accept-payment": "stripe/charge" },
1000
+ body: requestBody,
1001
+ signal: AbortSignal.timeout(3e4)
1002
+ });
1003
+ if (response.status !== 402) throw new LinkCheckoutError("PURCHASE_PREPARE_FAILED", `TixBit could not prepare this checkout (HTTP ${response.status}).`);
1004
+ const { amountCents, networkId, externalId } = stripeChallenge(response, input.maxAmountCents);
1005
+ const body = await response.json();
1006
+ if (typeof body.orderReference !== "string" || !ORDER_REFERENCE.test(body.orderReference)) {
1007
+ throw new LinkCheckoutError("ORDER_REFERENCE_MISSING", "TixBit did not return an order reference.");
1008
+ }
1009
+ const context = `Approve one TixBit ticket purchase for order ${body.orderReference}, listing ${purchase.listingId}, quantity ${purchase.quantity}. The total charge is USD ${(amountCents / 100).toFixed(2)}. This request permits one machine payment for this selected order only.`;
1010
+ const created = await linkCli([
1011
+ "spend-request",
1012
+ "create",
1013
+ "--credential-type",
1014
+ "shared_payment_token",
1015
+ "--network-id",
1016
+ networkId,
1017
+ "--amount",
1018
+ String(amountCents),
1019
+ "--currency",
1020
+ "usd",
1021
+ "--context",
1022
+ context,
1023
+ "--metadata",
1024
+ `order_reference:${body.orderReference}`,
1025
+ "--request-approval"
1026
+ ]);
1027
+ if (typeof created.id !== "string" || !SPEND_REQUEST_ID.test(created.id)) {
1028
+ throw new LinkCheckoutError("LINK_SPEND_REQUEST_INVALID", "Link did not return a valid spend request.");
1029
+ }
1030
+ const url = approvalUrl(created.approval_url);
1031
+ await saveState({
1032
+ version: 1,
1033
+ orderReference: body.orderReference,
1034
+ requestBody,
1035
+ spendRequestId: created.id,
1036
+ approvalUrl: url,
1037
+ amountCents,
1038
+ networkId,
1039
+ externalId
1040
+ });
1041
+ return {
1042
+ success: false,
1043
+ status: "approval_required",
1044
+ orderReference: body.orderReference,
1045
+ amountCents,
1046
+ approvalUrl: url,
1047
+ action: `Approve in Link, then run: tixbit link complete ${body.orderReference}`
1048
+ };
1049
+ }
1050
+ async function completeLinkPurchase(orderReference) {
1051
+ const state = await loadState(orderReference);
1052
+ if (state.result?.status === "fulfilled" || state.result?.status === "manual_review_required") return state.result;
1053
+ const approval = await linkCli(["spend-request", "retrieve", state.spendRequestId, "--include", "shared_payment_token"]);
1054
+ if (["denied", "declined", "expired", "canceled", "cancelled"].includes(String(approval.status))) {
1055
+ return {
1056
+ success: false,
1057
+ status: "rejected",
1058
+ orderReference,
1059
+ amountCents: state.amountCents,
1060
+ error: { code: "LINK_APPROVAL_ENDED", message: "The Link spend approval ended without payment." },
1061
+ action: "Start a new Link checkout if you still want this ticket."
1062
+ };
1063
+ }
1064
+ if (approval.status !== "approved") {
1065
+ return {
1066
+ success: false,
1067
+ status: "approval_required",
1068
+ orderReference,
1069
+ amountCents: state.amountCents,
1070
+ approvalUrl: state.approvalUrl,
1071
+ action: "Approve this spend request in Link, then run the same complete command."
1072
+ };
1073
+ }
1074
+ const token = approval.shared_payment_token?.id;
1075
+ if (approval.credential_type !== "shared_payment_token" || approval.amount !== state.amountCents || approval.currency !== "usd" || approval.network_id !== state.networkId || typeof token !== "string" || !/^spt_[A-Za-z0-9_]+$/.test(token)) {
1076
+ throw new LinkCheckoutError("LINK_APPROVAL_MISMATCH", "The Link approval does not match this order. No payment was submitted.");
1077
+ }
1078
+ let challengeChanged = false;
1079
+ const payments = Mppx.create({
1080
+ methods: [stripe.charge({ paymentMethod: "link", createToken: async ({ amount, currency, networkId }) => {
1081
+ if (Number(amount) !== state.amountCents || currency !== "usd" || networkId !== state.networkId) {
1082
+ challengeChanged = true;
1083
+ throw new LinkCheckoutError("CHALLENGE_CHANGED", "The payment amount or network changed. No payment was submitted.");
1084
+ }
1085
+ return token;
1086
+ } })],
1087
+ polyfill: false,
1088
+ maxPaymentRetries: 1,
1089
+ onChallenge: async (challenge) => {
1090
+ const request2 = challenge.request;
1091
+ const details = request2.methodDetails;
1092
+ if (challenge.method !== "stripe" || challenge.intent !== "charge" || Number(request2.amount) !== state.amountCents || request2.currency !== "usd" || details?.networkId !== state.networkId || request2.externalId !== state.externalId) {
1093
+ challengeChanged = true;
1094
+ throw new LinkCheckoutError("CHALLENGE_CHANGED", "The order or payment amount changed. No payment was submitted.");
1095
+ }
1096
+ return void 0;
1097
+ }
1098
+ });
1099
+ let decline = false;
1100
+ const client2 = new TixBitClient({
1101
+ paymentEndpoint: paymentEndpoint(),
1102
+ timeoutMs: 9e4,
1103
+ paymentFetch: async (url, init) => {
1104
+ const response = await payments.fetch(url, { ...init, headers: { ...init?.headers, "accept-payment": "stripe/charge" } });
1105
+ if (response.status === 402) {
1106
+ const body = await response.clone().json().catch(() => ({}));
1107
+ decline = typeof body.detail === "string" && /card was declined/i.test(body.detail);
1108
+ }
1109
+ return response;
1110
+ }
1111
+ });
1112
+ const request = JSON.parse(state.requestBody);
1113
+ const result = await client2.purchaseTickets(request);
1114
+ if (challengeChanged) throw new LinkCheckoutError("CHALLENGE_CHANGED", "The order or payment amount changed. No payment was submitted.");
1115
+ if (result.orderReference && result.orderReference !== orderReference) {
1116
+ throw new LinkCheckoutError("ORDER_REFERENCE_CHANGED", "TixBit returned a different order reference. Check the order before another payment.");
1117
+ }
1118
+ const output2 = {
1119
+ success: result.success,
1120
+ status: decline ? "payment_failed" : result.status,
1121
+ orderReference,
1122
+ amountCents: state.amountCents,
1123
+ ...result.order && !decline ? { order: result.order } : {},
1124
+ ...!result.success ? {
1125
+ error: decline ? { code: "CARD_DECLINED", message: "Stripe declined the card selected in Link." } : result.error,
1126
+ action: decline ? "Select another card in Link and start a new spend approval. No ticket was issued." : result.action
1127
+ } : {}
1128
+ };
1129
+ if (output2.status === "fulfilled" || output2.status === "manual_review_required") {
1130
+ state.result = output2;
1131
+ await saveState(state);
1132
+ }
1133
+ return output2;
1134
+ }
1135
+
855
1136
  // src/cli.ts
856
1137
  var client = (() => {
857
1138
  try {
@@ -860,7 +1141,7 @@ var client = (() => {
860
1141
  return handleError(error);
861
1142
  }
862
1143
  })();
863
- var packageJson = createRequire(import.meta.url)("../package.json");
1144
+ var packageJson = createRequire2(import.meta.url)("../package.json");
864
1145
  function output(data, json) {
865
1146
  data = redact(data, [process.env.TIXBIT_LINK_TOKEN ?? "", process.env.TIXBIT_ACCESS_TOKEN ?? "", process.env.MPPX_PRIVATE_KEY ?? ""]);
866
1147
  if (data && typeof data === "object" && !Array.isArray(data)) {
@@ -1103,9 +1384,9 @@ program.command("checkout <listingId>").description("Get a checkout link to buy
1103
1384
  listingInfo = (await client.getListing(listingId)).listing;
1104
1385
  } catch {
1105
1386
  }
1106
- const link = client.createCheckoutLink({ listingId, quantity });
1387
+ const link2 = client.createCheckoutLink({ listingId, quantity });
1107
1388
  if (isJson) {
1108
- output({ ...link, listing: listingInfo ?? null }, true);
1389
+ output({ ...link2, listing: listingInfo ?? null }, true);
1109
1390
  return;
1110
1391
  }
1111
1392
  process.stdout.write("\n\u{1F39F} Checkout Link\n\n");
@@ -1126,7 +1407,7 @@ program.command("checkout <listingId>").description("Get a checkout link to buy
1126
1407
  process.stdout.write(` Quantity: ${quantity}
1127
1408
 
1128
1409
  `);
1129
- process.stdout.write(` ${link.url}
1410
+ process.stdout.write(` ${link2.url}
1130
1411
 
1131
1412
  `);
1132
1413
  process.stdout.write(
@@ -1136,6 +1417,41 @@ program.command("checkout <listingId>").description("Get a checkout link to buy
1136
1417
  handleError(err);
1137
1418
  }
1138
1419
  });
1420
+ var link = program.command("link").description("Buy tickets with a Stripe Link spend approval");
1421
+ link.command("start <listingId>").description("Prepare a capped purchase and return a Link approval URL").requiredOption("--quantity <n>", "Number of tickets to buy").requiredOption("--email <email>", "Buyer email for ticket delivery").requiredOption("--max-price <total>", "Maximum total USD charge").option("--name <name>", "Optional buyer name").action(async (listingId, opts) => {
1422
+ try {
1423
+ output(await startLinkPurchase({
1424
+ listingId,
1425
+ quantity: positiveInteger(opts.quantity, "--quantity", 8),
1426
+ email: opts.email,
1427
+ name: opts.name,
1428
+ maxAmountCents: usdCents(opts.maxPrice)
1429
+ }), true);
1430
+ } catch (error) {
1431
+ const known = error instanceof LinkCheckoutError || error instanceof TypeError || error instanceof RangeError;
1432
+ output({ success: false, status: "rejected", error: {
1433
+ code: error instanceof LinkCheckoutError ? error.code : "LINK_CHECKOUT_FAILED",
1434
+ message: known && error instanceof Error ? error.message : "Link checkout could not start. Check your connection and try again."
1435
+ } }, true);
1436
+ }
1437
+ });
1438
+ link.command("complete <orderReference>").description("Complete an approved Link purchase with the saved order").action(async (orderReference) => {
1439
+ try {
1440
+ output(await completeLinkPurchase(orderReference), true);
1441
+ } catch (error) {
1442
+ const noPayment = error instanceof LinkCheckoutError && [
1443
+ "INVALID_ORDER_REFERENCE",
1444
+ "CHECKOUT_NOT_FOUND",
1445
+ "INVALID_CHECKOUT_STATE",
1446
+ "LINK_APPROVAL_MISMATCH",
1447
+ "CHALLENGE_CHANGED"
1448
+ ].includes(error.code);
1449
+ output({ success: false, status: noPayment ? "rejected" : "pending", orderReference, error: {
1450
+ code: error instanceof LinkCheckoutError ? error.code : "LINK_CHECKOUT_FAILED",
1451
+ message: error instanceof LinkCheckoutError ? error.message : "Payment outcome is unknown. Check this order before another payment."
1452
+ }, action: noPayment ? "Check the saved order and approval before another payment." : "Run the same complete command again to check this order. Do not start a new purchase yet." }, true);
1453
+ }
1454
+ });
1139
1455
  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
1456
  "--idempotency-key <uuid>",
1141
1457
  "Stable UUID v4 to reuse for retries and recovery"
@@ -1156,16 +1472,16 @@ program.command("purchase <listingId>").description("Buy tickets through MPP mac
1156
1472
  name: opts.name,
1157
1473
  idempotencyKey: opts.idempotencyKey
1158
1474
  });
1159
- const paymentEndpoint = process.env.TIXBIT_PAYMENT_URL;
1160
- new TixBitClient({ paymentEndpoint });
1475
+ const paymentEndpoint2 = process.env.TIXBIT_PAYMENT_URL;
1476
+ new TixBitClient({ paymentEndpoint: paymentEndpoint2 });
1161
1477
  publicErrorCode = "MPP_CLIENT_SETUP_FAILED";
1162
1478
  publicErrorMessage = "MPP payment client setup failed. Check the selected mppx account and local wallet configuration.";
1163
- const [{ Mppx, tempo }, { resolveAccount }] = await Promise.all([
1479
+ const [{ Mppx: Mppx2, tempo }, { resolveAccount }] = await Promise.all([
1164
1480
  import("mppx/client"),
1165
1481
  import("mppx/cli")
1166
1482
  ]);
1167
1483
  const account = await resolveAccount(opts.account);
1168
- const payments = Mppx.create({
1484
+ const payments = Mppx2.create({
1169
1485
  methods: [tempo({ account })],
1170
1486
  polyfill: false,
1171
1487
  maxPaymentRetries: 1,
@@ -1176,7 +1492,7 @@ program.command("purchase <listingId>").description("Buy tickets through MPP mac
1176
1492
  });
1177
1493
  const purchaseClient = new TixBitClient({
1178
1494
  baseUrl: process.env.TIXBIT_BASE_URL,
1179
- paymentEndpoint,
1495
+ paymentEndpoint: paymentEndpoint2,
1180
1496
  paymentFetch: (input, init) => payments.fetch(input, init)
1181
1497
  });
1182
1498
  const result = await purchaseClient.purchaseTickets(purchase);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tixbit",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Search events, browse listings, view seatmaps, and purchase TixBit tickets through MPP",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,6 +30,7 @@
30
30
  "prepublishOnly": "npm run build"
31
31
  },
32
32
  "dependencies": {
33
+ "@stripe/link-cli": "0.22.0",
33
34
  "commander": "^13.1.0",
34
35
  "mppx": "^0.8.15"
35
36
  },
@@ -40,7 +41,7 @@
40
41
  "vitest": "3.2.7"
41
42
  },
42
43
  "engines": {
43
- "node": "^20.19.0 || >=22.12.0"
44
+ "node": ">=22.12.0"
44
45
  },
45
46
  "keywords": [
46
47
  "tixbit",
@@ -12,9 +12,21 @@ npx tixbit <command>
12
12
  ```
13
13
 
14
14
  Discovery, listings, seatmaps, event URLs, link-only checkout, and machine
15
- purchase require no TixBit API key. Machine purchase requires an mppx account
16
- and an email address supplied by the buyer. Never infer the email from git, OS,
17
- or account state.
15
+ purchase require no TixBit API key. Link machine purchase needs the buyer's
16
+ Link sign-in, spend approval, and Node.js 22 or later. Tempo machine purchase needs an mppx account.
17
+ Both need an email address supplied by the buyer. Never infer the email from
18
+ git, OS, or account state.
19
+
20
+ For Stripe Link MPP payments, pair this skill with Stripe's official
21
+ `@stripe/link-cli` MCP server. Add `{"mcpServers":{"link":{"command":"npx","args":["@stripe/link-cli","--mcp"]}}}`
22
+ to a host that supports local MCP servers. The buyer must connect their Link
23
+ account. First inspect TixBit's `402` Stripe offer and exact order total, then
24
+ request an approved `shared_payment_token` spend for that network ID and total.
25
+ Use Link's `mpp pay` tool with the same purchase URL and POST body, including the
26
+ original UUID v4 idempotency key. Keep the one-time token inside Link CLI. The
27
+ published `tixbit purchase` command selects Tempo, and `tixbit buy --confirm`
28
+ cannot create a Link spend request. If the host does not expose Link's spend
29
+ request and MPP tools, use browser checkout until that host adds them.
18
30
 
19
31
  ## Commands
20
32
 
@@ -27,7 +39,13 @@ npx tixbit url <eventId>
27
39
  # Link-only browser checkout; this does not charge the buyer.
28
40
  npx tixbit checkout <listingId> --quantity 2 --json
29
41
 
30
- # Machine checkout; obtain explicit authorization before any real payment.
42
+ # Stripe Link machine checkout. Send approvalUrl to the buyer immediately.
43
+ npx tixbit link start <listingId> --quantity 1 \
44
+ --email buyer@example.com --max-price <approved-total-usd>
45
+ # After the buyer approves, use the orderReference returned by start.
46
+ npx tixbit link complete <orderReference>
47
+
48
+ # Tempo machine checkout; obtain explicit authorization before any real payment.
31
49
  npx tixbit purchase <listingId> \
32
50
  --quantity 2 \
33
51
  --email buyer@example.com \
@@ -36,13 +54,14 @@ npx tixbit purchase <listingId> \
36
54
  --json
37
55
  ```
38
56
 
39
- ## Additional commands (requires this build or a later release)
57
+ ## Additional commands (in `tixbit@0.1.2`)
40
58
 
41
59
  - `tixbit quote <eventId> --size 100`: refresh existing listings; requires live freshness.
42
60
  - `tixbit auth`: public browser sign-in and wallet setup links; not a CLI login. No command needs a developer API key.
43
61
  - `tixbit buy <listingId> --quantity 2 --max-price <total-usd> --email <buyer-email>`: quote only, no payment credential required.
62
+ - `tixbit link start` prepares one Stripe Link spend request, enforces the total cap, and returns an approval URL without charging. Present that URL immediately. `tixbit link complete` checks approval and pays with the saved order; use the same order reference for recovery. Do not print or extract the Link payment token.
44
63
  - Add `--confirm` only after approval. Without payment authorization, the result is `authorization_required` with a browser checkout URL, not a payment. Review the final total in the browser; the CLI cap is not transferred there. Optional advanced integrations can inject a user-approved `TIXBIT_LINK_TOKEN`; automatic payment still requires server cap support. Never put tokens in arguments.
45
- - Link supports 4-12 alphanumeric IDs only, not native `sl_UUID` listings. Use browser checkout for unsupported listings.
64
+ - The older `buy` web route supports 4-12 alphanumeric IDs only. The new Link MPP command accepts the listing IDs supported by the MPP server.
46
65
  - Without a signed-in user integration, seller commands return browser sign-in guidance and do not read or submit listings. Browser-to-CLI seller sessions are not supported. Do not extract browser tokens. Optional authorized integrations may supply `TIXBIT_ACCESS_TOKEN`; seller creation still requires `--confirm`, `termsAccepted: true`, and server ownership/access checks.
47
66
  - All commands emit JSON, including errors. Preserve full case-sensitive IDs.
48
67
  - Never retry uncertain Link payment automatically. Reconcile with support and Stripe Link first. Read seller listings after uncertain creation.
@@ -53,9 +72,12 @@ npx tixbit purchase <listingId> \
53
72
  1. Search and select the event.
54
73
  2. Fetch listings and the seatmap before presenting ticket options.
55
74
  3. Confirm the exact listing, quantity, and buyer email.
56
- 4. Use `checkout` for user-completed browser payment, or `purchase` only when the
57
- user explicitly authorizes machine payment.
58
- 5. Preserve the original idempotency key across retries and recovery checks.
75
+ 4. Use `checkout` for user-completed browser payment. For Stripe Link machine
76
+ payment, run `link start` with an approved cap, show `approvalUrl`, then run
77
+ `link complete` after approval. Use `purchase` for Tempo only when the user
78
+ explicitly authorizes that payment.
79
+ 5. Use the same order reference with `link complete` for recovery. For Tempo,
80
+ preserve the original idempotency key across retries and recovery checks.
59
81
  6. Treat `pending` and `manual_review_required` as non-final. Follow the returned
60
82
  `action`; never create another payment or ticket purchase for the same order.
61
83