@lizard-build/cli 4.0.23 → 4.0.24
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 +14 -0
- package/dist/commands/billing.d.ts +68 -0
- package/dist/commands/billing.js +435 -0
- package/dist/commands/billing.js.map +1 -0
- package/dist/commands/metrics.d.ts +13 -0
- package/dist/commands/metrics.js +33 -2
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/up.js +10 -3
- package/dist/commands/up.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +25 -0
- package/dist/lib/api.js +81 -26
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/skills-data.generated.js +1 -1
- package/dist/lib/skills-data.generated.js.map +1 -1
- package/dist/lib/updater.d.ts +1 -1
- package/dist/lib/updater.js +1 -1
- package/package.json +1 -4
- package/skill-data/core/SKILL.md +37 -29
- package/src/commands/billing.ts +571 -0
- package/src/commands/metrics.ts +49 -2
- package/src/commands/up.ts +9 -8
- package/src/index.ts +2 -2
- package/src/lib/api.ts +81 -26
- package/src/lib/updater.ts +1 -1
- package/test/unit/billing.test.ts +347 -0
- package/test/unit/metrics.test.ts +72 -0
- package/test/unit/payment-required.test.ts +110 -0
- package/dist/commands/credits.d.ts +0 -2
- package/dist/commands/credits.js +0 -331
- package/dist/commands/credits.js.map +0 -1
- package/dist/lib/x402.d.ts +0 -23
- package/dist/lib/x402.js +0 -148
- package/dist/lib/x402.js.map +0 -1
- package/src/commands/credits.ts +0 -379
- package/src/lib/x402.ts +0 -131
- package/test/unit/x402.test.ts +0 -61
package/dist/lib/x402.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"x402.js","sourceRoot":"","sources":["../../src/lib/x402.ts"],"names":[],"mappings":"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"}
|
package/src/commands/credits.ts
DELETED
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import { dollarsToCents, getX402Quote, payX402, getX402Status } from "../lib/x402.js";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import open from "open";
|
|
4
|
-
import * as p from "@clack/prompts";
|
|
5
|
-
import { Command } from "commander";
|
|
6
|
-
import { api, withQuery, APIError } from "../lib/api.js";
|
|
7
|
-
import { success, isJSONMode, printJSON, table, isTTY, timeAgo } from "../lib/format.js";
|
|
8
|
-
|
|
9
|
-
interface BalanceView {
|
|
10
|
-
plan: string;
|
|
11
|
-
status: "active" | "grace" | "frozen";
|
|
12
|
-
balanceCents: number;
|
|
13
|
-
overdraftLimitCents: number | null;
|
|
14
|
-
availableCents: number | null;
|
|
15
|
-
expiringCents: number;
|
|
16
|
-
expiringAt: number | null;
|
|
17
|
-
expiringCredits: { amountCents: number; expiresAt: number }[];
|
|
18
|
-
hourlyRateCents: number;
|
|
19
|
-
runwayHours: number | null;
|
|
20
|
-
graceSince: number | null;
|
|
21
|
-
graceHours: number;
|
|
22
|
-
neverFreeze: boolean;
|
|
23
|
-
invoicedMonthly: boolean;
|
|
24
|
-
email: string | null;
|
|
25
|
-
autoTopup: {
|
|
26
|
-
enabled: boolean; thresholdCents: number; amountCents: number;
|
|
27
|
-
paymentMethodIds: string[]; disabledReason: string | null;
|
|
28
|
-
lastAttemptAt: number | null; lastSuccessAt: number | null; consecutiveFailures: number;
|
|
29
|
-
};
|
|
30
|
-
pendingPromo: { code: string; creditCents: number; expiresAt: number | null } | null;
|
|
31
|
-
topupUrl: string;
|
|
32
|
-
purchase: { minCents: number; maxCents: number; feeBps: number; feeMinCents: number; feeFixedCents: number; feeInternationalBps: number; cryptoEnabled: boolean };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// `lizard credits` — account-scoped prepaid balance (LIZARD-177). Unlike
|
|
36
|
-
// most commands this never takes --project/--service: billing belongs to
|
|
37
|
-
// the user, not a workspace. See docs/credits-api.md on the platform repo
|
|
38
|
-
// for the contract this is built against.
|
|
39
|
-
|
|
40
|
-
function fmtCents(cents: number): string {
|
|
41
|
-
const sign = cents < 0 ? "-" : "";
|
|
42
|
-
return `${sign}$${(Math.abs(cents) / 100).toFixed(2)}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Accepts "10", "10.5", "$10.50" -> cents. Throws on anything else. */
|
|
46
|
-
function parseDollarsToCents(input: string): number {
|
|
47
|
-
const cleaned = input.trim().replace(/^\$/, "");
|
|
48
|
-
const value = Number(cleaned);
|
|
49
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
50
|
-
throw new Error(`Invalid amount: "${input}". Use a positive dollar amount, e.g. 10 or 10.50.`);
|
|
51
|
-
}
|
|
52
|
-
return Math.round(value * 100);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function statusLabel(status: string): string {
|
|
56
|
-
switch (status) {
|
|
57
|
-
case "active": return chalk.green(status);
|
|
58
|
-
case "grace": return chalk.yellow(status);
|
|
59
|
-
case "frozen": return chalk.red(status);
|
|
60
|
-
default: return status;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function printBalance(): Promise<BalanceView> {
|
|
65
|
-
const view = await api.get<BalanceView>("/api/billing/balance");
|
|
66
|
-
if (isJSONMode()) {
|
|
67
|
-
printJSON(view);
|
|
68
|
-
return view;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
console.log(`${chalk.bold(fmtCents(view.balanceCents))} ${statusLabel(view.status)} ${chalk.dim(`(${view.plan})`)}`);
|
|
72
|
-
|
|
73
|
-
if (view.neverFreeze) {
|
|
74
|
-
console.log(chalk.dim("Exempt from billing freeze — balance can go negative with no enforcement."));
|
|
75
|
-
} else {
|
|
76
|
-
if (view.overdraftLimitCents != null) {
|
|
77
|
-
console.log(chalk.dim(`Overdraft limit: `) + fmtCents(-view.overdraftLimitCents));
|
|
78
|
-
}
|
|
79
|
-
if (view.availableCents != null) {
|
|
80
|
-
console.log(chalk.dim(`Available before freeze: `) + fmtCents(view.availableCents));
|
|
81
|
-
}
|
|
82
|
-
if (view.graceSince) {
|
|
83
|
-
const deadline = view.graceSince + view.graceHours * 3_600_000;
|
|
84
|
-
const hoursLeft = Math.max(0, Math.round((deadline - Date.now()) / 3_600_000));
|
|
85
|
-
console.log(chalk.yellow(`In grace since ${timeAgo(view.graceSince)} — freezes in ~${hoursLeft}h if unpaid.`));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (view.expiringCents > 0 && view.expiringAt) {
|
|
90
|
-
console.log(chalk.dim(`Expiring: `) + `${fmtCents(view.expiringCents)} by ${new Date(view.expiringAt).toLocaleDateString()}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (view.hourlyRateCents > 0) {
|
|
94
|
-
const rate = chalk.dim(`(~${fmtCents(view.hourlyRateCents)}/hr)`);
|
|
95
|
-
if (view.runwayHours != null) {
|
|
96
|
-
const days = (view.runwayHours / 24).toFixed(1);
|
|
97
|
-
console.log(chalk.dim(`Runway: `) + `~${view.runwayHours}h (${days}d) at current usage ${rate}`);
|
|
98
|
-
} else {
|
|
99
|
-
console.log(chalk.dim(`Current usage rate: `) + rate);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
console.log(
|
|
104
|
-
chalk.dim("Auto top-up: ") +
|
|
105
|
-
(view.autoTopup.enabled
|
|
106
|
-
? `on — tops up ${fmtCents(view.autoTopup.amountCents)} when balance drops below ${fmtCents(view.autoTopup.thresholdCents)}`
|
|
107
|
-
: "off"),
|
|
108
|
-
);
|
|
109
|
-
if (view.autoTopup.disabledReason) {
|
|
110
|
-
console.log(chalk.yellow(` disabled: ${view.autoTopup.disabledReason}`));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (view.pendingPromo) {
|
|
114
|
-
console.log(chalk.dim("Pending promo: ") + `${view.pendingPromo.code} (${fmtCents(view.pendingPromo.creditCents)})`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return view;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
interface Transaction {
|
|
121
|
-
id: string; kind: string; amountCents: number; balanceAfterCents: number | null;
|
|
122
|
-
description: string | null; createdAt: number; externalRef: string | null;
|
|
123
|
-
receiptUrl: string | null; invoiceUrl: string | null; expiresAt: number | null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
interface PaymentMethod {
|
|
127
|
-
id: string; brand: string; last4: string; expMonth: number; expYear: number; isDefault?: boolean;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function registerCredits(program: Command) {
|
|
131
|
-
const cmd = program
|
|
132
|
-
.command("credits")
|
|
133
|
-
.alias("billing")
|
|
134
|
-
.description("Account balance, usage, and top-ups (prepaid credits)")
|
|
135
|
-
.action(async () => {
|
|
136
|
-
await printBalance();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
cmd
|
|
140
|
-
.command("balance")
|
|
141
|
-
.description("Show current balance, status, and runway")
|
|
142
|
-
.action(async () => {
|
|
143
|
-
await printBalance();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
cmd
|
|
147
|
-
.command("transactions")
|
|
148
|
-
.alias("ledger")
|
|
149
|
-
.description("List recent balance transactions")
|
|
150
|
-
.option("-l, --limit <n>", "Max results (1-100)", "20")
|
|
151
|
-
.option("-c, --cursor <cursor>", "Pagination cursor from a previous call")
|
|
152
|
-
.option("-u, --usage", "Include daily usage-deduction rows")
|
|
153
|
-
.action(async (opts) => {
|
|
154
|
-
const page = await api.get<{ items: Transaction[]; nextCursor: string | null }>(
|
|
155
|
-
withQuery("/api/billing/transactions", {
|
|
156
|
-
limit: opts.limit,
|
|
157
|
-
cursor: opts.cursor,
|
|
158
|
-
includeUsage: opts.usage ? "1" : undefined,
|
|
159
|
-
}),
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
if (isJSONMode()) {
|
|
163
|
-
printJSON(page);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (page.items.length === 0) {
|
|
168
|
-
console.log("No transactions.");
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
table(
|
|
173
|
-
["When", "Kind", "Amount", "Balance after", "Description"],
|
|
174
|
-
page.items.map((t) => [
|
|
175
|
-
timeAgo(t.createdAt),
|
|
176
|
-
t.kind,
|
|
177
|
-
t.amountCents >= 0 ? chalk.green(fmtCents(t.amountCents)) : chalk.red(fmtCents(t.amountCents)),
|
|
178
|
-
t.balanceAfterCents != null ? fmtCents(t.balanceAfterCents) : "—",
|
|
179
|
-
t.description ?? "",
|
|
180
|
-
]),
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
if (page.nextCursor) {
|
|
184
|
-
console.log(chalk.dim(`\nMore results: lizard credits transactions --cursor ${page.nextCursor}`));
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// ── Payment methods ───────────────────────────────────────────────
|
|
189
|
-
const pm = cmd.command("payment-methods").alias("pm").description("Manage saved payment methods");
|
|
190
|
-
|
|
191
|
-
pm.command("list")
|
|
192
|
-
.description("List saved payment methods")
|
|
193
|
-
.action(async () => {
|
|
194
|
-
const { items } = await api.get<{ items: PaymentMethod[] }>("/api/billing/payment-methods");
|
|
195
|
-
if (isJSONMode()) {
|
|
196
|
-
printJSON(items);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (items.length === 0) {
|
|
200
|
-
console.log("No saved payment methods. Add one with `lizard credits payment-methods add`.");
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
table(
|
|
204
|
-
["ID", "Card", "Expires", "Default"],
|
|
205
|
-
items.map((m) => [m.id, `${m.brand} •••• ${m.last4}`, `${m.expMonth}/${m.expYear}`, m.isDefault ? "yes" : ""]),
|
|
206
|
-
);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
pm.command("add")
|
|
210
|
-
.description("Save a new card via a Stripe Checkout link")
|
|
211
|
-
.option("--return-url <url>", "Where to redirect after saving the card")
|
|
212
|
-
.option("--no-open", "Print the link instead of opening a browser")
|
|
213
|
-
.action(async (opts) => {
|
|
214
|
-
const { url } = await api.post<{ url: string }>("/api/billing/payment-methods/setup", {
|
|
215
|
-
returnUrl: opts.returnUrl,
|
|
216
|
-
});
|
|
217
|
-
if (isJSONMode()) {
|
|
218
|
-
printJSON({ url });
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
console.log(`Open this link to save a card:\n${chalk.cyan(url)}`);
|
|
222
|
-
if (opts.open !== false && isTTY()) await open(url).catch(() => {});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
pm.command("remove <id>")
|
|
226
|
-
.alias("rm")
|
|
227
|
-
.description("Remove a saved payment method")
|
|
228
|
-
.option("-y, --yes", "Skip confirmation")
|
|
229
|
-
.action(async (id: string, opts) => {
|
|
230
|
-
if (!opts.yes) {
|
|
231
|
-
if (!isTTY()) throw new Error("Use -y to confirm in non-interactive mode");
|
|
232
|
-
const confirm = await p.confirm({ message: `Remove payment method ${chalk.bold(id)}?` });
|
|
233
|
-
if (p.isCancel(confirm) || !confirm) process.exit(5);
|
|
234
|
-
}
|
|
235
|
-
const out = await api.delete<{ ok: boolean }>(`/api/billing/payment-methods/${encodeURIComponent(id)}`);
|
|
236
|
-
if (isJSONMode()) {
|
|
237
|
-
printJSON(out);
|
|
238
|
-
} else {
|
|
239
|
-
success("Payment method removed");
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// ── Top up ────────────────────────────────────────────────────────
|
|
244
|
-
cmd
|
|
245
|
-
.command("topup <amount>")
|
|
246
|
-
.alias("purchase")
|
|
247
|
-
.description("Buy credits (amount in dollars, e.g. `lizard credits topup 20`)")
|
|
248
|
-
.option("--method <method>", "card, crypto, or x402", "card")
|
|
249
|
-
.option("--quote", "Show an x402 quote without paying")
|
|
250
|
-
.option("--max-total <amount>", "Maximum x402 payment including fees, in dollars")
|
|
251
|
-
.option("--request-id <uuid>", "Reuse this ID to resume an x402 purchase")
|
|
252
|
-
.option("-y, --yes", "Confirm the x402 payment within --max-total")
|
|
253
|
-
.option("--return-url <url>", "Where to redirect after payment")
|
|
254
|
-
.option("--no-open", "Print the checkout link instead of opening a browser")
|
|
255
|
-
.action(async (amount: string, opts) => {
|
|
256
|
-
if (opts.method === "x402") {
|
|
257
|
-
const creditCents = dollarsToCents(amount);
|
|
258
|
-
const quote = await getX402Quote(creditCents);
|
|
259
|
-
if (opts.quote) {
|
|
260
|
-
if (isJSONMode()) printJSON(quote);
|
|
261
|
-
else console.log(`Credits ${fmtCents(quote.creditCents)}, fee ${fmtCents(quote.feeCents)}, total ${fmtCents(quote.totalCents)} USDC on Base`);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (!opts.maxTotal) throw new Error("Use --max-total to set a payment limit including fees.");
|
|
265
|
-
const limit = dollarsToCents(opts.maxTotal);
|
|
266
|
-
if (quote.totalCents > limit) throw new Error("Payment exceeds --max-total.");
|
|
267
|
-
if (!opts.yes) {
|
|
268
|
-
if (!isTTY()) throw new Error("Use --yes and --max-total to authorize an x402 payment.");
|
|
269
|
-
const confirmed = await p.confirm({ message: `Pay ${fmtCents(quote.totalCents)} USDC for ${fmtCents(quote.creditCents)} credits?` });
|
|
270
|
-
if (p.isCancel(confirmed) || !confirmed) throw new Error("Payment cancelled.");
|
|
271
|
-
}
|
|
272
|
-
const result = await payX402(creditCents, limit, quote, opts.requestId);
|
|
273
|
-
if (isJSONMode()) printJSON(result);
|
|
274
|
-
else console.log(`Payment ${result.status}. Attempt: ${result.attemptId}. Request: ${result.requestId}`);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (opts.quote || opts.maxTotal || opts.requestId) throw new Error("--quote, --max-total, and --request-id require --method x402.");
|
|
278
|
-
const creditCents = parseDollarsToCents(amount);
|
|
279
|
-
if (opts.method !== "card" && opts.method !== "crypto") {
|
|
280
|
-
throw new Error(`--method must be "card", "crypto", or "x402", got "${opts.method}"`);
|
|
281
|
-
}
|
|
282
|
-
let out: { url?: string; sessionId?: string };
|
|
283
|
-
try {
|
|
284
|
-
out = await api.post("/api/billing/purchase", {
|
|
285
|
-
creditCents,
|
|
286
|
-
paymentMethod: opts.method,
|
|
287
|
-
returnUrl: opts.returnUrl,
|
|
288
|
-
});
|
|
289
|
-
} catch (e) {
|
|
290
|
-
if (e instanceof APIError && e.status === 409 && e.message === "CREDITS_NOT_ENABLED") {
|
|
291
|
-
throw new Error("Credits purchases aren't available yet on this account.");
|
|
292
|
-
}
|
|
293
|
-
throw e;
|
|
294
|
-
}
|
|
295
|
-
if (isJSONMode()) {
|
|
296
|
-
printJSON(out);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
if (!out.url) throw new Error("No checkout URL returned.");
|
|
300
|
-
console.log(`Complete payment (${fmtCents(creditCents)}) at:\n${chalk.cyan(out.url)}`);
|
|
301
|
-
if (opts.open !== false && isTTY()) await open(out.url).catch(() => {});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
cmd.command("payment-status <attempt-id>")
|
|
305
|
-
.description("Check or recover an x402 credit purchase")
|
|
306
|
-
.action(async (attemptId: string) => {
|
|
307
|
-
const result = await getX402Status(attemptId);
|
|
308
|
-
if (isJSONMode()) printJSON(result);
|
|
309
|
-
else console.log(`Payment ${result.status}. Attempt: ${result.attemptId}`);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// ── Auto top-up ───────────────────────────────────────────────────
|
|
313
|
-
const auto = cmd.command("auto-topup").description("Show or configure automatic top-up");
|
|
314
|
-
|
|
315
|
-
auto.action(async () => {
|
|
316
|
-
const settings = await api.get("/api/billing/auto-topup");
|
|
317
|
-
if (isJSONMode()) { printJSON(settings); return; }
|
|
318
|
-
console.log(JSON.stringify(settings, null, 2));
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
auto
|
|
322
|
-
.command("set")
|
|
323
|
-
.description("Configure automatic top-up")
|
|
324
|
-
.requiredOption("--payment-method <id...>", "Payment method ID(s) to charge, in priority order")
|
|
325
|
-
.option("--threshold <amount>", "Top up when balance drops below this (dollars)", "5")
|
|
326
|
-
.option("--amount <amount>", "How much to add per top-up (dollars)", "20")
|
|
327
|
-
.option("--disable", "Turn auto top-up off instead of configuring it")
|
|
328
|
-
.action(async (opts) => {
|
|
329
|
-
const body = {
|
|
330
|
-
enabled: !opts.disable,
|
|
331
|
-
thresholdCents: parseDollarsToCents(opts.threshold),
|
|
332
|
-
amountCents: parseDollarsToCents(opts.amount),
|
|
333
|
-
paymentMethodIds: opts.paymentMethod,
|
|
334
|
-
};
|
|
335
|
-
const out = await api.put("/api/billing/auto-topup", body);
|
|
336
|
-
if (isJSONMode()) { printJSON(out); return; }
|
|
337
|
-
success(opts.disable ? "Auto top-up disabled" : "Auto top-up configured");
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
auto
|
|
341
|
-
.command("run")
|
|
342
|
-
.description("Manually trigger an auto top-up check now")
|
|
343
|
-
.action(async () => {
|
|
344
|
-
try {
|
|
345
|
-
const out = await api.post<{ ok: boolean; creditedCents?: number; balanceCents?: number }>(
|
|
346
|
-
"/api/billing/auto-topup/run",
|
|
347
|
-
{},
|
|
348
|
-
);
|
|
349
|
-
if (isJSONMode()) { printJSON(out); return; }
|
|
350
|
-
if (out.ok) {
|
|
351
|
-
success(`Topped up ${fmtCents(out.creditedCents ?? 0)} — new balance ${fmtCents(out.balanceCents ?? 0)}`);
|
|
352
|
-
}
|
|
353
|
-
} catch (e) {
|
|
354
|
-
if (e instanceof APIError && e.status === 429) {
|
|
355
|
-
throw new Error(`Auto top-up rate-limited: ${(e.body as any)?.reason ?? e.message}`);
|
|
356
|
-
}
|
|
357
|
-
throw e;
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
// ── Promo codes ───────────────────────────────────────────────────
|
|
362
|
-
cmd
|
|
363
|
-
.command("promo <code>")
|
|
364
|
-
.description("Redeem a promo code")
|
|
365
|
-
.action(async (code: string) => {
|
|
366
|
-
try {
|
|
367
|
-
const out = await api.post<{ status: string; creditCents: number; expiresAt: number | null; balanceCents: number }>(
|
|
368
|
-
"/api/billing/promo/redeem",
|
|
369
|
-
{ code },
|
|
370
|
-
);
|
|
371
|
-
if (isJSONMode()) { printJSON(out); return; }
|
|
372
|
-
success(`Redeemed ${fmtCents(out.creditCents)} — new balance ${fmtCents(out.balanceCents)}`);
|
|
373
|
-
} catch (e) {
|
|
374
|
-
if (e instanceof APIError) throw new Error((e.body as any)?.message ?? e.message);
|
|
375
|
-
throw e;
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
|
package/src/lib/x402.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import type { PaymentRequired } from '@x402/core/types';
|
|
6
|
-
import { APIError, getBaseURL, getRequestToken } from './api.js';
|
|
7
|
-
|
|
8
|
-
const NETWORK = 'eip155:8453';
|
|
9
|
-
const ASSET = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
10
|
-
const ENDPOINT = '/api/billing/purchase/x402';
|
|
11
|
-
export interface X402Quote { creditCents: number; feeCents: number; totalCents: number; network: string; asset: string; payTo: string; }
|
|
12
|
-
export interface X402Result extends X402Quote { requestId: string; attemptId: string; status: string; transaction?: string | null; paymentIntentId?: string | null; }
|
|
13
|
-
class PaymentInputError extends Error {}
|
|
14
|
-
interface Journal { requestId: string; creditCents: number; maxTotalCents: number; origin: string; signature?: string; result?: X402Result; }
|
|
15
|
-
export function dollarsToCents(value: string): number {
|
|
16
|
-
if (!/^\d+(\.\d{1,2})?$/.test(value)) throw new Error('Use a positive dollar amount with at most two decimal places.');
|
|
17
|
-
const [whole, fraction = ''] = value.split('.');
|
|
18
|
-
const cents = Number(whole) * 100 + Number(fraction.padEnd(2, '0'));
|
|
19
|
-
if (!Number.isSafeInteger(cents) || cents <= 0) throw new Error('Invalid dollar amount.');
|
|
20
|
-
return cents;
|
|
21
|
-
}
|
|
22
|
-
export function paymentOrigin(): string {
|
|
23
|
-
const url = new URL(getBaseURL());
|
|
24
|
-
const trusted = process.env.LIZARD_X402_TRUSTED_ORIGIN || 'https://lizard.build';
|
|
25
|
-
if (url.protocol !== 'https:' || url.origin !== trusted || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
|
|
26
|
-
throw new Error('x402 requires a trusted HTTPS API origin. Set LIZARD_X402_TRUSTED_ORIGIN explicitly for another server.');
|
|
27
|
-
}
|
|
28
|
-
return url.origin;
|
|
29
|
-
}
|
|
30
|
-
async function request(method: string, route: string, body?: unknown, signature?: string): Promise<Response> {
|
|
31
|
-
const token = getRequestToken();
|
|
32
|
-
if (!token) throw new APIError(401, 'Sign in before buying credits.');
|
|
33
|
-
const headers: Record<string, string> = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
|
|
34
|
-
if (signature) headers['PAYMENT-SIGNATURE'] = signature;
|
|
35
|
-
return fetch(paymentOrigin() + route, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), redirect: 'error', signal: AbortSignal.timeout(45_000) });
|
|
36
|
-
}
|
|
37
|
-
async function json<T>(res: Response): Promise<T> {
|
|
38
|
-
const value = await res.json() as T & { error?: string; message?: string };
|
|
39
|
-
if (!res.ok) throw new APIError(res.status, value.error || value.message || 'Payment request failed.', value.error, value);
|
|
40
|
-
return value;
|
|
41
|
-
}
|
|
42
|
-
export function validateQuote(q: X402Quote, creditCents: number, maxTotalCents = Number.MAX_SAFE_INTEGER) {
|
|
43
|
-
if (!Number.isSafeInteger(q.creditCents) || !Number.isSafeInteger(q.feeCents) || !Number.isSafeInteger(q.totalCents) ||
|
|
44
|
-
q.creditCents !== creditCents || q.feeCents < 0 || q.totalCents !== q.creditCents + q.feeCents || q.totalCents > maxTotalCents ||
|
|
45
|
-
q.network !== NETWORK || q.asset.toLowerCase() !== ASSET.toLowerCase() || !/^0x[0-9a-fA-F]{40}$/.test(q.payTo)) throw new Error('Payment quote is invalid or exceeds --max-total.');
|
|
46
|
-
}
|
|
47
|
-
export async function getX402Quote(creditCents: number): Promise<X402Quote> {
|
|
48
|
-
const q = await json<X402Quote>(await request('POST', `${ENDPOINT}/quote`, { creditCents }));
|
|
49
|
-
validateQuote(q, creditCents);
|
|
50
|
-
return q;
|
|
51
|
-
}
|
|
52
|
-
export function validateChallenge(value: PaymentRequired, q: X402Quote, origin: string) {
|
|
53
|
-
const r = value.accepts?.[0];
|
|
54
|
-
if (value.x402Version !== 2 || value.accepts?.length !== 1 || value.resource?.url !== origin + ENDPOINT || !r ||
|
|
55
|
-
r.scheme !== 'exact' || r.network !== NETWORK || r.asset.toLowerCase() !== ASSET.toLowerCase() ||
|
|
56
|
-
r.payTo.toLowerCase() !== q.payTo.toLowerCase() || r.amount !== String(BigInt(q.totalCents) * 10_000n) ||
|
|
57
|
-
r.maxTimeoutSeconds <= 0 || r.maxTimeoutSeconds > 300 ||
|
|
58
|
-
(value.extensions && Object.keys(value.extensions).length > 0) ||
|
|
59
|
-
(r.extra?.assetTransferMethod !== undefined && r.extra.assetTransferMethod !== 'eip3009') ||
|
|
60
|
-
Object.keys(r.extra || {}).some(key => !['name', 'version', 'assetTransferMethod'].includes(key)) || r.extra?.name !== 'USD Coin' || r.extra?.version !== '2') throw new Error('Untrusted x402 payment challenge.');
|
|
61
|
-
}
|
|
62
|
-
function journalPath(creditCents: number, requestId?: string) {
|
|
63
|
-
if (requestId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(requestId)) throw new Error('--request-id must be a UUID.');
|
|
64
|
-
const account = createHash('sha256').update(paymentOrigin() + '\0' + getRequestToken()).digest('hex');
|
|
65
|
-
const dir = path.join(process.env.LIZARD_HOME || os.homedir(), '.lizard', 'payments', account);
|
|
66
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
67
|
-
const pending = path.join(dir, `pending-${creditCents}.json`);
|
|
68
|
-
if (requestId && fs.existsSync(pending)) {
|
|
69
|
-
const previous = JSON.parse(fs.readFileSync(pending, 'utf8')) as Journal;
|
|
70
|
-
if (previous.requestId === requestId) return pending;
|
|
71
|
-
}
|
|
72
|
-
return path.join(dir, requestId || `pending-${creditCents}.json`);
|
|
73
|
-
}
|
|
74
|
-
function persist(file: string, state: Journal) {
|
|
75
|
-
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
76
|
-
fs.writeFileSync(temporary, JSON.stringify(state), { mode: 0o600, flag: 'wx' });
|
|
77
|
-
fs.renameSync(temporary, file);
|
|
78
|
-
}
|
|
79
|
-
export async function payX402(creditCents: number, maxTotalCents: number, expected: X402Quote, requestId?: string): Promise<X402Result> {
|
|
80
|
-
validateQuote(expected, creditCents, maxTotalCents);
|
|
81
|
-
const origin = paymentOrigin();
|
|
82
|
-
const file = journalPath(creditCents, requestId);
|
|
83
|
-
let state: Journal = { requestId: requestId || randomUUID(), creditCents, maxTotalCents, origin };
|
|
84
|
-
try { fs.writeFileSync(file, JSON.stringify(state), { flag: 'wx', mode: 0o600 }); }
|
|
85
|
-
catch (e) {
|
|
86
|
-
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;
|
|
87
|
-
state = JSON.parse(fs.readFileSync(file, 'utf8')) as Journal;
|
|
88
|
-
if (state.origin !== origin || state.creditCents !== creditCents || state.maxTotalCents > maxTotalCents) throw new Error('Saved payment differs. Resume with the original amount and limit.');
|
|
89
|
-
if (state.result?.status === 'paid') {
|
|
90
|
-
if (requestId) return state.result;
|
|
91
|
-
throw new Error(`Previous payment ${state.requestId} is paid. Use a new --request-id UUID for another purchase of this amount.`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
let response = await request('POST', ENDPOINT, { creditCents, requestId: state.requestId }, state.signature);
|
|
96
|
-
if (response.status === 402 && !state.signature) {
|
|
97
|
-
const header = response.headers.get('PAYMENT-REQUIRED');
|
|
98
|
-
if (!header || header.length > 16_384) throw new Error('Server returned 402 without a valid x402 challenge.');
|
|
99
|
-
const challenge = JSON.parse(Buffer.from(header, 'base64').toString('utf8')) as PaymentRequired;
|
|
100
|
-
const body = await response.json() as X402Result;
|
|
101
|
-
validateQuote(body, creditCents, Math.min(state.maxTotalCents, maxTotalCents));
|
|
102
|
-
if (body.payTo.toLowerCase() !== expected.payTo.toLowerCase() || body.totalCents !== expected.totalCents) throw new Error('Quote changed. Request a fresh quote before paying.');
|
|
103
|
-
validateChallenge(challenge, body, origin);
|
|
104
|
-
const key = process.env.LIZARD_X402_PRIVATE_KEY;
|
|
105
|
-
if (!key || !/^0x[0-9a-fA-F]{64}$/.test(key)) throw new PaymentInputError('Set LIZARD_X402_PRIVATE_KEY to a funded Base wallet key through your secret manager.');
|
|
106
|
-
const [{ x402Client }, { ExactEvmScheme }, { privateKeyToAccount }] = await Promise.all([
|
|
107
|
-
import('@x402/core/client'), import('@x402/evm/exact/client'), import('viem/accounts'),
|
|
108
|
-
]);
|
|
109
|
-
const signer = privateKeyToAccount(key as `0x${string}`);
|
|
110
|
-
const client = new x402Client().register(NETWORK, new ExactEvmScheme(signer))
|
|
111
|
-
.setSpendControls({ maxAmountPerPayment: `$${(Math.min(state.maxTotalCents, maxTotalCents) / 100).toFixed(2)}` });
|
|
112
|
-
const payload = await client.createPaymentPayload(challenge);
|
|
113
|
-
state.signature = Buffer.from(JSON.stringify(payload)).toString('base64');
|
|
114
|
-
state.result = body;
|
|
115
|
-
persist(file, state); // Persist the authorization before sending it anywhere.
|
|
116
|
-
response = await request('POST', ENDPOINT, { creditCents, requestId: state.requestId }, state.signature);
|
|
117
|
-
}
|
|
118
|
-
const result = await json<X402Result>(response);
|
|
119
|
-
state.result = result;
|
|
120
|
-
persist(file, state);
|
|
121
|
-
return result;
|
|
122
|
-
} catch (e) {
|
|
123
|
-
if (e instanceof APIError && e.code === 'QUOTE_EXPIRED') throw new Error('Quote expired before payment was accepted. Use a new --request-id UUID.');
|
|
124
|
-
const message = e instanceof APIError || e instanceof PaymentInputError ? e.message : 'Payment did not finish. No new payment was started.';
|
|
125
|
-
throw new Error(`${message} Resume with --request-id ${state.requestId}. Saved state: ${file}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
export async function getX402Status(attemptId: string): Promise<X402Result> {
|
|
129
|
-
if (!/^[0-9a-f-]{36}$/i.test(attemptId)) throw new Error('Payment attempt ID must be a UUID.');
|
|
130
|
-
return json<X402Result>(await request('GET', `${ENDPOINT}/${attemptId}`));
|
|
131
|
-
}
|
package/test/unit/x402.test.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { dollarsToCents, validateQuote, validateChallenge, paymentOrigin, payX402, getX402Quote } from '../../src/lib/x402.js';
|
|
6
|
-
import { setBaseURL, setAccessToken } from '../../src/lib/api.js';
|
|
7
|
-
const payTo='0x'+'22'.repeat(20);
|
|
8
|
-
const quote={creditCents:2000,feeCents:31,totalCents:2031,network:'eip155:8453',asset:'0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',payTo};
|
|
9
|
-
const challenge={x402Version:2,resource:{url:'https://lizard.build/api/billing/purchase/x402',mimeType:'application/json'},accepts:[{scheme:'exact',network:'eip155:8453' as const,asset:quote.asset,amount:'20310000',payTo,maxTimeoutSeconds:300,extra:{name:'USD Coin',version:'2'}}]};
|
|
10
|
-
const fetchMock=vi.fn();
|
|
11
|
-
let home: string;
|
|
12
|
-
function unpaid() { return new Response(JSON.stringify({...quote,attemptId:'a0000000-0000-4000-8000-000000000001',status:'quoted',...challenge}), {status:402,headers:{'PAYMENT-REQUIRED':Buffer.from(JSON.stringify(challenge)).toString('base64')}}); }
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
fetchMock.mockReset(); vi.stubGlobal('fetch',fetchMock); setBaseURL('https://lizard.build'); setAccessToken('test-token');
|
|
15
|
-
home=fs.mkdtempSync(path.join(os.tmpdir(),'lizard-x402-')); vi.stubEnv('LIZARD_HOME',home);
|
|
16
|
-
vi.stubEnv('LIZARD_X402_TRUSTED_ORIGIN',''); vi.stubEnv('LIZARD_X402_PRIVATE_KEY','0x'+'11'.repeat(32));
|
|
17
|
-
});
|
|
18
|
-
afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); });
|
|
19
|
-
describe('x402 client', () => {
|
|
20
|
-
it('parses money without rounding fractional cents', () => {
|
|
21
|
-
expect(dollarsToCents('20.31')).toBe(2031);
|
|
22
|
-
for(const invalid of ['1.001','NaN','1e3','-1','0','Infinity']) expect(()=>dollarsToCents(invalid)).toThrow();
|
|
23
|
-
});
|
|
24
|
-
it('blocks untrusted hosts and malformed quotes', () => {
|
|
25
|
-
setBaseURL('http://lizard.build'); expect(paymentOrigin).toThrow();
|
|
26
|
-
setBaseURL('https://evil.example'); expect(paymentOrigin).toThrow();
|
|
27
|
-
expect(()=>validateQuote({...quote,totalCents:2200},2000,2100)).toThrow();
|
|
28
|
-
expect(()=>validateQuote({...quote,asset:payTo},2000,2100)).toThrow();
|
|
29
|
-
});
|
|
30
|
-
it('rejects a changed recipient, token, network, amount, domain or payment scheme', () => {
|
|
31
|
-
for(const patch of [{payTo:'0x'+'33'.repeat(20)},{asset:payTo},{network:'eip155:1'},{amount:'99999999'},{extra:{name:'USDC',version:'2'}},{scheme:'upto'},{extra:{name:'USD Coin',version:'2',assetTransferMethod:'permit2'}}]) {
|
|
32
|
-
expect(()=>validateChallenge({...challenge,accepts:[{...challenge.accepts[0],...patch}]} as never,quote,'https://lizard.build')).toThrow();
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
it('quotes without accessing a wallet or signing', async () => {
|
|
36
|
-
vi.stubEnv('LIZARD_X402_PRIVATE_KEY',''); fetchMock.mockResolvedValue(new Response(JSON.stringify(quote)));
|
|
37
|
-
expect(await getX402Quote(2000)).toEqual(quote); expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
38
|
-
});
|
|
39
|
-
it('never signs a plain INSUFFICIENT_CREDITS 402', async () => {
|
|
40
|
-
fetchMock.mockResolvedValue(new Response(JSON.stringify({error:'INSUFFICIENT_CREDITS'}),{status:402}));
|
|
41
|
-
await expect(payX402(2000,2100,quote)).rejects.toThrow('Resume with --request-id'); expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
42
|
-
});
|
|
43
|
-
it('persists the real SDK authorization and reuses it after a lost response', async () => {
|
|
44
|
-
fetchMock.mockResolvedValueOnce(unpaid()).mockRejectedValueOnce(new Error('network timeout'));
|
|
45
|
-
await expect(payX402(2000,2100,quote)).rejects.toThrow('Resume with --request-id');
|
|
46
|
-
const sent=fetchMock.mock.calls[1][1]; const first=JSON.parse(sent.body);
|
|
47
|
-
expect(sent.redirect).toBe('error'); expect(sent.headers.Authorization).toBe('Bearer test-token');
|
|
48
|
-
const payload=JSON.parse(Buffer.from(sent.headers['PAYMENT-SIGNATURE'],'base64').toString());
|
|
49
|
-
expect(payload.payload.authorization).toMatchObject({to:payTo,value:'20310000'});
|
|
50
|
-
const paid={...quote,requestId:first.requestId,attemptId:'a0000000-0000-4000-8000-000000000001',status:'paid'};
|
|
51
|
-
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify(paid)));
|
|
52
|
-
expect(await payX402(2000,2100,quote,first.requestId)).toEqual(paid);
|
|
53
|
-
expect(fetchMock.mock.calls[2][1].headers['PAYMENT-SIGNATURE']).toBe(sent.headers['PAYMENT-SIGNATURE']);
|
|
54
|
-
expect(JSON.parse(fetchMock.mock.calls[2][1].body).requestId).toBe(first.requestId);
|
|
55
|
-
expect(JSON.stringify(fs.readdirSync(path.join(home,'.lizard','payments')))).not.toContain('test-token');
|
|
56
|
-
await expect(payX402(2000,2100,quote)).rejects.toThrow('Previous payment');
|
|
57
|
-
});
|
|
58
|
-
it('refuses a quote over the approved limit before any request', async () => {
|
|
59
|
-
await expect(payX402(2000,2000,quote)).rejects.toThrow('exceeds --max-total'); expect(fetchMock).not.toHaveBeenCalled();
|
|
60
|
-
});
|
|
61
|
-
});
|