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 +44 -3
- package/dist/cli.js +326 -10
- package/package.json +3 -2
- package/skills/tixbit/SKILL.md +31 -9
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
1387
|
+
const link2 = client.createCheckoutLink({ listingId, quantity });
|
|
1107
1388
|
if (isJson) {
|
|
1108
|
-
output({ ...
|
|
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(` ${
|
|
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
|
|
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 =
|
|
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
|
|
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": "
|
|
44
|
+
"node": ">=22.12.0"
|
|
44
45
|
},
|
|
45
46
|
"keywords": [
|
|
46
47
|
"tixbit",
|
package/skills/tixbit/SKILL.md
CHANGED
|
@@ -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.
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
#
|
|
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 (
|
|
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
|
-
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|