@aeon-ai-pay/aigateway 0.1.4 → 0.1.6

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.
@@ -1,428 +0,0 @@
1
- /**
2
- * create-image:通过 x402 调用 Skill Boss 生成 AI 图像
3
- *
4
- * 服务端路径:GET {serviceUrl}/open/ai/x402/skillBoss/create?body=<urlencoded-json>&appId=<merchant>
5
- * 流程:fetch payment requirements → balance + allowance 检查
6
- * → (余额不足时)走 funding.mjs/fundSessionKey 充值
7
- * → x402 EIP-712 签名提交 → 下载图片到本地
8
- */
9
- import { createX402Api, decodePaymentResponse, fetchPaymentRequirements } from "../x402.mjs";
10
- import { resolve } from "../config.mjs";
11
- import { getWalletBalance, getAllowance } from "../balance.mjs";
12
- import axios from "axios";
13
- import {
14
- fundSessionKey,
15
- promptTopupAmount,
16
- MIN_TOPUP_USDT,
17
- TOPUP_PRESETS,
18
- } from "../funding.mjs";
19
- import { WalletConnectError } from "../walletconnect.mjs";
20
- import { emitOk, emitErr, logInfo } from "../output.mjs";
21
- import { mkdirSync, createWriteStream, existsSync, unlinkSync, openSync, readSync, closeSync, statSync } from "node:fs";
22
- import { join, basename, extname } from "node:path";
23
- import { homedir } from "node:os";
24
- import { URL } from "node:url";
25
- import { get as httpsGet } from "node:https";
26
- import { get as httpGet } from "node:http";
27
-
28
- const DEFAULT_IMAGE_DIR = join(homedir(), "aigateway-images");
29
- const DEFAULT_MODEL = "replicate/black-forest-labs/flux-schnell";
30
-
31
- export async function createImage(opts) {
32
- logInfo("Generating image...");
33
- const serviceUrl = resolve(opts.serviceUrl, "AIGATEWAY_SERVICE_URL", "serviceUrl");
34
- const privateKey = resolve(opts.privateKey, "EVM_PRIVATE_KEY", "privateKey");
35
- const { prompt, appId } = opts;
36
- const aspectRatio = opts.aspectRatio || "16:9";
37
- const outputFormat = (opts.outputFormat || "png").toLowerCase();
38
- const model = opts.model || DEFAULT_MODEL;
39
-
40
- if (!serviceUrl) {
41
- emitErr("create-image", "SERVICE_URL_MISSING", {
42
- message: "Missing service URL. Set env AIGATEWAY_SERVICE_URL if you need to override the built-in default.",
43
- appId,
44
- });
45
- return;
46
- }
47
- if (!privateKey) {
48
- emitErr("create-image", "WALLET_NOT_CONFIGURED", { appId });
49
- return;
50
- }
51
- if (!prompt || !prompt.trim()) {
52
- emitErr("create-image", "MISSING_PROMPT", { appId });
53
- return;
54
- }
55
-
56
- const bodyPayload = {
57
- model,
58
- inputs: {
59
- prompt,
60
- aspect_ratio: aspectRatio,
61
- output_format: outputFormat,
62
- },
63
- };
64
- const bodyParam = encodeURIComponent(JSON.stringify(bodyPayload));
65
- const url = `${serviceUrl}/open/ai/x402/skillBoss/create?body=${bodyParam}&appId=${encodeURIComponent(appId)}`;
66
-
67
- logInfo("Fetching payment requirements...");
68
- let requiredUsdt;
69
- let paymentReq;
70
- try {
71
- paymentReq = await fetchPaymentRequirements(url);
72
- requiredUsdt = paymentReq.amountUsdt;
73
- logInfo(`Required: ${requiredUsdt} USDT (pay to ${paymentReq.payTo})`);
74
- } catch (e) {
75
- emitErr("create-image", "PAYMENT_FETCH_FAILED", {
76
- message: `Failed to fetch payment requirements: ${e.message}`,
77
- appId,
78
- });
79
- return;
80
- }
81
-
82
- logInfo("Checking wallet...");
83
- let needTopup = false;
84
- let needGas = false;
85
- let sessionAddress;
86
- let topupAmount = null;
87
- let balanceInitialUsdt = null;
88
- let balanceBeforeChargeUsdt = null;
89
-
90
- try {
91
- const { address, usdt, bnb, bnbRaw } = await getWalletBalance(privateKey);
92
- sessionAddress = address;
93
- balanceInitialUsdt = usdt;
94
- balanceBeforeChargeUsdt = usdt;
95
- const usdtNum = parseFloat(usdt);
96
-
97
- logInfo(`Wallet: ${address}`);
98
- logInfo(`Balance: ${usdt} USDT, ${bnb} BNB`);
99
-
100
- const allowance = await getAllowance(address);
101
- const requiredWei = BigInt(paymentReq.amountWei);
102
- if (requiredWei === 0n) {
103
- emitErr("create-image", "INVALID_PAYMENT_AMOUNT", {
104
- message: "Server returned invalid payment amount (0). Please retry later.",
105
- appId,
106
- });
107
- return;
108
- }
109
- if (allowance >= requiredWei) {
110
- logInfo("Allowance sufficient, no approve needed.");
111
- } else {
112
- logInfo(`Allowance ${allowance} < required ${requiredWei}; approve needed.`);
113
- if (bnbRaw === 0n) {
114
- needGas = true;
115
- logInfo("No BNB for approve gas, will request BNB transfer.");
116
- }
117
- }
118
-
119
- if (usdtNum < requiredUsdt) {
120
- needTopup = true;
121
- const shortfall = requiredUsdt - usdtNum;
122
- const minTopup = Math.max(MIN_TOPUP_USDT, Math.ceil(shortfall));
123
- logInfo(`USDT insufficient: have ${usdtNum}, need ${requiredUsdt}, shortfall ${shortfall.toFixed(6)} (top-up minimum: ${minTopup} USDT)`);
124
-
125
- if (opts.topupAmount != null && String(opts.topupAmount).trim() !== "") {
126
- const amt = Number(opts.topupAmount);
127
- if (!Number.isFinite(amt) || amt <= 0) {
128
- emitErr("create-image", "AMOUNT_INVALID", {
129
- message: `Invalid --topup-amount: ${opts.topupAmount}`,
130
- appId,
131
- });
132
- return;
133
- }
134
- if (amt < minTopup) {
135
- emitErr("create-image", "TOPUP_AMOUNT_TOO_SMALL", {
136
- message: `--topup-amount ${amt} USDT is below the ${minTopup} USDT minimum for this call.`,
137
- minTopup,
138
- appId,
139
- });
140
- return;
141
- }
142
- topupAmount = String(opts.topupAmount);
143
- logInfo(`Using --topup-amount: ${topupAmount} USDT`);
144
- } else if (process.stdin.isTTY) {
145
- topupAmount = await promptTopupAmount(minTopup);
146
- logInfo(`Selected top-up amount: ${topupAmount} USDT`);
147
- } else {
148
- const presets = TOPUP_PRESETS.filter((v) => v >= minTopup);
149
- emitErr("create-image", "TOPUP_REQUIRED", {
150
- message: `USDT balance is below the ${minTopup} USDT minimum for this call. Choose a top-up amount and rerun with --topup-amount <usdt>.`,
151
- minTopup,
152
- required: requiredUsdt,
153
- currentBalance: balanceInitialUsdt,
154
- address: sessionAddress,
155
- appId,
156
- presets,
157
- hint: `Rerun: aigateway wallet-topup --amount <usdt> --app-id ${appId}`,
158
- });
159
- return;
160
- }
161
- }
162
- } catch (e) {
163
- emitErr("create-image", "BALANCE_CHECK_FAILED", {
164
- message: `Balance check failed: ${e.message}`,
165
- appId,
166
- });
167
- return;
168
- }
169
-
170
- if (needTopup || needGas) {
171
- logInfo("Funding flow triggered...");
172
- try {
173
- await fundSessionKey({
174
- sessionAddress,
175
- usdtAmount: needTopup ? topupAmount : null,
176
- needGas,
177
- });
178
- } catch (e) {
179
- if (e instanceof WalletConnectError) {
180
- emitErr("create-image", e.code, { message: e.message, address: sessionAddress, appId });
181
- } else {
182
- emitErr("create-image", "FUNDING_FAILED", { message: e.message, address: sessionAddress, appId });
183
- }
184
- return;
185
- }
186
-
187
- logInfo("Re-checking wallet balance...");
188
- try {
189
- const { usdt, bnbRaw } = await getWalletBalance(privateKey);
190
- balanceBeforeChargeUsdt = usdt;
191
- const usdtNum = parseFloat(usdt);
192
- if (needGas && bnbRaw === 0n) {
193
- emitErr("create-image", "INSUFFICIENT_BNB", {
194
- message: "No BNB for approve transaction after funding. Run 'aigateway wallet-gas' to add BNB manually.",
195
- address: sessionAddress,
196
- appId,
197
- });
198
- return;
199
- }
200
- if (usdtNum < requiredUsdt) {
201
- emitErr("create-image", "INSUFFICIENT_USDT", {
202
- message: "Still insufficient USDT after funding.",
203
- required: `${requiredUsdt} USDT`,
204
- available: `${usdt} USDT`,
205
- address: sessionAddress,
206
- appId,
207
- });
208
- return;
209
- }
210
- } catch (e) {
211
- emitErr("create-image", "BALANCE_CHECK_FAILED", {
212
- message: `Balance re-check failed: ${e.message}`,
213
- appId,
214
- });
215
- return;
216
- }
217
- }
218
-
219
- const { client } = createX402Api(privateKey);
220
- logInfo(`Submitting payment & request: ${url}`);
221
-
222
- try {
223
- const { x402HTTPClient } = await import("@aeon-ai-pay/core/client");
224
- const httpClient = new x402HTTPClient(client);
225
-
226
- const raw402 = paymentReq.raw402Response;
227
- const getHeader = (name) => {
228
- const value = raw402.headers[name] ?? raw402.headers[name.toLowerCase()];
229
- return typeof value === "string" ? value : undefined;
230
- };
231
- const paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, raw402.data);
232
- const paymentPayload = await client.createPaymentPayload(paymentRequired);
233
- const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
234
-
235
- const response = await axios.get(url, {
236
- headers: {
237
- ...paymentHeaders,
238
- "Access-Control-Expose-Headers": "PAYMENT-RESPONSE",
239
- },
240
- });
241
- const paymentResponse = decodePaymentResponse(response.headers);
242
-
243
- const transaction = response.data?.transaction || paymentResponse?.txHash || null;
244
- const images = Array.isArray(response.data?.data?.images) ? response.data.data.images : [];
245
-
246
- const outputDir = opts.output || DEFAULT_IMAGE_DIR;
247
- const downloaded = [];
248
- if (images.length > 0) {
249
- mkdirSync(outputDir, { recursive: true });
250
- for (const img of images) {
251
- const imgUrl = img?.url;
252
- if (!imgUrl) continue;
253
- try {
254
- const localPath = await downloadImage(imgUrl, outputDir);
255
- const meta = readImageMeta(localPath);
256
- downloaded.push({
257
- url: imgUrl,
258
- localPath,
259
- format: meta.format,
260
- width: meta.width,
261
- height: meta.height,
262
- sizeBytes: meta.sizeBytes,
263
- sizeHuman: meta.sizeHuman,
264
- });
265
- logInfo(`Saved: ${localPath} (${meta.format || "?"}, ${meta.width || "?"}×${meta.height || "?"}, ${meta.sizeHuman})`);
266
- } catch (e) {
267
- logInfo(`Failed to download ${imgUrl}: ${e.message}`);
268
- downloaded.push({ url: imgUrl, error: e.message });
269
- }
270
- }
271
- }
272
-
273
- let balanceAfterUsdt = null;
274
- try {
275
- const after = await getWalletBalance(privateKey);
276
- balanceAfterUsdt = after.usdt;
277
- } catch (e) {
278
- logInfo(`Post-payment balance check failed: ${e.message}`);
279
- }
280
-
281
- const result = {
282
- appId,
283
- prompt,
284
- aspectRatio,
285
- outputFormat,
286
- model,
287
- transaction,
288
- images: downloaded,
289
- balance: {
290
- initial: balanceInitialUsdt,
291
- before: balanceBeforeChargeUsdt,
292
- after: balanceAfterUsdt,
293
- charged: requiredUsdt,
294
- topup: topupAmount,
295
- },
296
- data: response.data,
297
- paymentResponse,
298
- };
299
- emitOk("create-image", result, { success: true, ...result });
300
- } catch (error) {
301
- emitErr("create-image", "PAYMENT_FAILED", {
302
- message: error.message,
303
- status: error.response?.status,
304
- data: error.response?.data,
305
- appId,
306
- });
307
- }
308
- }
309
-
310
- function readImageMeta(filePath) {
311
- const sizeBytes = statSync(filePath).size;
312
- const sizeHuman = humanSize(sizeBytes);
313
- let format = null, width = null, height = null;
314
- const fd = openSync(filePath, "r");
315
- try {
316
- const buf = Buffer.alloc(64 * 1024);
317
- const len = readSync(fd, buf, 0, buf.length, 0);
318
- if (len >= 24 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4E && buf[3] === 0x47) {
319
- format = "png";
320
- width = buf.readUInt32BE(16);
321
- height = buf.readUInt32BE(20);
322
- } else if (len >= 4 && buf[0] === 0xFF && buf[1] === 0xD8 && buf[2] === 0xFF) {
323
- format = "jpeg";
324
- let i = 2;
325
- while (i + 9 < len) {
326
- if (buf[i] !== 0xFF) { i++; continue; }
327
- while (i < len && buf[i] === 0xFF) i++;
328
- const marker = buf[i];
329
- i++;
330
- if (marker === 0xD8 || marker === 0xD9) continue;
331
- const segLen = buf.readUInt16BE(i);
332
- if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) {
333
- height = buf.readUInt16BE(i + 3);
334
- width = buf.readUInt16BE(i + 5);
335
- break;
336
- }
337
- i += segLen;
338
- }
339
- } else if (len >= 30 && buf.slice(0, 4).toString("ascii") === "RIFF" && buf.slice(8, 12).toString("ascii") === "WEBP") {
340
- format = "webp";
341
- const fourCC = buf.slice(12, 16).toString("ascii");
342
- if (fourCC === "VP8 ") {
343
- width = buf.readUInt16LE(26) & 0x3FFF;
344
- height = buf.readUInt16LE(28) & 0x3FFF;
345
- } else if (fourCC === "VP8L") {
346
- const b0 = buf[21], b1 = buf[22], b2 = buf[23], b3 = buf[24];
347
- width = ((b1 & 0x3F) << 8 | b0) + 1;
348
- height = ((b3 & 0x0F) << 10 | b2 << 2 | (b1 & 0xC0) >> 6) + 1;
349
- } else if (fourCC === "VP8X") {
350
- width = (buf[24] | (buf[25] << 8) | (buf[26] << 16)) + 1;
351
- height = (buf[27] | (buf[28] << 8) | (buf[29] << 16)) + 1;
352
- }
353
- }
354
- } catch {
355
- // leave null on parse failure
356
- } finally {
357
- closeSync(fd);
358
- }
359
- return { format, width, height, sizeBytes, sizeHuman };
360
- }
361
-
362
- function humanSize(bytes) {
363
- if (!Number.isFinite(bytes)) return "?";
364
- if (bytes < 1024) return `${bytes} B`;
365
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
366
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
367
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
368
- }
369
-
370
- function downloadImage(imgUrl, outputDir, { maxRedirects = 5, timeoutMs = 60_000 } = {}) {
371
- let filename;
372
- try {
373
- filename = basename(new URL(imgUrl).pathname) || `image-${Date.now()}.png`;
374
- } catch {
375
- filename = `image-${Date.now()}.png`;
376
- }
377
- if (!extname(filename)) filename += ".png";
378
-
379
- let target = join(outputDir, filename);
380
- if (existsSync(target)) {
381
- const ext = extname(filename);
382
- const stem = filename.slice(0, filename.length - ext.length);
383
- let i = 1;
384
- while (existsSync(join(outputDir, `${stem}-${i}${ext}`))) i++;
385
- target = join(outputDir, `${stem}-${i}${ext}`);
386
- }
387
-
388
- return new Promise((resolve, reject) => {
389
- const fetchOnce = (currentUrl, redirectsLeft) => {
390
- let parsed;
391
- try {
392
- parsed = new URL(currentUrl);
393
- } catch (e) {
394
- return reject(new Error(`Invalid URL: ${currentUrl}`));
395
- }
396
- const httpModule = parsed.protocol === "http:" ? httpGet : httpsGet;
397
- const req = httpModule(currentUrl, { timeout: timeoutMs }, (res) => {
398
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
399
- res.resume();
400
- if (redirectsLeft <= 0) return reject(new Error("Too many redirects"));
401
- const nextUrl = new URL(res.headers.location, currentUrl).toString();
402
- return fetchOnce(nextUrl, redirectsLeft - 1);
403
- }
404
- if (res.statusCode !== 200) {
405
- res.resume();
406
- return reject(new Error(`HTTP ${res.statusCode} from ${currentUrl}`));
407
- }
408
- const file = createWriteStream(target);
409
- res.pipe(file);
410
- file.on("finish", () => file.close(() => resolve(target)));
411
- file.on("error", (err) => {
412
- try { unlinkSync(target); } catch {}
413
- reject(err);
414
- });
415
- res.on("error", (err) => {
416
- file.destroy();
417
- try { unlinkSync(target); } catch {}
418
- reject(err);
419
- });
420
- });
421
- req.on("error", reject);
422
- req.on("timeout", () => {
423
- req.destroy(new Error(`Download timed out after ${timeoutMs}ms`));
424
- });
425
- };
426
- fetchOnce(imgUrl, maxRedirects);
427
- });
428
- }
package/src/sanitize.mjs DELETED
@@ -1,48 +0,0 @@
1
- /**
2
- * 卡片输出脱敏:隐藏敏感卡片信息(完整卡号→末4位、移除CVV、移除有效期)
3
- * CLI 输出 JSON 供 Agent 解析,Agent 按产品模板展示给用户
4
- */
5
-
6
- // 需要替换为末4位的字段
7
- const CARD_NUMBER_KEYS = new Set([
8
- "cardnumber", "cardno",
9
- ]);
10
-
11
- // 需要完全移除的字段
12
- const REMOVE_KEYS = new Set([
13
- "cvv", "cvv2", "cvc", "cvc2", "securitycode",
14
- "expiry", "expirydate", "expiredate", "cardexpiry",
15
- "expirationdate", "validthru",
16
- ]);
17
-
18
- /**
19
- * 递归脱敏对象:
20
- * - cardNumber/cardNo → 只保留末4位("•••• 3398")
21
- * - cvv/securityCode → 移除
22
- * - expiry/expireDate → 移除
23
- */
24
- export function sanitizeOutput(obj) {
25
- if (obj === null || obj === undefined) return obj;
26
- if (Array.isArray(obj)) return obj.map(sanitizeOutput);
27
- if (typeof obj !== "object") return obj;
28
-
29
- const result = {};
30
- for (const [key, value] of Object.entries(obj)) {
31
- const normalized = key.toLowerCase().replace(/[-_]/g, "");
32
-
33
- // 移除 CVV、有效期等敏感字段
34
- if (REMOVE_KEYS.has(normalized)) continue;
35
-
36
- // 卡号只保留末4位
37
- if (CARD_NUMBER_KEYS.has(normalized)) {
38
- if (typeof value === "string" && value.length >= 4) {
39
- result[key] = "•••• " + value.slice(-4);
40
- }
41
- // value 为 null 时不输出此字段
42
- continue;
43
- }
44
-
45
- result[key] = sanitizeOutput(value);
46
- }
47
- return result;
48
- }