cyymall-cli 0.1.5 → 0.1.7
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 +19 -4
- package/package.json +1 -1
- package/src/Untitled +1 -0
- package/src/cli.js +43 -4
- package/src/commands/apiCall.js +1 -1
- package/src/commands/auth.js +112 -48
- package/src/commands/cart.js +1 -5
- package/src/commands/order.js +6 -30
- package/src/commands/product.js +1 -5
- package/src/commands/shop.js +4 -20
- package/src/config.js +67 -1
- package/src/http.js +1 -2
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ npm publish --otp=123456
|
|
|
57
57
|
cyy config path
|
|
58
58
|
cyy auth send-code --phone <mobile>
|
|
59
59
|
cyy auth login --phone <mobile> --code <sms>
|
|
60
|
+
cyy auth import --token <appToken> [--member-id <id>] [--shop-id <id>] [--site-id <id>]
|
|
60
61
|
cyy auth whoami
|
|
61
62
|
cyy shop list
|
|
62
63
|
cyy shop sites --shop-id <门店ID>
|
|
@@ -70,14 +71,28 @@ cyy serve --port 8787
|
|
|
70
71
|
|
|
71
72
|
## Session vs bootstrap env
|
|
72
73
|
|
|
73
|
-
**After `cyy auth login`
|
|
74
|
-
后续 **`api call`、`product search`、`order quick`** 等命令都会从该文件加载并组装 Header,正常情况下不再需要设置 `CYY_BOOTSTRAP_*`。
|
|
74
|
+
**After `cyy auth login` or `cyy auth import` succeeds**, session fields live under **`~/.cyymall/config.json`** (`token`, `member_id`, `shop_id`, `site_id`, `version_code`, …). **`api call`、`product search`、`order quick`** 等命令读取**有效会话**:先合并配置文件,再用下表 `CYY_*` 覆盖(适合 Gateway / MCP 子进程注入、不落盘)。
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
**External token (skip SMS login):**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cyy auth import --token "<appToken>" --member-id "<id>" --shop-id "<id>"
|
|
80
|
+
# or ephemeral for one process:
|
|
81
|
+
export CYY_TOKEN="<appToken>"
|
|
82
|
+
export CYY_SHOP_ID="<id>"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**`CYY_BOOTSTRAP_*` 仅用于「尚未登录」时**(无 saved token 且无 `CYY_TOKEN`)。已登录时业务请求优先使用配置文件 + `CYY_TOKEN` 等会话变量。
|
|
77
86
|
|
|
78
87
|
| Variable | Purpose |
|
|
79
88
|
|----------|---------|
|
|
80
89
|
| `CYY_BASE_URL` | Override API host (default `https://dhcmall.ifoodbuy.com`) |
|
|
90
|
+
| `CYY_TOKEN` | Session token for current process (overrides saved config) |
|
|
91
|
+
| `CYY_MEMBER_ID` | Override `member_id` header |
|
|
92
|
+
| `CYY_SHOP_ID` | Override `shop_id` header |
|
|
93
|
+
| `CYY_SITE_ID` | Override `site_id` header |
|
|
94
|
+
| `CYY_VERSION_CODE` | Override `version_code` header |
|
|
95
|
+
| `CYY_PHONE` | Optional label in config display |
|
|
81
96
|
| `CYY_ENCRYPT_OFF` | `1` / `true`: disable hybrid crypto (plaintext `sendCodeV2`) |
|
|
82
97
|
| `CYY_ENCRYPT_DEBUG` | `1` / `true`: print RSA/AES diagnostics for hybrid decrypt failures |
|
|
83
98
|
| `CYY_BOOTSTRAP_TOKEN` | Optional - **only before login**, gateway may expect placeholder token |
|
|
@@ -92,4 +107,4 @@ cyy serve --port 8787
|
|
|
92
107
|
|
|
93
108
|
## Implementation phases
|
|
94
109
|
|
|
95
|
-
See [`../README_CLI.md`](../README_CLI.md) for staged rollout and acceptance notes.
|
|
110
|
+
See [`../README_CLI.md`](../README_CLI.md) for staged rollout and acceptance notes.
|
package/package.json
CHANGED
package/src/Untitled
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shop
|
package/src/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require("path");
|
|
|
4
4
|
const pkg = require(path.join(__dirname, "..", "package.json"));
|
|
5
5
|
|
|
6
6
|
const config = require("./config");
|
|
7
|
+
const http = require("./http");
|
|
7
8
|
const apiCall = require("./commands/apiCall");
|
|
8
9
|
const auth = require("./commands/auth");
|
|
9
10
|
const product = require("./commands/product");
|
|
@@ -31,14 +32,26 @@ cfgCmd
|
|
|
31
32
|
|
|
32
33
|
cfgCmd
|
|
33
34
|
.command("show")
|
|
34
|
-
.description("Show config (token masked)")
|
|
35
|
+
.description("Show config (token masked); includes effective session and env overrides")
|
|
35
36
|
.action(() => {
|
|
36
|
-
const
|
|
37
|
-
|
|
37
|
+
const file = config.loadConfig();
|
|
38
|
+
const session = config.getSession();
|
|
39
|
+
const envOverrides = config.getEnvSessionOverrideKeys();
|
|
40
|
+
if (!config.hasAuthToken(session) && !file) {
|
|
38
41
|
console.log(JSON.stringify({ loggedIn: false }, null, 2));
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
|
-
const masked = {
|
|
44
|
+
const masked = {
|
|
45
|
+
loggedIn: config.hasAuthToken(session),
|
|
46
|
+
saved: file
|
|
47
|
+
? { ...file, token: config.maskToken(String(file.token || "")) }
|
|
48
|
+
: null,
|
|
49
|
+
effective: {
|
|
50
|
+
...session,
|
|
51
|
+
token: config.maskToken(String(session.token || "")),
|
|
52
|
+
},
|
|
53
|
+
envOverrides: envOverrides.length ? envOverrides : undefined,
|
|
54
|
+
};
|
|
42
55
|
console.log(JSON.stringify(masked, null, 2));
|
|
43
56
|
});
|
|
44
57
|
|
|
@@ -92,6 +105,32 @@ authCmd
|
|
|
92
105
|
await auth.whoami();
|
|
93
106
|
});
|
|
94
107
|
|
|
108
|
+
authCmd
|
|
109
|
+
.command("import")
|
|
110
|
+
.description(
|
|
111
|
+
"Import external session token into ~/.cyymall/config.json (skip SMS login); optional --no-verify",
|
|
112
|
+
)
|
|
113
|
+
.option("--token <t>", "App session token (appToken / Authorization value)")
|
|
114
|
+
.option("--token-file <file>", "Read token from UTF-8 file (e.g. secret mount)")
|
|
115
|
+
.option("--member-id <id>", "memberId header")
|
|
116
|
+
.option("--shop-id <id>", "shopId header")
|
|
117
|
+
.option("--site-id <id>", "siteId header (default 1 if omitted and not in saved config)")
|
|
118
|
+
.option("--version-code <n>", "version-code header")
|
|
119
|
+
.option("--phone <p>", "Optional phone label for config display")
|
|
120
|
+
.option("--no-verify", "Save without calling GET /member/getInfo/V2")
|
|
121
|
+
.action(async (opts) => {
|
|
122
|
+
await auth.importSession({
|
|
123
|
+
token: opts.token,
|
|
124
|
+
tokenFile: opts.tokenFile,
|
|
125
|
+
memberId: opts.memberId,
|
|
126
|
+
shopId: opts.shopId,
|
|
127
|
+
siteId: opts.siteId,
|
|
128
|
+
versionCode: opts.versionCode,
|
|
129
|
+
phone: opts.phone,
|
|
130
|
+
noVerify: Boolean(opts.noVerify),
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
95
134
|
const prod = program.command("product").description("Product helpers");
|
|
96
135
|
|
|
97
136
|
prod
|
package/src/commands/apiCall.js
CHANGED
package/src/commands/auth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
+
const fs = require("fs");
|
|
3
4
|
const crypto = require("crypto");
|
|
4
5
|
const readline = require("readline/promises");
|
|
5
6
|
const { stdin: input, stdout: output } = require("process");
|
|
@@ -9,15 +10,15 @@ const biz = require("../biz");
|
|
|
9
10
|
const encrypt = require("../encrypt");
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* `/app/auth/ua
|
|
13
|
-
*
|
|
13
|
+
* `/app/auth/ua/sendCodeV2` responses may still be returned as a hybrid envelope.
|
|
14
|
+
* Decrypt the raw wire body first, then parse JSON, matching the App pipeline.
|
|
14
15
|
*
|
|
15
16
|
* @param {string} [rawWireText]
|
|
16
17
|
* @param {unknown} jsonAfterParse
|
|
17
|
-
* @param {string} clientPkForDecrypt
|
|
18
18
|
* @returns {unknown}
|
|
19
19
|
*/
|
|
20
|
-
function plainBizJsonFromSendCodeWire(rawWireText, jsonAfterParse
|
|
20
|
+
function plainBizJsonFromSendCodeWire(rawWireText, jsonAfterParse) {
|
|
21
|
+
const { clientPrivateKey } = encrypt.resolveEncryptKeys();
|
|
21
22
|
let wire =
|
|
22
23
|
typeof rawWireText === "string" && rawWireText.trim()
|
|
23
24
|
? rawWireText.trim()
|
|
@@ -44,26 +45,26 @@ function plainBizJsonFromSendCodeWire(rawWireText, jsonAfterParse, clientPkForDe
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
if (wire) {
|
|
48
|
-
const decryptedText = encrypt.decryptResponse(wire,
|
|
48
|
+
if (wire && clientPrivateKey) {
|
|
49
|
+
const decryptedText = encrypt.decryptResponse(wire, clientPrivateKey);
|
|
49
50
|
try {
|
|
50
51
|
const obj = decryptedText ? JSON.parse(decryptedText) : null;
|
|
51
52
|
if (obj != null && typeof obj === "object" && !Array.isArray(obj)) {
|
|
52
53
|
const out = encrypt.looksLikeHybridEnvelope(obj)
|
|
53
|
-
? encrypt.parsedJsonAfterHybridEnvelopeDecrypt(obj,
|
|
54
|
+
? encrypt.parsedJsonAfterHybridEnvelopeDecrypt(obj, clientPrivateKey)
|
|
54
55
|
: obj;
|
|
55
56
|
if (!encrypt.looksLikeHybridEnvelope(out)) {
|
|
56
57
|
return out;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
} catch {
|
|
60
|
-
// fall through to parsed
|
|
61
|
+
// fall through to parsed fallback
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
if (jsonAfterParse != null && typeof jsonAfterParse === "object" && !Array.isArray(jsonAfterParse)) {
|
|
65
|
-
return encrypt.looksLikeHybridEnvelope(jsonAfterParse)
|
|
66
|
-
? encrypt.parsedJsonAfterHybridEnvelopeDecrypt(jsonAfterParse,
|
|
66
|
+
return clientPrivateKey && encrypt.looksLikeHybridEnvelope(jsonAfterParse)
|
|
67
|
+
? encrypt.parsedJsonAfterHybridEnvelopeDecrypt(jsonAfterParse, clientPrivateKey)
|
|
67
68
|
: jsonAfterParse;
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -91,31 +92,24 @@ function extractBizMessage(json, fallback) {
|
|
|
91
92
|
: fallback;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
function
|
|
95
|
+
function buildEncryptedSendCodeBody(phone) {
|
|
96
|
+
const headers = /** @type {Record<string,string>} */ (http.buildDefaultHeaders());
|
|
97
|
+
const plainBody = JSON.stringify({ phone, objectCode: "3" });
|
|
95
98
|
if (!encrypt.encryptEnvConfigured()) {
|
|
96
|
-
return plainBody;
|
|
99
|
+
return { headers, body: plainBody };
|
|
97
100
|
}
|
|
98
101
|
headers.encrypte = "true";
|
|
99
102
|
const { serverPublicKey } = encrypt.resolveEncryptKeys();
|
|
100
103
|
const hy = encrypt.hybridEncrypt(plainBody, serverPublicKey);
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const { clientPrivateKey } = encrypt.resolveEncryptKeys();
|
|
106
|
-
if (!clientPrivateKey) return jsonAfterParse;
|
|
107
|
-
return plainBizJsonFromSendCodeWire(
|
|
108
|
-
typeof rawWireText === "string" ? rawWireText : "",
|
|
109
|
-
jsonAfterParse,
|
|
110
|
-
clientPrivateKey,
|
|
111
|
-
);
|
|
104
|
+
return {
|
|
105
|
+
headers,
|
|
106
|
+
body: encrypt.hybridEncryptResultToJson(hy),
|
|
107
|
+
};
|
|
112
108
|
}
|
|
113
109
|
|
|
114
110
|
async function requestSmsCode(phone) {
|
|
115
111
|
const url = http.moduleUrl("DEFAULT", "/app/auth/ua/sendCodeV2");
|
|
116
|
-
const headers
|
|
117
|
-
const plainBody = JSON.stringify({ phone, objectCode: "3" });
|
|
118
|
-
const body = buildHybridAuthBody(plainBody, headers);
|
|
112
|
+
const { headers, body } = buildEncryptedSendCodeBody(phone);
|
|
119
113
|
|
|
120
114
|
const { ok, status, json: jsonAfterParse, rawWireText } = await http.request(url, {
|
|
121
115
|
method: "POST",
|
|
@@ -125,7 +119,7 @@ async function requestSmsCode(phone) {
|
|
|
125
119
|
includeRawWireText: true,
|
|
126
120
|
});
|
|
127
121
|
|
|
128
|
-
const json =
|
|
122
|
+
const json = plainBizJsonFromSendCodeWire(rawWireText, jsonAfterParse);
|
|
129
123
|
|
|
130
124
|
if (!ok) {
|
|
131
125
|
const msg = extractBizMessage(json, `send login code failed (HTTP ${status})`);
|
|
@@ -134,26 +128,24 @@ async function requestSmsCode(phone) {
|
|
|
134
128
|
process.exit(2);
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
// decrypted into upstream business JSON.
|
|
140
|
-
return { status, json, acceptedByGateway: true };
|
|
131
|
+
// Only sendCodeV2 is encrypted. Once transport succeeds, treat the SMS request as accepted.
|
|
132
|
+
return { status, json, gatewayAccepted: true };
|
|
141
133
|
}
|
|
142
134
|
|
|
143
135
|
async function sendCode(phone) {
|
|
144
|
-
const { status, json,
|
|
136
|
+
const { status, json, gatewayAccepted } = await requestSmsCode(phone);
|
|
145
137
|
const traceId = buildTraceId();
|
|
146
138
|
console.log(
|
|
147
139
|
JSON.stringify(
|
|
148
140
|
{
|
|
149
141
|
success: true,
|
|
150
142
|
code: "OK",
|
|
151
|
-
message:
|
|
143
|
+
message: "code sent",
|
|
152
144
|
data: {
|
|
153
145
|
phone,
|
|
154
146
|
objectCode: "3",
|
|
155
147
|
httpStatus: status,
|
|
156
|
-
gatewayAccepted
|
|
148
|
+
gatewayAccepted,
|
|
157
149
|
upstream: json,
|
|
158
150
|
},
|
|
159
151
|
traceId,
|
|
@@ -168,7 +160,7 @@ async function sendCode(phone) {
|
|
|
168
160
|
async function loginWithCode(phone, code) {
|
|
169
161
|
const url = http.moduleUrl("DEFAULT", "/app/auth/ua/registerMember/v2");
|
|
170
162
|
const headers = /** @type {Record<string,string>} */ (http.buildDefaultHeaders());
|
|
171
|
-
const
|
|
163
|
+
const body = JSON.stringify({
|
|
172
164
|
phone,
|
|
173
165
|
objectCode: "3",
|
|
174
166
|
password: "",
|
|
@@ -177,18 +169,13 @@ async function loginWithCode(phone, code) {
|
|
|
177
169
|
smsCode: code,
|
|
178
170
|
wxOpenid: "",
|
|
179
171
|
});
|
|
180
|
-
const body = buildHybridAuthBody(plainBody, headers);
|
|
181
172
|
|
|
182
|
-
const { ok, json
|
|
173
|
+
const { ok, json } = await http.request(url, {
|
|
183
174
|
method: "POST",
|
|
184
175
|
headers,
|
|
185
176
|
body,
|
|
186
|
-
decryptHybridResponse: false,
|
|
187
|
-
includeRawWireText: true,
|
|
188
177
|
});
|
|
189
178
|
|
|
190
|
-
const json = decryptAuthBizJson(rawWireText, jsonAfterParse);
|
|
191
|
-
|
|
192
179
|
if (!ok || !biz.isBizSuccess(json)) {
|
|
193
180
|
const msg = extractBizMessage(json, "login failed");
|
|
194
181
|
console.error(`cyy: ${msg}`);
|
|
@@ -202,8 +189,8 @@ async function loginWithCode(phone, code) {
|
|
|
202
189
|
|
|
203
190
|
const loginId = String(data.loginId || "");
|
|
204
191
|
const memberId = loginId.includes("_") ? loginId.split("_")[0] : loginId;
|
|
205
|
-
|
|
206
192
|
const token = pickLoginToken(data);
|
|
193
|
+
|
|
207
194
|
if (!token) {
|
|
208
195
|
console.error(
|
|
209
196
|
"cyy: login reported success but data has no token (expected token, appToken, or accessToken)",
|
|
@@ -272,12 +259,89 @@ async function login(phone, code) {
|
|
|
272
259
|
await loginWithCode(phone, smsCode);
|
|
273
260
|
}
|
|
274
261
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Persist an externally supplied session (Native App / Gateway token), skipping SMS login.
|
|
264
|
+
* @param {object} opts
|
|
265
|
+
* @param {string} [opts.token]
|
|
266
|
+
* @param {string} [opts.tokenFile]
|
|
267
|
+
* @param {string} [opts.memberId]
|
|
268
|
+
* @param {string} [opts.shopId]
|
|
269
|
+
* @param {string} [opts.siteId]
|
|
270
|
+
* @param {string} [opts.versionCode]
|
|
271
|
+
* @param {string} [opts.phone]
|
|
272
|
+
* @param {boolean} [opts.noVerify]
|
|
273
|
+
*/
|
|
274
|
+
async function importSession(opts) {
|
|
275
|
+
let token = opts.token != null ? String(opts.token).trim() : "";
|
|
276
|
+
if (!token && opts.tokenFile) {
|
|
277
|
+
token = fs.readFileSync(opts.tokenFile, "utf8").trim();
|
|
278
|
+
}
|
|
279
|
+
if (!token) {
|
|
280
|
+
console.error("cyy: --token or --token-file is required");
|
|
279
281
|
process.exit(1);
|
|
280
282
|
}
|
|
283
|
+
|
|
284
|
+
const prev = config.loadConfig() || {};
|
|
285
|
+
const saved = {
|
|
286
|
+
...prev,
|
|
287
|
+
token,
|
|
288
|
+
member_id:
|
|
289
|
+
opts.memberId != null && String(opts.memberId).trim() !== ""
|
|
290
|
+
? String(opts.memberId).trim()
|
|
291
|
+
: String(prev.member_id || ""),
|
|
292
|
+
shop_id:
|
|
293
|
+
opts.shopId != null && String(opts.shopId).trim() !== ""
|
|
294
|
+
? String(opts.shopId).trim()
|
|
295
|
+
: String(prev.shop_id || ""),
|
|
296
|
+
site_id:
|
|
297
|
+
opts.siteId != null && String(opts.siteId).trim() !== ""
|
|
298
|
+
? String(opts.siteId).trim()
|
|
299
|
+
: String(prev.site_id || "1"),
|
|
300
|
+
version_code: String(
|
|
301
|
+
opts.versionCode != null && String(opts.versionCode).trim() !== ""
|
|
302
|
+
? opts.versionCode
|
|
303
|
+
: prev.version_code ?? http.DEFAULT_VERSION,
|
|
304
|
+
),
|
|
305
|
+
phone:
|
|
306
|
+
opts.phone != null && String(opts.phone).trim() !== ""
|
|
307
|
+
? String(opts.phone).trim()
|
|
308
|
+
: String(prev.phone || ""),
|
|
309
|
+
login_time: Math.floor(Date.now() / 1000),
|
|
310
|
+
session_source: "import",
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
config.saveConfig(saved);
|
|
314
|
+
|
|
315
|
+
if (opts.noVerify) {
|
|
316
|
+
const traceId = buildTraceId();
|
|
317
|
+
console.log(
|
|
318
|
+
JSON.stringify(
|
|
319
|
+
{
|
|
320
|
+
success: true,
|
|
321
|
+
code: "OK",
|
|
322
|
+
message: "session imported (not verified)",
|
|
323
|
+
data: {
|
|
324
|
+
phone: saved.phone,
|
|
325
|
+
member_id: saved.member_id,
|
|
326
|
+
shop_id: saved.shop_id,
|
|
327
|
+
site_id: saved.site_id,
|
|
328
|
+
version_code: saved.version_code,
|
|
329
|
+
token_preview: config.maskToken(saved.token),
|
|
330
|
+
},
|
|
331
|
+
traceId,
|
|
332
|
+
},
|
|
333
|
+
null,
|
|
334
|
+
2,
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
process.exit(0);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
await whoami();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function whoami() {
|
|
344
|
+
const cfg = config.requireAuthSession();
|
|
281
345
|
const url = http.moduleUrl("DEFAULT", "/member/getInfo/V2");
|
|
282
346
|
const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
|
|
283
347
|
delete headers["Content-Type"];
|
|
@@ -305,4 +369,4 @@ async function whoami() {
|
|
|
305
369
|
process.exit(success ? 0 : 2);
|
|
306
370
|
}
|
|
307
371
|
|
|
308
|
-
module.exports = { login, loginWithCode, sendCode, whoami };
|
|
372
|
+
module.exports = { login, loginWithCode, sendCode, whoami, importSession };
|
package/src/commands/cart.js
CHANGED
|
@@ -10,11 +10,7 @@ const biz = require("../biz");
|
|
|
10
10
|
* @param {{ bodyFile?: string, bodyJson?: string }} opts
|
|
11
11
|
*/
|
|
12
12
|
async function add(opts) {
|
|
13
|
-
const cfg = config.
|
|
14
|
-
if (!cfg?.token) {
|
|
15
|
-
console.error("cyy: not logged in.");
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
13
|
+
const cfg = config.requireAuthSession();
|
|
18
14
|
|
|
19
15
|
let raw = opts.bodyJson;
|
|
20
16
|
if (opts.bodyFile) {
|
package/src/commands/order.js
CHANGED
|
@@ -28,11 +28,7 @@ function envelope(success, message, data, exitCode) {
|
|
|
28
28
|
* @param {{ bodyFile?: string, bodyJson?: string }} opts
|
|
29
29
|
*/
|
|
30
30
|
async function preSettle(opts) {
|
|
31
|
-
const cfg = config.
|
|
32
|
-
if (!cfg?.token) {
|
|
33
|
-
console.error("cyy: not logged in.");
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
31
|
+
const cfg = config.requireAuthSession();
|
|
36
32
|
let raw = opts.bodyJson;
|
|
37
33
|
if (opts.bodyFile) raw = fs.readFileSync(opts.bodyFile, "utf8");
|
|
38
34
|
if (!raw) {
|
|
@@ -54,11 +50,7 @@ async function preSettle(opts) {
|
|
|
54
50
|
* @param {{ bodyFile?: string, bodyJson?: string }} opts
|
|
55
51
|
*/
|
|
56
52
|
async function confirm(opts) {
|
|
57
|
-
const cfg = config.
|
|
58
|
-
if (!cfg?.token) {
|
|
59
|
-
console.error("cyy: not logged in.");
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
53
|
+
const cfg = config.requireAuthSession();
|
|
62
54
|
let raw = opts.bodyJson;
|
|
63
55
|
if (opts.bodyFile) raw = fs.readFileSync(opts.bodyFile, "utf8");
|
|
64
56
|
if (!raw) {
|
|
@@ -77,11 +69,7 @@ async function confirm(opts) {
|
|
|
77
69
|
}
|
|
78
70
|
|
|
79
71
|
async function payUrl(opts) {
|
|
80
|
-
const cfg = config.
|
|
81
|
-
if (!cfg?.token) {
|
|
82
|
-
console.error("cyy: not logged in.");
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
72
|
+
const cfg = config.requireAuthSession();
|
|
85
73
|
const orderId = opts.orderId;
|
|
86
74
|
if (!orderId) {
|
|
87
75
|
console.error("cyy: --order-id required");
|
|
@@ -118,11 +106,7 @@ async function payUrl(opts) {
|
|
|
118
106
|
* @param {object} opts
|
|
119
107
|
*/
|
|
120
108
|
async function quick(opts) {
|
|
121
|
-
const cfg = config.
|
|
122
|
-
if (!cfg?.token) {
|
|
123
|
-
console.error("cyy: not logged in.");
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
109
|
+
const cfg = config.requireAuthSession();
|
|
126
110
|
|
|
127
111
|
const keyword = opts.keyword;
|
|
128
112
|
const quantity = Number(opts.quantity || 1);
|
|
@@ -329,11 +313,7 @@ async function quick(opts) {
|
|
|
329
313
|
* @param {{ page?: string, pageSize?: string, status?: string, shopId?: string, objectCode?: string, all?: boolean, shopKeyword?: string }} opts
|
|
330
314
|
*/
|
|
331
315
|
async function list(opts) {
|
|
332
|
-
const cfg = config.
|
|
333
|
-
if (!cfg?.token) {
|
|
334
|
-
console.error("cyy: not logged in.");
|
|
335
|
-
process.exit(1);
|
|
336
|
-
}
|
|
316
|
+
const cfg = config.requireAuthSession();
|
|
337
317
|
const shopId = opts.shopId != null && opts.shopId !== "" ? Number(opts.shopId) : Number(cfg.shop_id);
|
|
338
318
|
if (!Number.isFinite(shopId)) {
|
|
339
319
|
console.error("cyy: shopId missing (set shop_id in config or pass --shop-id)");
|
|
@@ -371,11 +351,7 @@ async function list(opts) {
|
|
|
371
351
|
* @param {{ orderId?: string, childId?: string }} opts
|
|
372
352
|
*/
|
|
373
353
|
async function cancel(opts) {
|
|
374
|
-
const cfg = config.
|
|
375
|
-
if (!cfg?.token) {
|
|
376
|
-
console.error("cyy: not logged in.");
|
|
377
|
-
process.exit(1);
|
|
378
|
-
}
|
|
354
|
+
const cfg = config.requireAuthSession();
|
|
379
355
|
const orderId = opts.orderId;
|
|
380
356
|
if (!orderId) {
|
|
381
357
|
console.error("cyy: --order-id required");
|
package/src/commands/product.js
CHANGED
|
@@ -9,11 +9,7 @@ const biz = require("../biz");
|
|
|
9
9
|
* @param {object} opts
|
|
10
10
|
*/
|
|
11
11
|
async function search(opts) {
|
|
12
|
-
const cfg = config.
|
|
13
|
-
if (!cfg?.token) {
|
|
14
|
-
console.error("cyy: not logged in. Run: cyy auth login");
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
12
|
+
const cfg = config.requireAuthSession();
|
|
17
13
|
|
|
18
14
|
const shopId = Number(opts.shopId ?? cfg.shop_id);
|
|
19
15
|
const siteId = Number(opts.siteId ?? cfg.site_id);
|
package/src/commands/shop.js
CHANGED
|
@@ -28,11 +28,7 @@ function envelope(success, message, upstream, exitCode) {
|
|
|
28
28
|
* @param {{ page?: string, pageSize?: string, name?: string, objectCode?: string }} opts
|
|
29
29
|
*/
|
|
30
30
|
async function list(opts) {
|
|
31
|
-
const cfg = config.
|
|
32
|
-
if (!cfg?.token) {
|
|
33
|
-
console.error("cyy: not logged in. Run: cyy auth login");
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
31
|
+
const cfg = config.requireAuthSession();
|
|
36
32
|
|
|
37
33
|
const body = JSON.stringify({
|
|
38
34
|
pageNum: Number(opts.page || 1),
|
|
@@ -62,11 +58,7 @@ async function list(opts) {
|
|
|
62
58
|
* @param {{ shopId?: string, siteName?: string }} opts
|
|
63
59
|
*/
|
|
64
60
|
async function sites(opts) {
|
|
65
|
-
const cfg = config.
|
|
66
|
-
if (!cfg?.token) {
|
|
67
|
-
console.error("cyy: not logged in. Run: cyy auth login");
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
61
|
+
const cfg = config.requireAuthSession();
|
|
70
62
|
|
|
71
63
|
const shopId = opts.shopId ?? cfg.shop_id;
|
|
72
64
|
if (shopId === undefined || shopId === null || String(shopId).trim() === "") {
|
|
@@ -179,11 +171,7 @@ async function shopIdBelongsToMember(cfg, shopId) {
|
|
|
179
171
|
* @param {{ shopId?: string }} opts
|
|
180
172
|
*/
|
|
181
173
|
async function useShop(opts) {
|
|
182
|
-
const cfg = config.
|
|
183
|
-
if (!cfg?.token) {
|
|
184
|
-
console.error("cyy: not logged in. Run: cyy auth login");
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
174
|
+
const cfg = config.requireAuthSession();
|
|
187
175
|
const shopId = opts.shopId;
|
|
188
176
|
if (shopId === undefined || shopId === null || String(shopId).trim() === "") {
|
|
189
177
|
console.error("cyy: shop use requires --shop-id");
|
|
@@ -229,11 +217,7 @@ async function useShop(opts) {
|
|
|
229
217
|
* @param {{ siteId?: string }} opts
|
|
230
218
|
*/
|
|
231
219
|
async function useSite(opts) {
|
|
232
|
-
const cfg = config.
|
|
233
|
-
if (!cfg?.token) {
|
|
234
|
-
console.error("cyy: not logged in. Run: cyy auth login");
|
|
235
|
-
process.exit(1);
|
|
236
|
-
}
|
|
220
|
+
const cfg = config.requireAuthSession();
|
|
237
221
|
const shopId = cfg.shop_id;
|
|
238
222
|
if (shopId === undefined || shopId === null || String(shopId).trim() === "") {
|
|
239
223
|
console.error("cyy: no shop_id in session; run: cyy shop use --shop-id <id>");
|
package/src/config.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const os = require("os");
|
|
6
6
|
|
|
7
|
-
const DEFAULT_BASE_URL = "https://
|
|
7
|
+
const DEFAULT_BASE_URL = "https://yunping.ifoodbuy.com/";
|
|
8
8
|
|
|
9
9
|
/** BuildConfig-style module prefixes (app-api-cli-spec §1.2) */
|
|
10
10
|
const MODULE_MAP = {
|
|
@@ -69,9 +69,71 @@ function maskToken(t) {
|
|
|
69
69
|
return `${t.slice(0, 8)}...${t.slice(-4)}`;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/** Env keys that override ~/.cyymall/config.json for the current process (ephemeral). */
|
|
73
|
+
const SESSION_ENV_KEYS = [
|
|
74
|
+
["token", "CYY_TOKEN"],
|
|
75
|
+
["member_id", "CYY_MEMBER_ID"],
|
|
76
|
+
["shop_id", "CYY_SHOP_ID"],
|
|
77
|
+
["site_id", "CYY_SITE_ID"],
|
|
78
|
+
["version_code", "CYY_VERSION_CODE"],
|
|
79
|
+
["phone", "CYY_PHONE"],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Effective session: saved config merged with CYY_* env overrides (env wins when set).
|
|
84
|
+
* @returns {Record<string, unknown>}
|
|
85
|
+
*/
|
|
86
|
+
function getSession() {
|
|
87
|
+
const file = loadConfig() || {};
|
|
88
|
+
/** @type {Record<string, unknown>} */
|
|
89
|
+
const merged = { ...file };
|
|
90
|
+
for (const [field, envKey] of SESSION_ENV_KEYS) {
|
|
91
|
+
const v = process.env[envKey];
|
|
92
|
+
if (v != null && String(v).trim() !== "") {
|
|
93
|
+
merged[field] = String(v).trim();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return merged;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {Record<string, unknown>|null|undefined} [session]
|
|
101
|
+
*/
|
|
102
|
+
function hasAuthToken(session) {
|
|
103
|
+
const s = session ?? getSession();
|
|
104
|
+
const t = s?.token;
|
|
105
|
+
return t != null && String(t).trim() !== "";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @returns {Record<string, unknown>}
|
|
110
|
+
*/
|
|
111
|
+
function requireAuthSession() {
|
|
112
|
+
if (!hasAuthToken()) {
|
|
113
|
+
console.error(
|
|
114
|
+
"cyy: not logged in. Use one of: cyy auth login --phone ... | cyy auth import --token ... | CYY_TOKEN=...",
|
|
115
|
+
);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
return getSession();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Names of CYY_* env vars currently overriding saved config. */
|
|
122
|
+
function getEnvSessionOverrideKeys() {
|
|
123
|
+
const active = [];
|
|
124
|
+
for (const [, envKey] of SESSION_ENV_KEYS) {
|
|
125
|
+
const v = process.env[envKey];
|
|
126
|
+
if (v != null && String(v).trim() !== "") {
|
|
127
|
+
active.push(envKey);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return active;
|
|
131
|
+
}
|
|
132
|
+
|
|
72
133
|
module.exports = {
|
|
73
134
|
DEFAULT_BASE_URL,
|
|
74
135
|
MODULE_MAP,
|
|
136
|
+
SESSION_ENV_KEYS,
|
|
75
137
|
getBaseUrl,
|
|
76
138
|
getConfigDir,
|
|
77
139
|
getConfigPath,
|
|
@@ -79,4 +141,8 @@ module.exports = {
|
|
|
79
141
|
loadConfig,
|
|
80
142
|
saveConfig,
|
|
81
143
|
maskToken,
|
|
144
|
+
getSession,
|
|
145
|
+
hasAuthToken,
|
|
146
|
+
requireAuthSession,
|
|
147
|
+
getEnvSessionOverrideKeys,
|
|
82
148
|
};
|
package/src/http.js
CHANGED
|
@@ -46,8 +46,7 @@ function buildAuthHeaders(cfg) {
|
|
|
46
46
|
|
|
47
47
|
/** Headers for unauthenticated calls (e.g. login) — still need default shop/site for gateway */
|
|
48
48
|
function buildDefaultHeaders() {
|
|
49
|
-
|
|
50
|
-
return buildAuthHeaders(cfg);
|
|
49
|
+
return buildAuthHeaders(config.getSession());
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
/**
|