@itpay/cli 0.1.10 → 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/lib/http.js ADDED
@@ -0,0 +1,151 @@
1
+ import { DEFAULT_API_BASE, apiBase, apiTimeoutMs, configuredApiBase, cryptoRandom, readConfig, readCredentials, readSessionToken, readState, safeErrorMessage, writeState } from "./env.js";
2
+
3
+ async function coreApi(pathname, options = {}, flags = {}) {
4
+ const headers = { "Content-Type": "application/json" };
5
+ const credentials = readCredentials();
6
+ if (flags.access_token) {
7
+ const raw = String(flags.access_token);
8
+ headers.Authorization = raw.toLowerCase().startsWith("bearer ") ? raw : `Bearer ${raw}`;
9
+ } else {
10
+ const sessionToken = readSessionToken(credentials);
11
+ if (sessionToken) headers.Authorization = `Bearer ${sessionToken}`;
12
+ }
13
+ if (options.idempotencyKey || flags.idempotency_key) {
14
+ headers["Idempotency-Key"] = String(options.idempotencyKey || flags.idempotency_key);
15
+ }
16
+ if (!options.noAgentHeaders) {
17
+ headers["X-ItPay-Agent-Fingerprint"] = coreAgentFingerprint(flags);
18
+ headers["X-ItPay-Agent-Name"] = coreAgentDisplayName(flags);
19
+ }
20
+ if (options.ops) {
21
+ headers["X-ItPay-Ops-Token"] = sandboxOpsToken(flags);
22
+ }
23
+ Object.assign(headers, options.headers || {});
24
+ if (flags.request_id) headers["X-Request-ID"] = String(flags.request_id);
25
+ if (flags.correlation_id) headers["X-Correlation-ID"] = String(flags.correlation_id);
26
+ const targetURL = coreURL(pathname, flags);
27
+ const timeoutMs = apiTimeoutMs(flags);
28
+ const controller = new AbortController();
29
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
30
+ let response;
31
+ try {
32
+ response = await fetch(targetURL, {
33
+ method: options.method || "GET",
34
+ headers,
35
+ body: options.body ? JSON.stringify(options.body) : undefined,
36
+ signal: controller.signal
37
+ });
38
+ } catch (error) {
39
+ if (error?.name === "AbortError") {
40
+ throw new Error(`request timed out after ${Math.ceil(timeoutMs / 1000)}s: ${safeRequestTarget(targetURL)}`);
41
+ }
42
+ throw new Error(`network request failed: ${safeRequestTarget(targetURL)}: ${safeErrorMessage(error)}`);
43
+ } finally {
44
+ clearTimeout(timer);
45
+ }
46
+ const text = await response.text();
47
+ let payload = {};
48
+ if (text) {
49
+ try {
50
+ payload = JSON.parse(text);
51
+ } catch {
52
+ payload = { text };
53
+ }
54
+ }
55
+ if (!response.ok || payload.success === false) {
56
+ const error = new Error(payload.error || payload.message || `request failed: ${response.status}`);
57
+ error.status = response.status;
58
+ if (response.status === 401 && String(pathname).startsWith("/v1/me/")) {
59
+ error.message = "buyer session required or expired; run status --refresh --json, then run setup --method alipay --json if unauthenticated";
60
+ error.next = [
61
+ { type: "verify_buyer_session", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true },
62
+ { type: "start_auth_if_needed", command: cliCommand("setup", "--method", "alipay", "--json"), safe_for_agent: true }
63
+ ];
64
+ }
65
+ throw error;
66
+ }
67
+ return payload.data ?? payload;
68
+ }
69
+
70
+ function safeRequestTarget(target) {
71
+ try {
72
+ const url = new URL(String(target));
73
+ return `${url.origin}${url.pathname}`;
74
+ } catch {
75
+ return String(target).split("?")[0];
76
+ }
77
+ }
78
+
79
+ function coreAgentFingerprint(flags = {}) {
80
+ const explicit = flags.agent_fingerprint || flags.agent_device_fingerprint || process.env.ITPAY_AGENT_FINGERPRINT || process.env.ITPAY_AGENT_DEVICE_FINGERPRINT;
81
+ if (explicit) return String(explicit);
82
+ const state = readState();
83
+ if (state.core_agent_fingerprint) return state.core_agent_fingerprint;
84
+ const fingerprint = `itp_cli_${cryptoRandom()}`;
85
+ writeState({ ...state, core_agent_fingerprint: fingerprint });
86
+ return fingerprint;
87
+ }
88
+
89
+ function coreAgentDisplayName(flags = {}) {
90
+ return String(flags.agent_name || flags.agent_display_name || process.env.ITPAY_AGENT_NAME || "ItPay CLI buyer agent");
91
+ }
92
+
93
+ function coreURL(pathname, flags = {}) {
94
+ if (/^https?:\/\//i.test(String(pathname))) return String(pathname);
95
+ return `${coreApiBase(flags)}${pathname.startsWith("/") ? "" : "/"}${pathname}`;
96
+ }
97
+
98
+ function coreApiBase(flags = {}, config = readConfig()) {
99
+ const base = flags.api_base || flags.core_api_base || configuredApiBase(config) || DEFAULT_API_BASE;
100
+ return String(base).replace(/\/$/, "");
101
+ }
102
+
103
+ function sandboxOpsToken(flags = {}) {
104
+ const token = flags.ops_token || flags.sandbox_ops_token || process.env.ITPAY_SANDBOX_OPS_TOKEN || process.env.ITPAY_OPS_TOKEN;
105
+ if (!token) throw new Error("sandbox ops token is required; set ITPAY_SANDBOX_OPS_TOKEN or pass --ops-token");
106
+ return String(token);
107
+ }
108
+
109
+ async function api(pathname, options, flags = {}) {
110
+ const config = readConfig();
111
+ const credentials = readCredentials();
112
+ const base = apiBase(flags, config);
113
+ const headers = { "Content-Type": "application/json" };
114
+ if (flags.access_token) {
115
+ headers.Authorization = String(flags.access_token);
116
+ } else {
117
+ const sessionToken = readSessionToken(credentials);
118
+ if (sessionToken) {
119
+ headers.Authorization = `Bearer ${sessionToken}`;
120
+ }
121
+ }
122
+ if (flags.new_api_user || flags.new_api_user_id || flags.user_id) {
123
+ headers["New-Api-User"] = String(flags.new_api_user || flags.new_api_user_id || flags.user_id);
124
+ }
125
+ const timeoutMs = apiTimeoutMs(flags);
126
+ const controller = new AbortController();
127
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
128
+ let response;
129
+ try {
130
+ response = await fetch(`${base}${pathname}`, {
131
+ method: options.method,
132
+ headers,
133
+ body: options.body ? JSON.stringify(options.body) : undefined,
134
+ signal: controller.signal
135
+ });
136
+ } catch (error) {
137
+ if (error?.name === "AbortError") {
138
+ throw new Error(`request timed out after ${Math.ceil(timeoutMs / 1000)}s: ${pathname}`);
139
+ }
140
+ throw error;
141
+ } finally {
142
+ clearTimeout(timer);
143
+ }
144
+ const payload = await response.json().catch(() => ({}));
145
+ if (!response.ok || payload.success === false) {
146
+ throw new Error(payload.message || `request failed: ${response.status}`);
147
+ }
148
+ return payload.data ?? payload;
149
+ }
150
+
151
+ export { coreApi, safeRequestTarget, coreAgentFingerprint, coreAgentDisplayName, coreURL, coreApiBase, sandboxOpsToken, api };
package/lib/ops.js ADDED
@@ -0,0 +1,135 @@
1
+ import { coreApi } from "./http.js";
2
+ import { cryptoRandom, intFlag, output, positional, queryString, readJSON } from "./env.js";
3
+
4
+ async function ops(command, rest, flags) {
5
+ if (command !== "sandbox") throw new Error(`unknown ops command: ${command || ""}`);
6
+ const area = rest[0];
7
+ const action = rest[1];
8
+ if (area === "worker" && action === "run-once") {
9
+ output(await coreApi("/v1/sandbox/workers/run-once", { method: "POST", ops: true }, flags));
10
+ return;
11
+ }
12
+ if (area === "recover-alipay-once") {
13
+ output(await coreApi("/v1/local/workers/recover-alipay-sandbox-once", { method: "POST", ops: true }, flags));
14
+ return;
15
+ }
16
+ if (area === "payment" && action === "query") {
17
+ const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 2);
18
+ if (!paymentIntentID) throw new Error("payment_intent_id is required");
19
+ output(await coreApi(`/v1/payment-intents/${encodeURIComponent(paymentIntentID)}/alipay-sandbox-query`, { method: "POST", ops: true }, flags));
20
+ return;
21
+ }
22
+ if (area === "refund") {
23
+ const refundID = flags.refund || flags.refund_id || positional(rest, 2);
24
+ if (!refundID) throw new Error("refund_id is required");
25
+ if (action === "show") {
26
+ output(await coreApi(`/v1/sandbox/ops/refunds/${encodeURIComponent(refundID)}`, { method: "GET", ops: true }, flags));
27
+ return;
28
+ }
29
+ if (action === "approve" || action === "reject") {
30
+ output(await coreApi(`/v1/sandbox/ops/refunds/${encodeURIComponent(refundID)}/${action}`, {
31
+ method: "POST",
32
+ ops: true,
33
+ idempotencyKey: flags.idempotency_key || `idem_cli_refund_${action}_${refundID}`,
34
+ body: {
35
+ reason_code: flags.reason || flags.reason_code || (action === "approve" ? "approved_by_ops" : "not_eligible"),
36
+ note: flags.note || ""
37
+ }
38
+ }, flags));
39
+ return;
40
+ }
41
+ if (action === "execute") {
42
+ output(await coreApi(`/v1/sandbox/ops/refunds/${encodeURIComponent(refundID)}/execute`, {
43
+ method: "POST",
44
+ ops: true,
45
+ idempotencyKey: flags.idempotency_key || `idem_cli_refund_execute_${refundID}`
46
+ }, flags));
47
+ return;
48
+ }
49
+ }
50
+ if (area === "ledger" && action === "entries") {
51
+ const params = new URLSearchParams();
52
+ if (flags.order || flags.order_id) params.set("order_id", String(flags.order || flags.order_id));
53
+ if (flags.refund || flags.refund_id) params.set("refund_id", String(flags.refund || flags.refund_id));
54
+ if (flags.payment_intent || flags.payment_intent_id) params.set("payment_intent_id", String(flags.payment_intent || flags.payment_intent_id));
55
+ if ([...params.keys()].length === 0) {
56
+ throw new Error("ledger filter is required: use --order, --refund, or --payment-intent");
57
+ }
58
+ output(await coreApi(`/v1/sandbox/ops/ledger/entries${queryString(params)}`, { method: "GET", ops: true }, flags));
59
+ return;
60
+ }
61
+ if (area === "reconciliation") {
62
+ if (action === "run") {
63
+ output(await coreApi("/v1/sandbox/ops/reconciliation-runs", {
64
+ method: "POST",
65
+ ops: true,
66
+ idempotencyKey: flags.idempotency_key || `idem_cli_reconciliation_${cryptoRandom()}`,
67
+ body: {
68
+ reconciliation_run_id: flags.reconciliation_run_id || flags.run_id || "",
69
+ status: flags.status || "matched",
70
+ expected_amount_minor: intFlag(flags.expected_amount_minor || flags.expected || 0, "expected_amount_minor"),
71
+ observed_amount_minor: intFlag(flags.observed_amount_minor || flags.observed || 0, "observed_amount_minor"),
72
+ currency: flags.currency || "CNY",
73
+ raw_statement_ref: flags.raw_statement_ref || flags.statement_ref || ""
74
+ }
75
+ }, flags));
76
+ return;
77
+ }
78
+ if (action === "show") {
79
+ const runID = flags.reconciliation_run_id || flags.run_id || positional(rest, 2);
80
+ if (!runID) throw new Error("reconciliation_run_id is required");
81
+ output(await coreApi(`/v1/sandbox/ops/reconciliation-runs/${encodeURIComponent(runID)}`, { method: "GET", ops: true }, flags));
82
+ return;
83
+ }
84
+ }
85
+ if (area === "settlement" && action === "show") {
86
+ const settlementBatchID = flags.settlement_batch || flags.settlement_batch_id || positional(rest, 2);
87
+ if (!settlementBatchID) throw new Error("settlement_batch_id is required");
88
+ output(await coreApi(`/v1/sandbox/ops/settlement-batches/${encodeURIComponent(settlementBatchID)}`, { method: "GET", ops: true }, flags));
89
+ return;
90
+ }
91
+ throw new Error(`unknown ops sandbox command: ${rest.join(" ")}`);
92
+ }
93
+
94
+ async function admin(command, rest, flags) {
95
+ if (command === "orders") {
96
+ const params = new URLSearchParams();
97
+ for (const key of ["account_id", "status", "provider", "limit"]) {
98
+ if (flags[key]) params.set(key, flags[key]);
99
+ }
100
+ const suffix = params.toString() ? `?${params.toString()}` : "";
101
+ output(await api(`/api/itp/admin/orders${suffix}`, { method: "GET" }, flags));
102
+ return;
103
+ }
104
+ if (command === "payment-events") {
105
+ const params = new URLSearchParams();
106
+ for (const key of ["order_id", "checkout_id", "out_trade_no", "provider", "event_type", "signature_verified", "limit"]) {
107
+ if (flags[key]) params.set(key, flags[key]);
108
+ }
109
+ const suffix = params.toString() ? `?${params.toString()}` : "";
110
+ output(await api(`/api/itp/admin/payment-events${suffix}`, { method: "GET" }, flags));
111
+ return;
112
+ }
113
+ if (command === "outbox") {
114
+ const params = new URLSearchParams();
115
+ for (const key of ["status", "event_type", "aggregate_id", "limit"]) {
116
+ if (flags[key]) params.set(key, flags[key]);
117
+ }
118
+ const suffix = params.toString() ? `?${params.toString()}` : "";
119
+ output(await api(`/api/itp/admin/outbox${suffix}`, { method: "GET" }, flags));
120
+ return;
121
+ }
122
+ if (command === "process-outbox") {
123
+ output(await api("/api/itp/admin/outbox/process", { method: "POST" }, flags));
124
+ return;
125
+ }
126
+ if (command === "recover-order") {
127
+ const orderId = rest[0];
128
+ if (!orderId) throw new Error("order_id is required");
129
+ output(await api(`/api/itp/admin/orders/${encodeURIComponent(orderId)}/recover`, { method: "POST" }, flags));
130
+ return;
131
+ }
132
+ throw new Error(`unknown admin command: ${command || ""}`);
133
+ }
134
+
135
+ export { ops, admin };