@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.
- package/CHANGELOG.md +48 -8
- package/bin/cli.mjs +23 -34
- package/docs/exit-codes.md +2 -1
- package/docs/release-process.md +9 -7
- package/package.json +1 -1
- package/scripts/postinstall.mjs +6 -6
- package/skills/aigateway/SKILL.md +369 -272
- package/src/balance.mjs +7 -7
- package/src/catalog.mjs +38 -0
- package/src/commands/clean.mjs +5 -5
- package/src/commands/sb-invoke.mjs +407 -0
- package/src/commands/sb-tools.mjs +37 -0
- package/src/commands/wallet-gas.mjs +2 -1
- package/src/commands/wallet-init.mjs +20 -21
- package/src/commands/wallet-topup.mjs +15 -15
- package/src/commands/wallet-withdraw.mjs +7 -7
- package/src/config.mjs +24 -24
- package/src/error-codes.mjs +22 -14
- package/src/funding.mjs +2 -2
- package/src/inputs-validator.mjs +125 -0
- package/src/output.mjs +19 -16
- package/src/tools-download.mjs +264 -0
- package/src/update-check.mjs +50 -47
- package/src/walletconnect.mjs +65 -63
- package/src/x402.mjs +13 -10
- package/skills/aigateway/references/check-status.md +0 -68
- package/skills/aigateway/references/create-card.md +0 -114
- package/skills/aigateway/references/store.md +0 -87
- package/src/commands/create-card-status.mjs +0 -67
- package/src/commands/create-card.mjs +0 -352
- package/src/commands/create-image.mjs +0 -428
- package/src/sanitize.mjs +0 -48
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* topup
|
|
2
|
+
* wallet-topup: top up the session wallet and run the one-time facilitator approve.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* 4. session key
|
|
12
|
-
* 5.
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Verify the session key exists (created earlier by `aigateway wallet-init`).
|
|
6
|
+
* 2. Check USDT balance + facilitator allowance.
|
|
7
|
+
* 3. If USDT < LOW_BALANCE_THRESHOLD (1 USDT) or allowance == 0, open WalletConnect to fund:
|
|
8
|
+
* - Top-up amount: TTY mode picks interactively from presets [5, 10, 20, 50] or a custom value;
|
|
9
|
+
* non-TTY mode requires `--amount <usdt>`, otherwise TOPUP_REQUIRED is emitted.
|
|
10
|
+
* - 0.0003 BNB is transferred too when an approve transaction is needed.
|
|
11
|
+
* 4. The session key broadcasts ERC20.approve(facilitator, MaxUint256) once.
|
|
12
|
+
* 5. Re-query balance / allowance and return the final state.
|
|
13
13
|
*/
|
|
14
14
|
import { resolve } from "../config.mjs";
|
|
15
15
|
import { getWalletBalance, getAllowance } from "../balance.mjs";
|
|
@@ -71,7 +71,7 @@ export async function topup(opts) {
|
|
|
71
71
|
const needApprove = allowance === 0n;
|
|
72
72
|
const needGas = needApprove && bnbRaw === 0n;
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// Already prepared: balance is sufficient and facilitator is approved
|
|
75
75
|
if (!needTopup && !needApprove) {
|
|
76
76
|
logInfo("Wallet already prepared (balance ≥ minimum, facilitator approved).");
|
|
77
77
|
const data = {
|
|
@@ -89,7 +89,7 @@ export async function topup(opts) {
|
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
//
|
|
92
|
+
// Decide the top-up amount
|
|
93
93
|
let topupAmount = null;
|
|
94
94
|
if (needTopup) {
|
|
95
95
|
if (balanceLow) {
|
|
@@ -134,7 +134,7 @@ export async function topup(opts) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
// WalletConnect
|
|
137
|
+
// WalletConnect top-up (USDT + optional BNB for approve gas)
|
|
138
138
|
if (needTopup || needGas) {
|
|
139
139
|
const willTransfer = [];
|
|
140
140
|
if (needTopup) willTransfer.push(`${topupAmount} USDT`);
|
|
@@ -161,7 +161,7 @@ export async function topup(opts) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
//
|
|
164
|
+
// Session key broadcasts the one-time facilitator approve
|
|
165
165
|
let approveTx = null;
|
|
166
166
|
if (needApprove) {
|
|
167
167
|
let postBnbRaw = bnbRaw;
|
|
@@ -193,7 +193,7 @@ export async function topup(opts) {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
//
|
|
196
|
+
// Final balance + allowance re-check
|
|
197
197
|
let finalUsdt = usdt;
|
|
198
198
|
let finalBnb = bnb;
|
|
199
199
|
let finalAllowance = allowance;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* withdraw
|
|
2
|
+
* wallet-withdraw: move the session key's funds (USDT + BNB) back to the main wallet.
|
|
3
3
|
*/
|
|
4
4
|
import { createPublicClient, createWalletClient, http, parseUnits, formatUnits, encodeFunctionData } from "viem";
|
|
5
5
|
import { privateKeyToAccount } from "viem/accounts";
|
|
@@ -54,7 +54,7 @@ export async function withdraw(opts) {
|
|
|
54
54
|
|
|
55
55
|
const isWithdrawAll = !opts.amount;
|
|
56
56
|
|
|
57
|
-
//
|
|
57
|
+
// No funds at all
|
|
58
58
|
if (balance.usdtRaw === 0n && balance.bnbRaw === 0n) {
|
|
59
59
|
emitErr("wallet-withdraw", "NO_FUNDS", { message: "No funds to withdraw.", appId });
|
|
60
60
|
return;
|
|
@@ -63,9 +63,9 @@ export async function withdraw(opts) {
|
|
|
63
63
|
let usdtTxHash = null;
|
|
64
64
|
let bnbTxHash = null;
|
|
65
65
|
|
|
66
|
-
// 1.
|
|
66
|
+
// 1. Reclaim USDT (only when USDT balance > 0)
|
|
67
67
|
if (balance.usdtRaw > 0n) {
|
|
68
|
-
// USDT
|
|
68
|
+
// USDT transfer needs BNB for gas
|
|
69
69
|
if (balance.bnbRaw === 0n) {
|
|
70
70
|
emitErr("wallet-withdraw", "INSUFFICIENT_BNB", {
|
|
71
71
|
message: "No BNB for gas. Withdraw is a normal on-chain transfer and requires BNB to pay gas.",
|
|
@@ -119,7 +119,7 @@ export async function withdraw(opts) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// 2.
|
|
122
|
+
// 2. Reclaim remaining BNB (only when withdrawing everything)
|
|
123
123
|
if (isWithdrawAll) {
|
|
124
124
|
const freshBalance = balance.usdtRaw > 0n
|
|
125
125
|
? await getBalanceByAddress(sessionAddress)
|
|
@@ -128,7 +128,7 @@ export async function withdraw(opts) {
|
|
|
128
128
|
if (freshBalance.bnbRaw > 0n) {
|
|
129
129
|
try {
|
|
130
130
|
const gasPrice = await publicClient.getGasPrice();
|
|
131
|
-
//
|
|
131
|
+
// Reserve a 20% buffer to absorb gas-price fluctuations
|
|
132
132
|
const gasCost = BNB_TRANSFER_GAS * (gasPrice * 120n / 100n);
|
|
133
133
|
const sendable = freshBalance.bnbRaw - gasCost;
|
|
134
134
|
|
|
@@ -159,7 +159,7 @@ export async function withdraw(opts) {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
//
|
|
162
|
+
// Final balance lookup
|
|
163
163
|
let finalBalance;
|
|
164
164
|
try {
|
|
165
165
|
finalBalance = await getBalanceByAddress(sessionAddress);
|
package/src/config.mjs
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Config management: ~/.aigateway/config.json
|
|
3
|
+
* Resolution priority: CLI args > env vars > config.json
|
|
4
4
|
*
|
|
5
|
-
* AEON AI Gateway
|
|
6
|
-
* 不同能力(虚拟卡 / Skill Boss 调用)走不同的路径前缀。
|
|
5
|
+
* AEON AI Gateway uses a single x402 service (ai-api.aeon.xyz).
|
|
7
6
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import {readFileSync, writeFileSync, mkdirSync, chmodSync} from "fs";
|
|
8
|
+
import {join} from "path";
|
|
9
|
+
import {homedir} from "os";
|
|
11
10
|
|
|
12
11
|
const CONFIG_DIR = join(homedir(), ".aigateway");
|
|
13
12
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
14
13
|
|
|
15
14
|
const DEFAULTS = {
|
|
16
|
-
|
|
15
|
+
serviceUrl: "https://ai-api-dev.aeon.xyz",
|
|
16
|
+
// serviceUrl: "https://ai-api.aeon.xyz",
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export function loadConfig() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
try {
|
|
21
|
+
return {...DEFAULTS, ...JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))};
|
|
22
|
+
} catch {
|
|
23
|
+
return {...DEFAULTS};
|
|
24
|
+
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function saveConfig(config) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
mkdirSync(CONFIG_DIR, {recursive: true});
|
|
29
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {mode: 0o600});
|
|
30
|
+
chmodSync(CONFIG_FILE, 0o600);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* Resolve a value with priority: cliValue > envKey > config[configKey]
|
|
35
35
|
*/
|
|
36
36
|
export function resolve(cliValue, envKey, configKey) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
if (cliValue) return cliValue;
|
|
38
|
+
if (process.env[envKey]) return process.env[envKey];
|
|
39
|
+
const cfg = loadConfig();
|
|
40
|
+
return cfg[configKey] || undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function getConfigPath() {
|
|
44
|
-
|
|
44
|
+
return CONFIG_FILE;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function isSessionKeyMode() {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
return config.mode === "session-key";
|
|
50
50
|
}
|
package/src/error-codes.mjs
CHANGED
|
@@ -1,39 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Error-code registry — single source of truth shared by the CLI and the docs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 0
|
|
6
|
-
* 1
|
|
7
|
-
* 2
|
|
8
|
-
* 3
|
|
9
|
-
* 4
|
|
4
|
+
* Exit-code semantics:
|
|
5
|
+
* 0 success
|
|
6
|
+
* 1 user error (bad argument, insufficient balance, configuration, user reject)
|
|
7
|
+
* 2 timeout (polling, WalletConnect, signature, on-chain wait)
|
|
8
|
+
* 3 service / network
|
|
9
|
+
* 4 internal error
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export const ERROR_CODES = {
|
|
13
|
-
// =====
|
|
13
|
+
// ===== User error (exit 1) =====
|
|
14
14
|
WALLET_NOT_CONFIGURED: { exit: 1, message: "Wallet not configured. Run: aigateway wallet-init" },
|
|
15
15
|
SERVICE_URL_MISSING: { exit: 1, message: "Service URL not configured." },
|
|
16
16
|
AMOUNT_INVALID: { exit: 1, message: "Invalid amount." },
|
|
17
|
-
AMOUNT_OUT_OF_RANGE: { exit: 1, message: "Amount is outside the allowed range." },
|
|
18
17
|
AMOUNT_EXCEEDS_BALANCE: { exit: 1, message: "Requested amount exceeds available balance." },
|
|
19
18
|
INSUFFICIENT_USDT: { exit: 1, message: "Insufficient USDT balance." },
|
|
20
19
|
INSUFFICIENT_BNB: { exit: 1, message: "Insufficient BNB for gas." },
|
|
21
20
|
NO_FUNDS: { exit: 1, message: "No funds available." },
|
|
22
21
|
NO_MAIN_WALLET: { exit: 1, message: "No main wallet address configured. Use --to <address>." },
|
|
23
|
-
|
|
22
|
+
MISSING_MODEL: { exit: 1, message: "Missing --model. Provide a tool model id (see references/tools.md)." },
|
|
23
|
+
MISSING_INPUTS: { exit: 1, message: "Missing --inputs. Provide a JSON object or @path/to/file.json." },
|
|
24
|
+
INVALID_INPUTS_JSON: { exit: 1, message: "Failed to parse --inputs as JSON." },
|
|
25
|
+
INVALID_INPUTS: { exit: 1, message: "Inputs failed schema validation." },
|
|
26
|
+
INPUTS_FILE_NOT_FOUND: { exit: 1, message: "Inputs file (passed via --inputs @path) not found." },
|
|
27
|
+
INVALID_MODEL_ID: { exit: 1, message: "Server rejected the model id." },
|
|
28
|
+
MODEL_PRICING_NOT_CONFIGURED: { exit: 1, message: "This model is not yet priced on the gateway. Ask the operator to add it to skillboss-pricing.json." },
|
|
29
|
+
INVALID_BODY: { exit: 1, message: "Server rejected the request body." },
|
|
24
30
|
TOPUP_REQUIRED: { exit: 1, message: "Wallet top-up required. Choose an amount and rerun with --topup-amount <usdt>." },
|
|
25
31
|
TOPUP_AMOUNT_TOO_SMALL: { exit: 1, message: "Top-up amount is below the minimum." },
|
|
26
32
|
PAYMENT_REJECTED: { exit: 1, message: "Payment approval was rejected. Please try again if you'd like to proceed." },
|
|
27
33
|
|
|
28
|
-
// =====
|
|
34
|
+
// ===== Timeout (exit 2) =====
|
|
29
35
|
PAYMENT_TIMEOUT: { exit: 2, message: "Payment approval timed out. Please try again." },
|
|
30
36
|
WC_SESSION_EXPIRED: { exit: 2, message: "WalletConnect session expired." },
|
|
31
|
-
POLL_TIMEOUT: { exit: 2, message: "Polling timed out. Card may still be provisioning." },
|
|
32
37
|
TX_TIMEOUT: { exit: 2, message: "On-chain transaction timed out." },
|
|
38
|
+
UPDATE_APPLIED: { exit: 2, message: "Package was just upgraded. Rerun the previous command on the new version." },
|
|
33
39
|
|
|
34
|
-
// =====
|
|
40
|
+
// ===== Service / network (exit 3) =====
|
|
35
41
|
SERVICE_UNAVAILABLE: { exit: 3, message: "Service unavailable or network error." },
|
|
36
42
|
PAYMENT_FETCH_FAILED: { exit: 3, message: "Failed to fetch payment requirements." },
|
|
43
|
+
CATALOG_FETCH_FAILED: { exit: 3, message: "Failed to fetch tools catalog from the server." },
|
|
37
44
|
BALANCE_CHECK_FAILED: { exit: 3, message: "Failed to check balance." },
|
|
38
45
|
ALLOWANCE_CHECK_FAILED: { exit: 3, message: "Failed to check allowance." },
|
|
39
46
|
TX_REVERTED: { exit: 3, message: "On-chain transaction reverted." },
|
|
@@ -42,9 +49,10 @@ export const ERROR_CODES = {
|
|
|
42
49
|
INVALID_PAYMENT_AMOUNT: { exit: 3, message: "Server returned invalid payment amount." },
|
|
43
50
|
PAYMENT_FAILED: { exit: 3, message: "Payment request failed." },
|
|
44
51
|
IMAGE_DOWNLOAD_FAILED: { exit: 3, message: "Image download failed." },
|
|
52
|
+
DOWNLOAD_FAILED: { exit: 3, message: "Output file download failed." },
|
|
45
53
|
FUNDING_FAILED: { exit: 3, message: "Funding flow failed." },
|
|
46
54
|
|
|
47
|
-
// =====
|
|
55
|
+
// ===== Internal (exit 4) =====
|
|
48
56
|
INTERNAL_ERROR: { exit: 4, message: "Internal error." },
|
|
49
57
|
WALLET_ERROR: { exit: 1, message: "Wallet operation failed." },
|
|
50
58
|
};
|
package/src/funding.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Funding flow: WalletConnect-based USDT/BNB transfer to session key,
|
|
3
3
|
* plus on-chain pre-authorization (ERC20.approve facilitator).
|
|
4
4
|
*
|
|
5
|
-
* Shared by:
|
|
6
|
-
*
|
|
5
|
+
* Shared by: sb-invoke (lazy top-up when balance is short),
|
|
6
|
+
* wallet-topup (proactive pre-flight).
|
|
7
7
|
*/
|
|
8
8
|
import {
|
|
9
9
|
withWallet,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JSON Schema validator for tools-catalog inputs schemas.
|
|
3
|
+
*
|
|
4
|
+
* Supports the subset of JSON Schema actually used in tools-catalog.json:
|
|
5
|
+
* - type: object | string | number | integer | boolean | array
|
|
6
|
+
* - required: string[]
|
|
7
|
+
* - properties: { fieldName: <fieldSchema> }
|
|
8
|
+
* - enum: any[]
|
|
9
|
+
* - minimum / maximum (number, integer)
|
|
10
|
+
* - items (for arrays)
|
|
11
|
+
* - oneOf: [schema, ...] (treated as "any-of" — passes if any branch matches)
|
|
12
|
+
*
|
|
13
|
+
* We intentionally avoid pulling in ajv to keep the CLI dep tree small.
|
|
14
|
+
*
|
|
15
|
+
* @returns {{ ok: boolean, errors: Array<{ field: string, message: string, kind: "missing"|"type"|"enum"|"range"|"other" }> }}
|
|
16
|
+
*/
|
|
17
|
+
export function validateInputs(inputs, schema) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
if (!schema) return { ok: true, errors };
|
|
20
|
+
|
|
21
|
+
// root must be an object
|
|
22
|
+
if (schema.type === "object" || schema.properties || schema.required) {
|
|
23
|
+
if (typeof inputs !== "object" || inputs === null || Array.isArray(inputs)) {
|
|
24
|
+
errors.push({ field: "(root)", kind: "type", message: "inputs must be a JSON object" });
|
|
25
|
+
return { ok: false, errors };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Required fields
|
|
30
|
+
if (Array.isArray(schema.required)) {
|
|
31
|
+
for (const field of schema.required) {
|
|
32
|
+
const v = inputs[field];
|
|
33
|
+
if (v === undefined || v === null || (typeof v === "string" && v.trim() === "")) {
|
|
34
|
+
errors.push({ field, kind: "missing", message: `required field "${field}" is missing` });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Per-field validation
|
|
40
|
+
if (schema.properties) {
|
|
41
|
+
for (const [field, fieldSchema] of Object.entries(schema.properties)) {
|
|
42
|
+
if (inputs[field] === undefined || inputs[field] === null) continue;
|
|
43
|
+
errors.push(...validateField(field, inputs[field], fieldSchema));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { ok: errors.length === 0, errors };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateField(field, value, schema) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
|
|
53
|
+
// oneOf: pass if any branch accepts (no errors).
|
|
54
|
+
if (Array.isArray(schema.oneOf)) {
|
|
55
|
+
const branchErrors = schema.oneOf.map((sub) => validateField(field, value, sub));
|
|
56
|
+
const anyPass = branchErrors.some((errs) => errs.length === 0);
|
|
57
|
+
if (!anyPass) {
|
|
58
|
+
errors.push({
|
|
59
|
+
field,
|
|
60
|
+
kind: "type",
|
|
61
|
+
message: `value does not match any of oneOf schemas (tried ${schema.oneOf.length} branches)`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return errors;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Type check
|
|
68
|
+
if (schema.type) {
|
|
69
|
+
if (!checkType(value, schema.type)) {
|
|
70
|
+
errors.push({
|
|
71
|
+
field,
|
|
72
|
+
kind: "type",
|
|
73
|
+
message: `expected type "${schema.type}", got "${actualType(value)}"`,
|
|
74
|
+
});
|
|
75
|
+
return errors; // bail out on type mismatch
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Enum
|
|
80
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
|
|
81
|
+
errors.push({
|
|
82
|
+
field,
|
|
83
|
+
kind: "enum",
|
|
84
|
+
message: `value ${JSON.stringify(value)} must be one of [${schema.enum.map((v) => JSON.stringify(v)).join(", ")}]`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Number range
|
|
89
|
+
if ((schema.type === "number" || schema.type === "integer") && typeof value === "number") {
|
|
90
|
+
if (schema.minimum != null && value < schema.minimum) {
|
|
91
|
+
errors.push({ field, kind: "range", message: `value ${value} is below minimum ${schema.minimum}` });
|
|
92
|
+
}
|
|
93
|
+
if (schema.maximum != null && value > schema.maximum) {
|
|
94
|
+
errors.push({ field, kind: "range", message: `value ${value} is above maximum ${schema.maximum}` });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Array items
|
|
99
|
+
if (schema.type === "array" && Array.isArray(value) && schema.items) {
|
|
100
|
+
value.forEach((item, i) => {
|
|
101
|
+
errors.push(...validateField(`${field}[${i}]`, item, schema.items));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return errors;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function checkType(value, type) {
|
|
109
|
+
switch (type) {
|
|
110
|
+
case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
111
|
+
case "array": return Array.isArray(value);
|
|
112
|
+
case "string": return typeof value === "string";
|
|
113
|
+
case "number": return typeof value === "number" && !Number.isNaN(value);
|
|
114
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
115
|
+
case "boolean": return typeof value === "boolean";
|
|
116
|
+
case "null": return value === null;
|
|
117
|
+
default: return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function actualType(value) {
|
|
122
|
+
if (value === null) return "null";
|
|
123
|
+
if (Array.isArray(value)) return "array";
|
|
124
|
+
return typeof value;
|
|
125
|
+
}
|
package/src/output.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Unified output envelope: JSON on stdout + tiered stderr logs.
|
|
3
3
|
*
|
|
4
|
-
* stdout:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* stderr:
|
|
4
|
+
* stdout: a single line of final JSON (machine-readable)
|
|
5
|
+
* - success: { ok: true, command, version, data }
|
|
6
|
+
* - failure: { ok: false, command, version, error: { code, message, ...context } }
|
|
7
|
+
* stderr: progress logs (human-readable; agents can ignore them)
|
|
8
8
|
*
|
|
9
|
-
* --legacy-output
|
|
9
|
+
* --legacy-output mode: emit the pre-envelope shape so scripts / agents that
|
|
10
|
+
* already parse the old JSON can migrate gradually.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { readFileSync } from "fs";
|
|
@@ -30,10 +31,11 @@ export function isLegacyMode() { return LEGACY_MODE; }
|
|
|
30
31
|
export function isVerboseMode() { return VERBOSE_MODE; }
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {object}
|
|
34
|
+
* Emit a success result. Callers should let the function return naturally
|
|
35
|
+
* (do not call process.exit afterwards).
|
|
36
|
+
* @param {string} command - command name, e.g. "sb-invoke" / "wallet-init"
|
|
37
|
+
* @param {object} data - placed under envelope.data
|
|
38
|
+
* @param {object} [legacyShape] - the legacy-mode payload; if omitted, `data` is used
|
|
37
39
|
*/
|
|
38
40
|
export function emitOk(command, data, legacyShape) {
|
|
39
41
|
if (LEGACY_MODE) {
|
|
@@ -44,10 +46,11 @@ export function emitOk(command, data, legacyShape) {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
|
-
*
|
|
49
|
+
* Emit an error and exit with the corresponding exit code.
|
|
48
50
|
* @param {string} command
|
|
49
|
-
* @param {string} code - ERROR_CODES
|
|
50
|
-
* @param {object} [details] -
|
|
51
|
+
* @param {string} code - key of ERROR_CODES
|
|
52
|
+
* @param {object} [details] - extra fields. `message` overrides the default message;
|
|
53
|
+
* `legacy` fully replaces the output in legacy mode.
|
|
51
54
|
*/
|
|
52
55
|
export function emitErr(command, code, details = {}) {
|
|
53
56
|
const info = ERROR_CODES[code] || ERROR_CODES.INTERNAL_ERROR;
|
|
@@ -69,17 +72,17 @@ export function emitErr(command, code, details = {}) {
|
|
|
69
72
|
process.exit(exit);
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
/**
|
|
75
|
+
/** Progress log (suppressed in quiet mode) */
|
|
73
76
|
export function logInfo(msg) {
|
|
74
77
|
if (!QUIET_MODE) console.error(msg);
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
/**
|
|
80
|
+
/** Verbose log (only emitted in verbose mode) */
|
|
78
81
|
export function logVerbose(msg) {
|
|
79
82
|
if (VERBOSE_MODE && !QUIET_MODE) console.error(msg);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
/**
|
|
85
|
+
/** Error log (still emitted in quiet mode) */
|
|
83
86
|
export function logError(msg) {
|
|
84
87
|
console.error(msg);
|
|
85
88
|
}
|