@huoye/app-ship 0.2.0 → 0.3.0
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 +172 -139
- package/browser-extension/capture-fields.js +195 -0
- package/browser-extension/manifest.json +8 -0
- package/browser-extension/popup.html +152 -0
- package/browser-extension/popup.js +312 -0
- package/dist/browser-native.js +2031 -0
- package/dist/chunk-PUSXETML.js +21041 -0
- package/dist/cli.js +4936 -1632
- package/dist/index.cjs +7457 -4068
- package/dist/index.d.cts +1309 -531
- package/dist/index.d.ts +1309 -531
- package/dist/index.js +7397 -4060
- package/dist/mcp-tools-PTJDFF2T.js +8703 -0
- package/dist/mcp.js +15 -0
- package/dist/stdio-BXBWA2I3.js +514 -0
- package/docs/browser-credentials.md +178 -0
- package/package.json +26 -8
- package/scripts/register-browser-host.mjs +110 -0
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
// src/browser-native.ts
|
|
2
|
+
import { lstat as lstat3, readFile as readFile2 } from "fs/promises";
|
|
3
|
+
import path3 from "path";
|
|
4
|
+
|
|
5
|
+
// src/credential-store.ts
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { chmod, lstat, mkdir, open, readFile, rename, unlink } from "fs/promises";
|
|
11
|
+
|
|
12
|
+
// src/release-contract.ts
|
|
13
|
+
var RELEASE_PLATFORM_CAPABILITIES = {
|
|
14
|
+
honor: {
|
|
15
|
+
platform: "honor",
|
|
16
|
+
channelKind: "store",
|
|
17
|
+
supportedActions: ["create_draft"],
|
|
18
|
+
defaultAction: "create_draft",
|
|
19
|
+
resultingState: "draft_created",
|
|
20
|
+
afterApproval: "not_applicable",
|
|
21
|
+
cancelUntil: "before_remote_request",
|
|
22
|
+
withdrawal: "not_verified",
|
|
23
|
+
riskLevel: "high",
|
|
24
|
+
automaticRetry: "disabled",
|
|
25
|
+
requiresExplicitConfirmation: true,
|
|
26
|
+
evidence: "official_documentation",
|
|
27
|
+
note: "\u7ED1\u5B9A\u5B89\u88C5\u5305\u5230\u8363\u8000\u8349\u7A3F\uFF0C\u4E0D\u63D0\u4EA4\u5BA1\u6838\u3002"
|
|
28
|
+
},
|
|
29
|
+
meizu: {
|
|
30
|
+
platform: "meizu",
|
|
31
|
+
channelKind: "store",
|
|
32
|
+
supportedActions: ["upload_build"],
|
|
33
|
+
defaultAction: "upload_build",
|
|
34
|
+
resultingState: "build_uploaded",
|
|
35
|
+
afterApproval: "not_applicable",
|
|
36
|
+
cancelUntil: "before_remote_request",
|
|
37
|
+
withdrawal: "not_verified",
|
|
38
|
+
riskLevel: "high",
|
|
39
|
+
automaticRetry: "disabled",
|
|
40
|
+
requiresExplicitConfirmation: true,
|
|
41
|
+
evidence: "official_documentation",
|
|
42
|
+
note: "\u4EC5\u6682\u5B58 APK \u6587\u4EF6\uFF0C\u4E0D\u7ED1\u5B9A\u7248\u672C\u3001\u4E0D\u9001\u5BA1\u6216\u53D1\u5E03\u3002"
|
|
43
|
+
},
|
|
44
|
+
samsung: {
|
|
45
|
+
platform: "samsung",
|
|
46
|
+
channelKind: "store",
|
|
47
|
+
supportedActions: ["create_draft"],
|
|
48
|
+
defaultAction: "create_draft",
|
|
49
|
+
resultingState: "draft_created",
|
|
50
|
+
afterApproval: "not_applicable",
|
|
51
|
+
cancelUntil: "before_remote_request",
|
|
52
|
+
withdrawal: "not_verified",
|
|
53
|
+
riskLevel: "high",
|
|
54
|
+
automaticRetry: "disabled",
|
|
55
|
+
requiresExplicitConfirmation: true,
|
|
56
|
+
evidence: "official_documentation",
|
|
57
|
+
note: "\u521B\u5EFA\u6216\u66F4\u65B0\u4E09\u661F\u8349\u7A3F\u5E76\u7ED1\u5B9A\u5B89\u88C5\u5305\uFF0C\u4E0D\u63D0\u4EA4\u5BA1\u6838\u6216\u4E0A\u7EBF\u3002"
|
|
58
|
+
},
|
|
59
|
+
pgyer: {
|
|
60
|
+
platform: "pgyer",
|
|
61
|
+
channelKind: "test_distribution",
|
|
62
|
+
supportedActions: ["distribute_test_build"],
|
|
63
|
+
defaultAction: "distribute_test_build",
|
|
64
|
+
resultingState: "test_build_distributed",
|
|
65
|
+
afterApproval: "not_applicable",
|
|
66
|
+
cancelUntil: "before_distribution_created",
|
|
67
|
+
withdrawal: "console_only",
|
|
68
|
+
riskLevel: "high",
|
|
69
|
+
automaticRetry: "disabled",
|
|
70
|
+
requiresExplicitConfirmation: true,
|
|
71
|
+
evidence: "official_and_code",
|
|
72
|
+
note: "\u4E0A\u4F20\u5B8C\u6210\u5373\u5F62\u6210\u5B89\u88C5\u94FE\u63A5\uFF0C\u5FC5\u987B\u663E\u5F0F\u9009\u62E9\u516C\u5F00\u3001\u5BC6\u7801\u6216\u9080\u8BF7\u5B89\u88C5\u3002"
|
|
73
|
+
},
|
|
74
|
+
huawei: {
|
|
75
|
+
platform: "huawei",
|
|
76
|
+
channelKind: "store",
|
|
77
|
+
supportedActions: ["create_draft", "submit_review"],
|
|
78
|
+
defaultAction: "create_draft",
|
|
79
|
+
resultingState: "draft_created",
|
|
80
|
+
afterApproval: "platform_controlled",
|
|
81
|
+
cancelUntil: "while_remote_draft_exists",
|
|
82
|
+
withdrawal: "not_verified",
|
|
83
|
+
riskLevel: "high",
|
|
84
|
+
automaticRetry: "disabled",
|
|
85
|
+
requiresExplicitConfirmation: true,
|
|
86
|
+
evidence: "official_and_code",
|
|
87
|
+
note: "\u8349\u7A3F\u4E0A\u4F20\u4E0E\u9001\u5BA1\u662F\u4E0D\u540C\u52A8\u4F5C\uFF1B\u9001\u5BA1\u5FC5\u987B\u5355\u72EC\u786E\u8BA4\u3002"
|
|
88
|
+
},
|
|
89
|
+
harmony: {
|
|
90
|
+
platform: "harmony",
|
|
91
|
+
channelKind: "store",
|
|
92
|
+
supportedActions: ["create_draft"],
|
|
93
|
+
defaultAction: "create_draft",
|
|
94
|
+
resultingState: "draft_created",
|
|
95
|
+
afterApproval: "not_applicable",
|
|
96
|
+
cancelUntil: "while_remote_draft_exists",
|
|
97
|
+
withdrawal: "not_applicable",
|
|
98
|
+
riskLevel: "medium",
|
|
99
|
+
automaticRetry: "allowed_for_pre_submission_steps",
|
|
100
|
+
requiresExplicitConfirmation: true,
|
|
101
|
+
evidence: "code_observation",
|
|
102
|
+
note: "\u5F53\u524D submit \u4EE3\u7801\u672A\u5B9E\u73B0\uFF1B\u9A8C\u8BC1\u524D\u53EA\u5F00\u653E\u8349\u7A3F\u3002"
|
|
103
|
+
},
|
|
104
|
+
xiaomi: {
|
|
105
|
+
platform: "xiaomi",
|
|
106
|
+
channelKind: "store",
|
|
107
|
+
supportedActions: ["submit_review", "schedule_release_after_review"],
|
|
108
|
+
defaultAction: "submit_review",
|
|
109
|
+
resultingState: "review_submitted",
|
|
110
|
+
afterApproval: "platform_controlled",
|
|
111
|
+
cancelUntil: "platform_controlled",
|
|
112
|
+
withdrawal: "not_verified",
|
|
113
|
+
riskLevel: "critical",
|
|
114
|
+
automaticRetry: "disabled",
|
|
115
|
+
requiresExplicitConfirmation: true,
|
|
116
|
+
evidence: "official_and_code",
|
|
117
|
+
note: "/dev/push \u4F1A\u63D0\u4EA4\u5E94\u7528\uFF1B\u65E0\u6C99\u76D2\uFF0C\u9001\u5BA1\u6B65\u9AA4\u4E0D\u5F97\u81EA\u52A8\u91CD\u8BD5\u3002"
|
|
118
|
+
},
|
|
119
|
+
oppo: {
|
|
120
|
+
platform: "oppo",
|
|
121
|
+
channelKind: "store",
|
|
122
|
+
supportedActions: ["submit_review_auto_release"],
|
|
123
|
+
defaultAction: "submit_review_auto_release",
|
|
124
|
+
resultingState: "review_submitted_auto_release",
|
|
125
|
+
afterApproval: "auto_release",
|
|
126
|
+
cancelUntil: "not_verified",
|
|
127
|
+
withdrawal: "not_verified",
|
|
128
|
+
riskLevel: "critical",
|
|
129
|
+
automaticRetry: "disabled",
|
|
130
|
+
requiresExplicitConfirmation: true,
|
|
131
|
+
evidence: "code_observation",
|
|
132
|
+
note: "\u5F53\u524D\u9002\u914D\u5668\u9001\u5BA1\u4E14\u5BA1\u6838\u901A\u8FC7\u540E\u7ACB\u5373\u53D1\u5E03\uFF1B\u5B98\u65B9\u5B57\u6BB5\u7EA7\u80FD\u529B\u4ECD\u9700\u771F\u5B9E\u8D26\u53F7\u9A8C\u8BC1\u3002"
|
|
133
|
+
},
|
|
134
|
+
vivo: {
|
|
135
|
+
platform: "vivo",
|
|
136
|
+
channelKind: "store",
|
|
137
|
+
supportedActions: ["submit_review_auto_release"],
|
|
138
|
+
defaultAction: "submit_review_auto_release",
|
|
139
|
+
resultingState: "review_submitted_auto_release",
|
|
140
|
+
afterApproval: "auto_release",
|
|
141
|
+
cancelUntil: "not_verified",
|
|
142
|
+
withdrawal: "not_verified",
|
|
143
|
+
riskLevel: "critical",
|
|
144
|
+
automaticRetry: "disabled",
|
|
145
|
+
requiresExplicitConfirmation: true,
|
|
146
|
+
evidence: "code_observation",
|
|
147
|
+
note: "\u5F53\u524D\u9002\u914D\u5668\u5199\u6B7B onlineType=1\uFF1B\u5B98\u65B9\u5B57\u6BB5\u7EA7\u80FD\u529B\u4ECD\u9700\u771F\u5B9E\u8D26\u53F7\u9A8C\u8BC1\u3002"
|
|
148
|
+
},
|
|
149
|
+
tencent: {
|
|
150
|
+
platform: "tencent",
|
|
151
|
+
channelKind: "store",
|
|
152
|
+
supportedActions: ["submit_review_auto_release"],
|
|
153
|
+
defaultAction: "submit_review_auto_release",
|
|
154
|
+
resultingState: "review_submitted_auto_release",
|
|
155
|
+
afterApproval: "auto_release",
|
|
156
|
+
cancelUntil: "not_verified",
|
|
157
|
+
withdrawal: "not_verified",
|
|
158
|
+
riskLevel: "critical",
|
|
159
|
+
automaticRetry: "disabled",
|
|
160
|
+
requiresExplicitConfirmation: true,
|
|
161
|
+
evidence: "code_observation",
|
|
162
|
+
note: "\u5F53\u524D\u9002\u914D\u5668\u9ED8\u8BA4 deploy_type=1\uFF1B\u5B98\u65B9\u5B57\u6BB5\u7EA7\u80FD\u529B\u4ECD\u9700\u771F\u5B9E\u8D26\u53F7\u9A8C\u8BC1\u3002"
|
|
163
|
+
},
|
|
164
|
+
app_store: {
|
|
165
|
+
platform: "app_store",
|
|
166
|
+
channelKind: "store",
|
|
167
|
+
supportedActions: ["upload_build"],
|
|
168
|
+
defaultAction: "upload_build",
|
|
169
|
+
resultingState: "build_uploaded",
|
|
170
|
+
afterApproval: "not_applicable",
|
|
171
|
+
cancelUntil: "before_remote_request",
|
|
172
|
+
withdrawal: "not_applicable",
|
|
173
|
+
riskLevel: "medium",
|
|
174
|
+
automaticRetry: "allowed_for_pre_submission_steps",
|
|
175
|
+
requiresExplicitConfirmation: true,
|
|
176
|
+
evidence: "official_and_code",
|
|
177
|
+
note: "\u5F53\u524D\u53EA\u4E0A\u4F20\u6784\u5EFA\uFF0C\u4E0D\u9009\u62E9\u6784\u5EFA\u3001\u4E0D\u9001\u5BA1\u3002"
|
|
178
|
+
},
|
|
179
|
+
google_play: {
|
|
180
|
+
platform: "google_play",
|
|
181
|
+
channelKind: "store",
|
|
182
|
+
supportedActions: ["create_draft"],
|
|
183
|
+
defaultAction: "create_draft",
|
|
184
|
+
resultingState: "draft_created",
|
|
185
|
+
afterApproval: "not_applicable",
|
|
186
|
+
cancelUntil: "while_remote_draft_exists",
|
|
187
|
+
withdrawal: "not_applicable",
|
|
188
|
+
riskLevel: "medium",
|
|
189
|
+
automaticRetry: "allowed_for_pre_submission_steps",
|
|
190
|
+
requiresExplicitConfirmation: true,
|
|
191
|
+
evidence: "official_and_code",
|
|
192
|
+
note: "\u4EC5\u4FDD\u5B58 internal \u6216 production \u8349\u7A3F\uFF0C\u4E0D\u9001\u5BA1\u3001\u4E0D\u5411\u6D4B\u8BD5\u7528\u6237\u6216\u751F\u4EA7\u7528\u6237\u5F00\u653E\u3002"
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// src/credentials.ts
|
|
197
|
+
function assertSafeCredentialMetadata(metadata) {
|
|
198
|
+
const keys = Object.keys(metadata);
|
|
199
|
+
const forbidden = keys.filter(
|
|
200
|
+
(key) => /secret|token|password|private|content|path|key$/i.test(key)
|
|
201
|
+
);
|
|
202
|
+
if (forbidden.length > 0) {
|
|
203
|
+
throw new Error(`\u51ED\u636E\u5143\u6570\u636E\u5305\u542B\u7981\u6B62\u5B57\u6BB5: ${forbidden.join(", ")}`);
|
|
204
|
+
}
|
|
205
|
+
if (!metadata.credentialProfile.trim()) {
|
|
206
|
+
throw new Error("credentialProfile \u4E0D\u80FD\u4E3A\u7A7A");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function assertCredentialProfileName(value) {
|
|
210
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(value)) {
|
|
211
|
+
throw new Error("credentialProfile \u53EA\u80FD\u5305\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u548C\u8FDE\u5B57\u7B26\uFF0C\u957F\u5EA6\u4E0D\u8D85\u8FC7 80");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/credential-store.ts
|
|
216
|
+
var VAULT_SCHEMA_VERSION = 1;
|
|
217
|
+
var MASTER_KEY_ACCOUNT = "__vault_master_key_v1__";
|
|
218
|
+
var DEFAULT_KEYCHAIN_SERVICE = "cn.huoye.app-ship.credentials.v1";
|
|
219
|
+
var MAX_SECURITY_OUTPUT_BYTES = 1024 * 1024;
|
|
220
|
+
var MAX_SECRET_VALUE_BYTES = 5 * 1024 * 1024;
|
|
221
|
+
var MAX_VAULT_BYTES = 20 * 1024 * 1024;
|
|
222
|
+
var DEFAULT_CREDENTIAL_VAULT_PATH = path.join(
|
|
223
|
+
process.env.APP_SHIP_HOME || path.join(homedir(), ".app-ship"),
|
|
224
|
+
"credentials.v1.json"
|
|
225
|
+
);
|
|
226
|
+
var MacOSKeychainSecretStore = class {
|
|
227
|
+
constructor(service = DEFAULT_KEYCHAIN_SERVICE, runner = runSecurity) {
|
|
228
|
+
this.service = service;
|
|
229
|
+
this.runner = runner;
|
|
230
|
+
if (process.platform !== "darwin") {
|
|
231
|
+
throw new Error("\u5F53\u524D\u5B89\u5168\u5B58\u50A8\u5782\u76F4\u5207\u7247\u4EC5\u9A8C\u8BC1 macOS Keychain");
|
|
232
|
+
}
|
|
233
|
+
assertSafeIdentifier(service, "Keychain service");
|
|
234
|
+
}
|
|
235
|
+
async getSecret(account) {
|
|
236
|
+
assertSafeIdentifier(account, "Keychain account");
|
|
237
|
+
const result = await this.runner([
|
|
238
|
+
"find-generic-password",
|
|
239
|
+
"-s",
|
|
240
|
+
this.service,
|
|
241
|
+
"-a",
|
|
242
|
+
account,
|
|
243
|
+
"-w"
|
|
244
|
+
]);
|
|
245
|
+
if (result.code === 44) return void 0;
|
|
246
|
+
if (result.code !== 0) {
|
|
247
|
+
throw new Error("macOS Keychain \u8BFB\u53D6\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u5F53\u524D\u7528\u6237 Keychain \u5DF2\u89E3\u9501\u5E76\u5141\u8BB8\u8BBF\u95EE");
|
|
248
|
+
}
|
|
249
|
+
return result.stdout.replace(/[\r\n]+$/, "");
|
|
250
|
+
}
|
|
251
|
+
async setSecret(account, secret) {
|
|
252
|
+
assertSafeIdentifier(account, "Keychain account");
|
|
253
|
+
if (!secret) throw new Error("\u4E0D\u80FD\u5199\u5165\u7A7A\u79D8\u5BC6");
|
|
254
|
+
const result = await this.runner(
|
|
255
|
+
["add-generic-password", "-U", "-s", this.service, "-a", account, "-w"],
|
|
256
|
+
`${secret}
|
|
257
|
+
${secret}
|
|
258
|
+
`
|
|
259
|
+
);
|
|
260
|
+
if (result.code !== 0) {
|
|
261
|
+
throw new Error("macOS Keychain \u5199\u5165\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u5F53\u524D\u7528\u6237 Keychain \u53EF\u7528");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async deleteSecret(account) {
|
|
265
|
+
assertSafeIdentifier(account, "Keychain account");
|
|
266
|
+
const result = await this.runner(["delete-generic-password", "-s", this.service, "-a", account]);
|
|
267
|
+
if (result.code === 44) return false;
|
|
268
|
+
if (result.code !== 0) {
|
|
269
|
+
throw new Error("macOS Keychain \u5220\u9664\u5931\u8D25");
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var LocalCredentialProfileStore = class {
|
|
275
|
+
constructor(secretStore, vaultPath = DEFAULT_CREDENTIAL_VAULT_PATH) {
|
|
276
|
+
this.secretStore = secretStore;
|
|
277
|
+
this.vaultPath = vaultPath;
|
|
278
|
+
}
|
|
279
|
+
async listMetadata() {
|
|
280
|
+
const document = await this.readDocument();
|
|
281
|
+
return Object.values(document.profiles).map(({ metadata }) => ({ ...metadata })).sort((left, right) => left.credentialProfile.localeCompare(right.credentialProfile));
|
|
282
|
+
}
|
|
283
|
+
async getMetadata(credentialProfile) {
|
|
284
|
+
assertCredentialProfileName(credentialProfile);
|
|
285
|
+
const document = await this.readDocument();
|
|
286
|
+
const metadata = document.profiles[credentialProfile]?.metadata;
|
|
287
|
+
return metadata ? { ...metadata } : void 0;
|
|
288
|
+
}
|
|
289
|
+
async saveProfile(input, options = {}) {
|
|
290
|
+
return this.withWriteLock(() => this.saveProfileUnlocked(input, options));
|
|
291
|
+
}
|
|
292
|
+
async saveProfileUnlocked(input, options) {
|
|
293
|
+
assertCredentialProfileName(input.credentialProfile);
|
|
294
|
+
const document = await this.readDocument();
|
|
295
|
+
if (document.profiles[input.credentialProfile] && !options.overwrite) {
|
|
296
|
+
throw new Error(`credentialProfile \u5DF2\u5B58\u5728: ${input.credentialProfile}`);
|
|
297
|
+
}
|
|
298
|
+
const fields = validateFields(input.material.fields);
|
|
299
|
+
const files = await normalizeSecretFiles(input.material.files);
|
|
300
|
+
if (Object.keys(fields).length === 0 && Object.keys(files).length === 0) {
|
|
301
|
+
throw new Error("\u51ED\u636E\u6750\u6599\u4E0D\u80FD\u4E3A\u7A7A");
|
|
302
|
+
}
|
|
303
|
+
const metadata = {
|
|
304
|
+
credentialProfile: input.credentialProfile,
|
|
305
|
+
platform: input.platform,
|
|
306
|
+
configured: true,
|
|
307
|
+
verified: false,
|
|
308
|
+
storageKind: Object.keys(files).length > 0 ? "encrypted_local_vault" : "os_secure_storage"
|
|
309
|
+
};
|
|
310
|
+
assertSafeCredentialMetadata(metadata);
|
|
311
|
+
const encryptedFiles = Object.keys(files).length > 0 ? await this.encryptFiles(input.credentialProfile, input.platform, files) : void 0;
|
|
312
|
+
const previousSecret = await this.secretStore.getSecret(input.credentialProfile);
|
|
313
|
+
await this.secretStore.setSecret(input.credentialProfile, JSON.stringify(fields));
|
|
314
|
+
try {
|
|
315
|
+
document.profiles[input.credentialProfile] = {
|
|
316
|
+
metadata,
|
|
317
|
+
...encryptedFiles ? { encryptedFiles } : {}
|
|
318
|
+
};
|
|
319
|
+
await this.writeDocument(document);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
try {
|
|
322
|
+
await this.restoreSecret(input.credentialProfile, previousSecret);
|
|
323
|
+
} catch {
|
|
324
|
+
throw new Error("\u51ED\u636E vault \u5199\u5165\u5931\u8D25\u4E14 Keychain \u56DE\u6EDA\u672A\u5B8C\u6210\uFF0C\u8BF7\u505C\u6B62\u6267\u884C\u5E76\u4EBA\u5DE5\u68C0\u67E5");
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
return { ...metadata };
|
|
329
|
+
}
|
|
330
|
+
async deleteProfile(credentialProfile) {
|
|
331
|
+
return this.withWriteLock(() => this.deleteProfileUnlocked(credentialProfile));
|
|
332
|
+
}
|
|
333
|
+
async deleteProfileUnlocked(credentialProfile) {
|
|
334
|
+
assertCredentialProfileName(credentialProfile);
|
|
335
|
+
const document = await this.readDocument();
|
|
336
|
+
const existingRecord = document.profiles[credentialProfile];
|
|
337
|
+
const previousSecret = await this.secretStore.getSecret(credentialProfile);
|
|
338
|
+
if (!existingRecord && previousSecret === void 0) return false;
|
|
339
|
+
await this.secretStore.deleteSecret(credentialProfile);
|
|
340
|
+
try {
|
|
341
|
+
delete document.profiles[credentialProfile];
|
|
342
|
+
await this.writeDocument(document);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
try {
|
|
345
|
+
await this.restoreSecret(credentialProfile, previousSecret);
|
|
346
|
+
} catch {
|
|
347
|
+
throw new Error("\u51ED\u636E vault \u5220\u9664\u5931\u8D25\u4E14 Keychain \u56DE\u6EDA\u672A\u5B8C\u6210\uFF0C\u8BF7\u505C\u6B62\u6267\u884C\u5E76\u4EBA\u5DE5\u68C0\u67E5");
|
|
348
|
+
}
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* 为可信本机调用读取当前平台材料;普通回显不解密文件,主动查看仅在本机返回内容。
|
|
355
|
+
* @param request - 明确平台、档案与用途;local_view 由用户点击查看触发,local_export 继续拒绝。
|
|
356
|
+
*/
|
|
357
|
+
async resolveForLocalExecution(request) {
|
|
358
|
+
assertCredentialProfileName(request.credentialProfile);
|
|
359
|
+
if (request.purpose === "local_export") {
|
|
360
|
+
throw new Error("\u672C\u5730\u51ED\u636E\u5BFC\u51FA\u5C1A\u672A\u5B9E\u73B0\u8EAB\u4EFD\u786E\u8BA4\uFF0C\u5F53\u524D\u7981\u6B62\u5BFC\u51FA");
|
|
361
|
+
}
|
|
362
|
+
const document = await this.readDocument();
|
|
363
|
+
const record2 = document.profiles[request.credentialProfile];
|
|
364
|
+
if (!record2 || record2.metadata.platform !== request.platform) {
|
|
365
|
+
throw new Error("credentialProfile \u4E0D\u5B58\u5728\u6216\u4E0E\u76EE\u6807\u5E73\u53F0\u4E0D\u5339\u914D");
|
|
366
|
+
}
|
|
367
|
+
const serializedFields = await this.secretStore.getSecret(request.credentialProfile);
|
|
368
|
+
if (serializedFields === void 0) {
|
|
369
|
+
throw new Error("credentialProfile \u7684 Keychain \u6750\u6599\u7F3A\u5931");
|
|
370
|
+
}
|
|
371
|
+
const fields = parseStoredFields(serializedFields);
|
|
372
|
+
if (request.purpose === "local_display") return { fields };
|
|
373
|
+
const files = record2.encryptedFiles ? await this.decryptFiles(request.credentialProfile, request.platform, record2.encryptedFiles) : void 0;
|
|
374
|
+
return {
|
|
375
|
+
fields,
|
|
376
|
+
...files ? {
|
|
377
|
+
files: Object.fromEntries(
|
|
378
|
+
Object.entries(files).map(([name, content]) => [name, { content }])
|
|
379
|
+
)
|
|
380
|
+
} : {}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async encryptFiles(credentialProfile, platform, files) {
|
|
384
|
+
const key = await this.getOrCreateMasterKey();
|
|
385
|
+
const iv = randomBytes(12);
|
|
386
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv, { authTagLength: 16 });
|
|
387
|
+
cipher.setAAD(createAad(credentialProfile, platform));
|
|
388
|
+
const ciphertext = Buffer.concat([cipher.update(JSON.stringify(files), "utf8"), cipher.final()]);
|
|
389
|
+
return {
|
|
390
|
+
algorithm: "aes-256-gcm",
|
|
391
|
+
iv: iv.toString("base64"),
|
|
392
|
+
authTag: cipher.getAuthTag().toString("base64"),
|
|
393
|
+
ciphertext: ciphertext.toString("base64")
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
async decryptFiles(credentialProfile, platform, envelope) {
|
|
397
|
+
if (envelope.algorithm !== "aes-256-gcm") {
|
|
398
|
+
throw new Error("\u51ED\u636E vault \u4F7F\u7528\u4E86\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7B97\u6CD5");
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const key = await this.getMasterKey();
|
|
402
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(envelope.iv, "base64"), {
|
|
403
|
+
authTagLength: 16
|
|
404
|
+
});
|
|
405
|
+
decipher.setAAD(createAad(credentialProfile, platform));
|
|
406
|
+
decipher.setAuthTag(Buffer.from(envelope.authTag, "base64"));
|
|
407
|
+
const plaintext = Buffer.concat([
|
|
408
|
+
decipher.update(Buffer.from(envelope.ciphertext, "base64")),
|
|
409
|
+
decipher.final()
|
|
410
|
+
]);
|
|
411
|
+
return validateFields(JSON.parse(plaintext.toString("utf8")));
|
|
412
|
+
} catch {
|
|
413
|
+
throw new Error("\u51ED\u636E vault \u5B8C\u6574\u6027\u6821\u9A8C\u5931\u8D25\uFF0C\u6587\u4EF6\u53EF\u80FD\u635F\u574F\u6216\u4E3B\u5BC6\u94A5\u4E0D\u5339\u914D");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async getOrCreateMasterKey() {
|
|
417
|
+
const existing = await this.secretStore.getSecret(MASTER_KEY_ACCOUNT);
|
|
418
|
+
if (existing) return decodeMasterKey(existing);
|
|
419
|
+
const key = randomBytes(32);
|
|
420
|
+
await this.secretStore.setSecret(MASTER_KEY_ACCOUNT, key.toString("base64"));
|
|
421
|
+
return key;
|
|
422
|
+
}
|
|
423
|
+
async getMasterKey() {
|
|
424
|
+
const value = await this.secretStore.getSecret(MASTER_KEY_ACCOUNT);
|
|
425
|
+
if (!value) throw new Error("\u51ED\u636E vault \u4E3B\u5BC6\u94A5\u7F3A\u5931");
|
|
426
|
+
return decodeMasterKey(value);
|
|
427
|
+
}
|
|
428
|
+
async restoreSecret(account, previousSecret) {
|
|
429
|
+
if (previousSecret === void 0) {
|
|
430
|
+
await this.secretStore.deleteSecret(account);
|
|
431
|
+
} else {
|
|
432
|
+
await this.secretStore.setSecret(account, previousSecret);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async readDocument() {
|
|
436
|
+
try {
|
|
437
|
+
const fileStat = await lstat(this.vaultPath);
|
|
438
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile()) {
|
|
439
|
+
throw new Error("\u51ED\u636E vault \u5FC5\u987B\u662F\u666E\u901A\u6587\u4EF6\u4E14\u4E0D\u80FD\u662F\u7B26\u53F7\u94FE\u63A5");
|
|
440
|
+
}
|
|
441
|
+
if (fileStat.size > MAX_VAULT_BYTES) throw new Error("\u51ED\u636E vault \u8D85\u8FC7\u5B89\u5168\u5927\u5C0F\u4E0A\u9650");
|
|
442
|
+
if ((fileStat.mode & 63) !== 0) {
|
|
443
|
+
throw new Error("\u51ED\u636E vault \u6743\u9650\u4E0D\u5B89\u5168\uFF0C\u8981\u6C42\u4EC5\u5F53\u524D\u7528\u6237\u53EF\u8BFB\u5199\uFF080600\uFF09");
|
|
444
|
+
}
|
|
445
|
+
const value = JSON.parse(await readFile(this.vaultPath, "utf8"));
|
|
446
|
+
return validateDocument(value);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (error?.code === "ENOENT") return emptyDocument();
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/** 多个 Native Host/CLI 进程共享同一安全库,读改写必须排他;冲突明确失败,不覆盖或自动重试。 */
|
|
453
|
+
async withWriteLock(operation) {
|
|
454
|
+
const directory = path.dirname(this.vaultPath);
|
|
455
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
456
|
+
const directoryStat = await lstat(directory);
|
|
457
|
+
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory() || process.getuid && directoryStat.uid !== process.getuid()) {
|
|
458
|
+
throw new Error("\u51ED\u636E vault \u76EE\u5F55\u5FC5\u987B\u662F\u666E\u901A\u76EE\u5F55\u4E14\u4E0D\u80FD\u662F\u7B26\u53F7\u94FE\u63A5");
|
|
459
|
+
}
|
|
460
|
+
if (process.platform !== "win32" && (directoryStat.mode & 63) !== 0) {
|
|
461
|
+
throw new Error("\u51ED\u636E vault \u76EE\u5F55\u6743\u9650\u4E0D\u5B89\u5168\uFF0C\u8981\u6C42\u4EC5\u5F53\u524D\u7528\u6237\u53EF\u8BBF\u95EE\uFF080700\uFF09");
|
|
462
|
+
}
|
|
463
|
+
const lockPath = `${this.vaultPath}.write-lock`;
|
|
464
|
+
let lock;
|
|
465
|
+
try {
|
|
466
|
+
lock = await open(lockPath, "wx", 384);
|
|
467
|
+
} catch {
|
|
468
|
+
throw new Error("\u51ED\u636E\u5B89\u5168\u5E93\u6B63\u5728\u5199\u5165\u6216\u5B58\u5728\u672A\u6E05\u7406\u5199\u9501\uFF0C\u8BF7\u6838\u5BF9\u540E\u91CD\u8BD5");
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
return await operation();
|
|
472
|
+
} finally {
|
|
473
|
+
await lock.close();
|
|
474
|
+
await unlink(lockPath);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async writeDocument(document) {
|
|
478
|
+
const temporaryPath = `${this.vaultPath}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
479
|
+
try {
|
|
480
|
+
const handle = await open(temporaryPath, "wx", 384);
|
|
481
|
+
try {
|
|
482
|
+
await handle.writeFile(`${JSON.stringify(document, null, 2)}
|
|
483
|
+
`, "utf8");
|
|
484
|
+
await handle.sync();
|
|
485
|
+
} finally {
|
|
486
|
+
await handle.close();
|
|
487
|
+
}
|
|
488
|
+
await rename(temporaryPath, this.vaultPath);
|
|
489
|
+
await chmod(this.vaultPath, 384);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
await unlink(temporaryPath).catch(() => void 0);
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
function createDefaultCredentialProfileStore(vaultPath = DEFAULT_CREDENTIAL_VAULT_PATH) {
|
|
497
|
+
if (process.platform !== "darwin") {
|
|
498
|
+
throw new Error("\u5F53\u524D\u7248\u672C\u4EC5\u5B8C\u6210 macOS Keychain \u5782\u76F4\u5207\u7247\uFF0CLinux/Windows \u5C1A\u672A\u9A8C\u8BC1");
|
|
499
|
+
}
|
|
500
|
+
return new LocalCredentialProfileStore(new MacOSKeychainSecretStore(), vaultPath);
|
|
501
|
+
}
|
|
502
|
+
async function runSecurity(args, input) {
|
|
503
|
+
return new Promise((resolve, reject) => {
|
|
504
|
+
const child = spawn("/usr/bin/security", args, {
|
|
505
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
506
|
+
});
|
|
507
|
+
let stdout = "";
|
|
508
|
+
let outputBytes = 0;
|
|
509
|
+
const timeout = setTimeout(() => {
|
|
510
|
+
child.kill("SIGTERM");
|
|
511
|
+
reject(new Error("macOS Keychain \u64CD\u4F5C\u8D85\u65F6"));
|
|
512
|
+
}, 15e3);
|
|
513
|
+
child.stdout.on("data", (chunk) => {
|
|
514
|
+
outputBytes += chunk.length;
|
|
515
|
+
if (outputBytes > MAX_SECURITY_OUTPUT_BYTES) {
|
|
516
|
+
child.kill("SIGTERM");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
stdout += chunk.toString("utf8");
|
|
520
|
+
});
|
|
521
|
+
child.stderr.on("data", (chunk) => {
|
|
522
|
+
outputBytes += chunk.length;
|
|
523
|
+
if (outputBytes > MAX_SECURITY_OUTPUT_BYTES) child.kill("SIGTERM");
|
|
524
|
+
});
|
|
525
|
+
child.on("error", () => {
|
|
526
|
+
clearTimeout(timeout);
|
|
527
|
+
reject(new Error("\u65E0\u6CD5\u542F\u52A8 macOS security \u5DE5\u5177"));
|
|
528
|
+
});
|
|
529
|
+
child.on("close", (code) => {
|
|
530
|
+
clearTimeout(timeout);
|
|
531
|
+
if (outputBytes > MAX_SECURITY_OUTPUT_BYTES) {
|
|
532
|
+
reject(new Error("macOS Keychain \u8F93\u51FA\u8D85\u8FC7\u5B89\u5168\u4E0A\u9650"));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
resolve({ code: code ?? 1, stdout });
|
|
536
|
+
});
|
|
537
|
+
child.stdin.on("error", () => {
|
|
538
|
+
});
|
|
539
|
+
child.stdin.end(input);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
async function normalizeSecretFiles(files) {
|
|
543
|
+
const result = {};
|
|
544
|
+
for (const [name, file] of Object.entries(files || {})) {
|
|
545
|
+
if (!/^[a-zA-Z0-9._-]{1,80}$/.test(name)) {
|
|
546
|
+
throw new Error(`\u6587\u4EF6\u578B\u51ED\u636E\u540D\u79F0\u4E0D\u5B89\u5168: ${name}`);
|
|
547
|
+
}
|
|
548
|
+
if (file.content && file.externalPath) {
|
|
549
|
+
throw new Error(`\u6587\u4EF6\u578B\u51ED\u636E ${name} \u4E0D\u80FD\u540C\u65F6\u63D0\u4F9B content \u4E0E externalPath`);
|
|
550
|
+
}
|
|
551
|
+
if (file.content !== void 0) {
|
|
552
|
+
if (!file.content) throw new Error(`\u6587\u4EF6\u578B\u51ED\u636E ${name} \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
553
|
+
if (Buffer.byteLength(file.content, "utf8") > MAX_SECRET_VALUE_BYTES) {
|
|
554
|
+
throw new Error(`\u6587\u4EF6\u578B\u51ED\u636E ${name} \u8D85\u8FC7\u5B89\u5168\u5927\u5C0F\u4E0A\u9650`);
|
|
555
|
+
}
|
|
556
|
+
result[name] = file.content;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (!file.externalPath) throw new Error(`\u6587\u4EF6\u578B\u51ED\u636E ${name} \u7F3A\u5C11\u5185\u5BB9\u6216\u8DEF\u5F84`);
|
|
560
|
+
const fileStat = await lstat(file.externalPath);
|
|
561
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile()) {
|
|
562
|
+
throw new Error(`\u5916\u90E8\u51ED\u636E\u8DEF\u5F84\u5FC5\u987B\u662F\u666E\u901A\u6587\u4EF6\u4E14\u4E0D\u80FD\u662F\u7B26\u53F7\u94FE\u63A5: ${name}`);
|
|
563
|
+
}
|
|
564
|
+
if (process.platform !== "win32" && (fileStat.mode & 63) !== 0) {
|
|
565
|
+
throw new Error(`\u5916\u90E8\u51ED\u636E\u6587\u4EF6\u6743\u9650\u4E0D\u5B89\u5168\uFF0C\u8981\u6C42 0600: ${name}`);
|
|
566
|
+
}
|
|
567
|
+
if (fileStat.size > MAX_SECRET_VALUE_BYTES) {
|
|
568
|
+
throw new Error(`\u5916\u90E8\u51ED\u636E\u6587\u4EF6\u8D85\u8FC7\u5B89\u5168\u5927\u5C0F\u4E0A\u9650: ${name}`);
|
|
569
|
+
}
|
|
570
|
+
const content = await readFile(file.externalPath, "utf8");
|
|
571
|
+
if (!content) throw new Error(`\u5916\u90E8\u51ED\u636E\u6587\u4EF6\u4E3A\u7A7A: ${name}`);
|
|
572
|
+
result[name] = content;
|
|
573
|
+
}
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
function validateFields(value) {
|
|
577
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
578
|
+
throw new Error("\u51ED\u636E\u5B57\u6BB5\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u5BF9\u8C61");
|
|
579
|
+
}
|
|
580
|
+
const result = {};
|
|
581
|
+
for (const [key, item] of Object.entries(value)) {
|
|
582
|
+
if (!/^[a-zA-Z0-9._-]{1,80}$/.test(key) || ["__proto__", "prototype", "constructor"].includes(key) || typeof item !== "string") {
|
|
583
|
+
throw new Error("\u51ED\u636E\u5B57\u6BB5\u540D\u79F0\u6216\u503C\u4E0D\u5408\u6CD5");
|
|
584
|
+
}
|
|
585
|
+
if (Buffer.byteLength(item, "utf8") > MAX_SECRET_VALUE_BYTES) {
|
|
586
|
+
throw new Error(`\u51ED\u636E\u5B57\u6BB5\u8D85\u8FC7\u5B89\u5168\u5927\u5C0F\u4E0A\u9650: ${key}`);
|
|
587
|
+
}
|
|
588
|
+
result[key] = item;
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
function parseStoredFields(value) {
|
|
593
|
+
try {
|
|
594
|
+
return validateFields(JSON.parse(value));
|
|
595
|
+
} catch {
|
|
596
|
+
throw new Error("Keychain \u4E2D\u7684\u51ED\u636E\u5B57\u6BB5\u683C\u5F0F\u65E0\u6548");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function validateDocument(value) {
|
|
600
|
+
if (value?.schemaVersion !== VAULT_SCHEMA_VERSION || !value.profiles || typeof value.profiles !== "object" || Array.isArray(value.profiles)) {
|
|
601
|
+
throw new Error("\u51ED\u636E vault \u7248\u672C\u6216\u7ED3\u6784\u65E0\u6548");
|
|
602
|
+
}
|
|
603
|
+
for (const [name, record2] of Object.entries(value.profiles)) {
|
|
604
|
+
assertCredentialProfileName(name);
|
|
605
|
+
if (!record2 || typeof record2 !== "object" || Array.isArray(record2)) {
|
|
606
|
+
throw new Error("\u51ED\u636E vault \u7684 Profile \u7ED3\u6784\u65E0\u6548");
|
|
607
|
+
}
|
|
608
|
+
assertSafeCredentialMetadata(record2.metadata);
|
|
609
|
+
if (record2.metadata.credentialProfile !== name) {
|
|
610
|
+
throw new Error("\u51ED\u636E vault \u7684 Profile \u7D22\u5F15\u4E0D\u4E00\u81F4");
|
|
611
|
+
}
|
|
612
|
+
if (!(record2.metadata.platform in RELEASE_PLATFORM_CAPABILITIES) || typeof record2.metadata.configured !== "boolean" || typeof record2.metadata.verified !== "boolean" || !["os_secure_storage", "encrypted_local_vault", "external_file"].includes(
|
|
613
|
+
record2.metadata.storageKind
|
|
614
|
+
)) {
|
|
615
|
+
throw new Error("\u51ED\u636E vault \u7684\u5143\u6570\u636E\u65E0\u6548");
|
|
616
|
+
}
|
|
617
|
+
if (record2.encryptedFiles) validateEnvelope(record2.encryptedFiles);
|
|
618
|
+
}
|
|
619
|
+
return value;
|
|
620
|
+
}
|
|
621
|
+
function validateEnvelope(value) {
|
|
622
|
+
if (value?.algorithm !== "aes-256-gcm" || typeof value.iv !== "string" || typeof value.authTag !== "string" || typeof value.ciphertext !== "string") {
|
|
623
|
+
throw new Error("\u51ED\u636E vault \u7684\u52A0\u5BC6\u4FE1\u5C01\u65E0\u6548");
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function emptyDocument() {
|
|
627
|
+
return { schemaVersion: VAULT_SCHEMA_VERSION, profiles: {} };
|
|
628
|
+
}
|
|
629
|
+
function createAad(credentialProfile, platform) {
|
|
630
|
+
return Buffer.from(`app-ship\0${VAULT_SCHEMA_VERSION}\0${platform}\0${credentialProfile}`, "utf8");
|
|
631
|
+
}
|
|
632
|
+
function decodeMasterKey(value) {
|
|
633
|
+
const key = Buffer.from(value, "base64");
|
|
634
|
+
if (key.length !== 32) throw new Error("\u51ED\u636E vault \u4E3B\u5BC6\u94A5\u957F\u5EA6\u65E0\u6548");
|
|
635
|
+
return key;
|
|
636
|
+
}
|
|
637
|
+
function assertSafeIdentifier(value, label) {
|
|
638
|
+
if (!/^[a-zA-Z0-9._-]{1,120}$/.test(value)) {
|
|
639
|
+
throw new Error(`${label} \u5305\u542B\u4E0D\u5B89\u5168\u5B57\u7B26`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/credential-setup.ts
|
|
644
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
645
|
+
import { constants as constants3 } from "fs";
|
|
646
|
+
import { lstat as lstat2, mkdir as mkdir2, open as open2, readdir, realpath, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
647
|
+
import path2 from "path";
|
|
648
|
+
import yaml from "js-yaml";
|
|
649
|
+
|
|
650
|
+
// src/preflight-http.ts
|
|
651
|
+
var ProbeFailure = class extends Error {
|
|
652
|
+
/**
|
|
653
|
+
* 创建脱敏核验错误。
|
|
654
|
+
* @param code - 对外允许公开的失败分类。
|
|
655
|
+
*/
|
|
656
|
+
constructor(code) {
|
|
657
|
+
super(code);
|
|
658
|
+
this.code = code;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
async function jsonRequest(transport, url, init, allowNoContent = false) {
|
|
662
|
+
const response = await transport(url, { ...init, redirect: "error" });
|
|
663
|
+
if (response.status === 401) throw new ProbeFailure("invalid_credentials");
|
|
664
|
+
if (response.status === 404) throw new ProbeFailure("application_not_found");
|
|
665
|
+
if (response.status === 403) throw new ProbeFailure("rejected");
|
|
666
|
+
if (allowNoContent && response.status === 204) return {};
|
|
667
|
+
if (!response.ok || !response.body) throw new ProbeFailure("network_unavailable");
|
|
668
|
+
const reader = response.body.getReader();
|
|
669
|
+
const chunks = [];
|
|
670
|
+
let bytes = 0;
|
|
671
|
+
try {
|
|
672
|
+
for (; ; ) {
|
|
673
|
+
const next = await reader.read();
|
|
674
|
+
if (next.done) break;
|
|
675
|
+
bytes += next.value.length;
|
|
676
|
+
if (bytes > 1048576) {
|
|
677
|
+
await reader.cancel();
|
|
678
|
+
throw new ProbeFailure("invalid_response");
|
|
679
|
+
}
|
|
680
|
+
chunks.push(next.value);
|
|
681
|
+
}
|
|
682
|
+
} finally {
|
|
683
|
+
reader.releaseLock();
|
|
684
|
+
}
|
|
685
|
+
let result;
|
|
686
|
+
try {
|
|
687
|
+
result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
688
|
+
} catch {
|
|
689
|
+
throw new ProbeFailure("invalid_response");
|
|
690
|
+
}
|
|
691
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
692
|
+
throw new ProbeFailure("invalid_response");
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/remaining-store-preflight.ts
|
|
697
|
+
import {
|
|
698
|
+
constants,
|
|
699
|
+
createHash,
|
|
700
|
+
createHmac,
|
|
701
|
+
createPublicKey as createPublicKey2,
|
|
702
|
+
publicEncrypt,
|
|
703
|
+
sign as sign2
|
|
704
|
+
} from "crypto";
|
|
705
|
+
|
|
706
|
+
// src/credential-file-material.ts
|
|
707
|
+
import { createPrivateKey, createPublicKey, X509Certificate, sign } from "crypto";
|
|
708
|
+
function parseGoogleServiceAccount(content) {
|
|
709
|
+
try {
|
|
710
|
+
const value = JSON.parse(content);
|
|
711
|
+
if (value.type !== "service_account" || !/^[^\s@]+@[^\s@]+\.iam\.gserviceaccount\.com$/u.test(value.client_email) || typeof value.private_key !== "string" || value.token_uri !== void 0 && value.token_uri !== "https://oauth2.googleapis.com/token" || createPrivateKey(value.private_key).asymmetricKeyType !== "rsa")
|
|
712
|
+
throw new Error();
|
|
713
|
+
return value;
|
|
714
|
+
} catch {
|
|
715
|
+
throw new Error("invalid_service_account");
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function normalizeXiaomiPublicKey(content) {
|
|
719
|
+
try {
|
|
720
|
+
let key;
|
|
721
|
+
try {
|
|
722
|
+
key = new X509Certificate(content).publicKey;
|
|
723
|
+
} catch {
|
|
724
|
+
key = createPublicKey(
|
|
725
|
+
content.includes("-----BEGIN") ? content : `-----BEGIN PUBLIC KEY-----
|
|
726
|
+
${content}
|
|
727
|
+
-----END PUBLIC KEY-----`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
if (key.asymmetricKeyType !== "rsa" || /PRIVATE KEY/u.test(content)) throw new Error();
|
|
731
|
+
return key.export({ type: "spki", format: "pem" }).toString();
|
|
732
|
+
} catch {
|
|
733
|
+
throw new Error("invalid_public_key");
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function appleCredentialToken(fields, content) {
|
|
737
|
+
if (!/^[A-Za-z0-9]{10}$/u.test(fields.apiKeyId || "") || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/iu.test(fields.apiIssuerId || ""))
|
|
738
|
+
throw new Error("invalid_apple_key");
|
|
739
|
+
const key = createPrivateKey(content);
|
|
740
|
+
if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1")
|
|
741
|
+
throw new Error("invalid_apple_key");
|
|
742
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
743
|
+
const header = Buffer.from(
|
|
744
|
+
JSON.stringify({ alg: "ES256", kid: fields.apiKeyId, typ: "JWT" })
|
|
745
|
+
).toString("base64url");
|
|
746
|
+
const payload = Buffer.from(
|
|
747
|
+
JSON.stringify({
|
|
748
|
+
iss: fields.apiIssuerId,
|
|
749
|
+
aud: "appstoreconnect-v1",
|
|
750
|
+
iat: now,
|
|
751
|
+
exp: now + 600
|
|
752
|
+
})
|
|
753
|
+
).toString("base64url");
|
|
754
|
+
const body = `${header}.${payload}`;
|
|
755
|
+
return `${body}.${sign("sha256", Buffer.from(body), { key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
756
|
+
}
|
|
757
|
+
function importedCredentialMaterial(platform, content, fields) {
|
|
758
|
+
if (Buffer.byteLength(content) > 65536 || !content.trim())
|
|
759
|
+
throw new Error("invalid_credential_file");
|
|
760
|
+
if (platform === "google_play") {
|
|
761
|
+
if (Object.keys(fields).length) throw new Error("invalid_import_fields");
|
|
762
|
+
const value = parseGoogleServiceAccount(content);
|
|
763
|
+
return { fields: {}, files: { serviceAccountJson: { content: JSON.stringify(value) } } };
|
|
764
|
+
}
|
|
765
|
+
const expected = platform === "app_store" ? ["apiIssuerId", "apiKeyId", "appleId", "appPassword"] : platform === "xiaomi" ? ["userName", "privateKey"] : [];
|
|
766
|
+
if (!expected.length || Object.keys(fields).length !== expected.length || expected.some(
|
|
767
|
+
(key) => typeof fields[key] !== "string" || !fields[key].trim() || fields[key].length > 8192 || /[\r\n\0*•●]/u.test(fields[key])
|
|
768
|
+
))
|
|
769
|
+
throw new Error("invalid_import_fields");
|
|
770
|
+
if (platform === "app_store") {
|
|
771
|
+
try {
|
|
772
|
+
appleCredentialToken(fields, content);
|
|
773
|
+
} catch {
|
|
774
|
+
throw new Error("invalid_apple_key");
|
|
775
|
+
}
|
|
776
|
+
return { fields, files: { privateKey: { content } } };
|
|
777
|
+
}
|
|
778
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(fields.userName)) throw new Error("invalid_import_fields");
|
|
779
|
+
return { fields, files: { publicKey: { content: normalizeXiaomiPublicKey(content) } } };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/remaining-store-preflight.ts
|
|
783
|
+
function hmac(fields, secret) {
|
|
784
|
+
return createHmac("sha256", secret).update(
|
|
785
|
+
Object.keys(fields).sort().map((key) => `${key}=${fields[key]}`).join("&")
|
|
786
|
+
).digest("hex");
|
|
787
|
+
}
|
|
788
|
+
async function preflightRemainingStore(input, store, result, transport, signal) {
|
|
789
|
+
let material;
|
|
790
|
+
try {
|
|
791
|
+
material = await store.resolveForLocalExecution({
|
|
792
|
+
platform: input.platform,
|
|
793
|
+
credentialProfile: input.credentialProfile,
|
|
794
|
+
purpose: "verify"
|
|
795
|
+
});
|
|
796
|
+
} catch {
|
|
797
|
+
return result;
|
|
798
|
+
}
|
|
799
|
+
const fields = material.fields;
|
|
800
|
+
const field = (name) => {
|
|
801
|
+
const value = fields[name];
|
|
802
|
+
if (!value || value.length > 8192 || /[\r\n\0]/u.test(value))
|
|
803
|
+
throw new ProbeFailure("invalid_credentials");
|
|
804
|
+
return value;
|
|
805
|
+
};
|
|
806
|
+
const request = (url, init = {}) => jsonRequest(transport, url, { ...init, signal });
|
|
807
|
+
const matchPackage = (value) => {
|
|
808
|
+
if (typeof value !== "string" || !value) throw new ProbeFailure("invalid_response");
|
|
809
|
+
if (value !== input.packageName) throw new ProbeFailure("application_mismatch");
|
|
810
|
+
};
|
|
811
|
+
const matchId = (value) => {
|
|
812
|
+
if (typeof value !== "string" || !/^\d{1,32}$/u.test(value))
|
|
813
|
+
throw new ProbeFailure("invalid_response");
|
|
814
|
+
if (input.appId && value !== input.appId) throw new ProbeFailure("application_mismatch");
|
|
815
|
+
result.appId = value;
|
|
816
|
+
};
|
|
817
|
+
if (signal.aborted) throw new ProbeFailure("network_unavailable");
|
|
818
|
+
if (input.platform === "oppo") {
|
|
819
|
+
const authUrl = new URL("https://oop-openapi-cn.heytapmobi.com/developer/v1/token");
|
|
820
|
+
authUrl.search = new URLSearchParams({
|
|
821
|
+
client_id: field("clientId"),
|
|
822
|
+
client_secret: field("clientSecret")
|
|
823
|
+
}).toString();
|
|
824
|
+
const auth = await request(authUrl);
|
|
825
|
+
if (auth.errno !== 0)
|
|
826
|
+
throw new ProbeFailure(
|
|
827
|
+
Number.isInteger(auth.errno) ? "invalid_credentials" : "invalid_response"
|
|
828
|
+
);
|
|
829
|
+
const token = auth.data?.access_token;
|
|
830
|
+
if (typeof token !== "string" || !token || /[\r\n\0]/u.test(token))
|
|
831
|
+
throw new ProbeFailure("invalid_response");
|
|
832
|
+
result.authentication = "verified";
|
|
833
|
+
const params = {
|
|
834
|
+
access_token: token,
|
|
835
|
+
pkg_name: input.packageName,
|
|
836
|
+
timestamp: String(Math.floor(Date.now() / 1e3))
|
|
837
|
+
};
|
|
838
|
+
const url = new URL("https://oop-openapi-cn.heytapmobi.com/resource/v1/app/info");
|
|
839
|
+
url.search = new URLSearchParams({
|
|
840
|
+
...params,
|
|
841
|
+
api_sign: hmac(params, field("clientSecret"))
|
|
842
|
+
}).toString();
|
|
843
|
+
const response = await request(url);
|
|
844
|
+
if (response.errno !== 0)
|
|
845
|
+
throw new ProbeFailure(Number.isInteger(response.errno) ? "rejected" : "invalid_response");
|
|
846
|
+
matchPackage(response.data?.pkg_name);
|
|
847
|
+
matchId(response.data?.app_id);
|
|
848
|
+
} else if (input.platform === "vivo") {
|
|
849
|
+
const params = {
|
|
850
|
+
method: "app.query.details",
|
|
851
|
+
access_key: field("accessKey"),
|
|
852
|
+
timestamp: String(Date.now()),
|
|
853
|
+
format: "json",
|
|
854
|
+
v: "1.0",
|
|
855
|
+
sign_method: "HMAC-SHA256",
|
|
856
|
+
target_app_key: "developer",
|
|
857
|
+
packageName: input.packageName
|
|
858
|
+
};
|
|
859
|
+
const response = await request(new URL("https://developer-api.vivo.com.cn/router/rest"), {
|
|
860
|
+
method: "POST",
|
|
861
|
+
body: new URLSearchParams({ ...params, sign: hmac(params, field("accessSecret")) })
|
|
862
|
+
});
|
|
863
|
+
if (!Number.isInteger(response.code)) throw new ProbeFailure("invalid_response");
|
|
864
|
+
if (response.code !== 0)
|
|
865
|
+
throw new ProbeFailure(response.code === 10001 ? "invalid_credentials" : "rejected");
|
|
866
|
+
result.authentication = "verified";
|
|
867
|
+
if (response.subCode !== void 0 && response.subCode !== "0" && response.subCode !== 0)
|
|
868
|
+
throw new ProbeFailure(
|
|
869
|
+
String(response.subCode) === "11001" ? "application_not_found" : "rejected"
|
|
870
|
+
);
|
|
871
|
+
matchPackage(response.data?.packageName);
|
|
872
|
+
} else if (input.platform === "tencent") {
|
|
873
|
+
const appId = field("appId");
|
|
874
|
+
if (!/^\d{1,32}$/u.test(appId)) throw new ProbeFailure("invalid_credentials");
|
|
875
|
+
if (input.appId && input.appId !== appId) throw new ProbeFailure("application_mismatch");
|
|
876
|
+
const params = {
|
|
877
|
+
user_id: field("userId"),
|
|
878
|
+
timestamp: String(Math.floor(Date.now() / 1e3)),
|
|
879
|
+
app_id: appId,
|
|
880
|
+
pkg_name: input.packageName
|
|
881
|
+
};
|
|
882
|
+
const response = await request(
|
|
883
|
+
new URL("https://p.open.qq.com/open_file/developer_api/query_app_detail"),
|
|
884
|
+
{
|
|
885
|
+
method: "POST",
|
|
886
|
+
body: new URLSearchParams({ ...params, sign: hmac(params, field("accessSecret")) })
|
|
887
|
+
}
|
|
888
|
+
);
|
|
889
|
+
if (!Number.isInteger(response.ret)) throw new ProbeFailure("invalid_response");
|
|
890
|
+
if (response.ret !== 0)
|
|
891
|
+
throw new ProbeFailure(
|
|
892
|
+
[1000019, 1000020].includes(Number(response.ret)) ? "invalid_credentials" : [1000008, 3000002].includes(Number(response.ret)) ? "application_not_found" : [1000009, 3000003].includes(Number(response.ret)) ? "application_mismatch" : "rejected"
|
|
893
|
+
);
|
|
894
|
+
result.authentication = "verified";
|
|
895
|
+
matchPackage(response.pkg_name);
|
|
896
|
+
} else if (input.platform === "xiaomi") {
|
|
897
|
+
let publicKey;
|
|
898
|
+
try {
|
|
899
|
+
publicKey = createPublicKey2(
|
|
900
|
+
normalizeXiaomiPublicKey(material.files?.publicKey?.content || "")
|
|
901
|
+
);
|
|
902
|
+
} catch {
|
|
903
|
+
throw new ProbeFailure("invalid_credentials");
|
|
904
|
+
}
|
|
905
|
+
const data = JSON.stringify({ userName: field("userName"), packageName: input.packageName });
|
|
906
|
+
const signature = Buffer.from(
|
|
907
|
+
JSON.stringify({
|
|
908
|
+
sig: [{ name: "RequestData", hash: createHash("md5").update(data).digest("hex") }],
|
|
909
|
+
password: field("privateKey")
|
|
910
|
+
})
|
|
911
|
+
);
|
|
912
|
+
const block = Math.ceil((publicKey.asymmetricKeyDetails?.modulusLength || 1024) / 8) - 11;
|
|
913
|
+
const parts = [];
|
|
914
|
+
for (let i = 0; i < signature.length; i += block)
|
|
915
|
+
parts.push(
|
|
916
|
+
publicEncrypt(
|
|
917
|
+
{ key: publicKey, padding: constants.RSA_PKCS1_PADDING },
|
|
918
|
+
signature.subarray(i, i + block)
|
|
919
|
+
)
|
|
920
|
+
);
|
|
921
|
+
const body = new FormData();
|
|
922
|
+
body.set("RequestData", data);
|
|
923
|
+
body.set("SIG", Buffer.concat(parts).toString("hex"));
|
|
924
|
+
const response = await request(
|
|
925
|
+
new URL("https://api.developer.xiaomi.com/devupload/dev/query"),
|
|
926
|
+
{ method: "POST", body }
|
|
927
|
+
);
|
|
928
|
+
if (!Number.isInteger(response.result)) throw new ProbeFailure("invalid_response");
|
|
929
|
+
if (response.result !== 0)
|
|
930
|
+
throw new ProbeFailure(response.result === -20014 ? "invalid_credentials" : "rejected");
|
|
931
|
+
result.authentication = "verified";
|
|
932
|
+
if (!response.packageInfo) throw new ProbeFailure("application_not_found");
|
|
933
|
+
matchPackage(response.packageInfo?.packageName);
|
|
934
|
+
} else if (input.platform === "app_store") {
|
|
935
|
+
if ((fields.appleId || fields.appPassword) && !fields.apiKeyId && !fields.apiIssuerId && !material.files?.privateKey?.content)
|
|
936
|
+
return { ...result, code: "unsupported_credential_method" };
|
|
937
|
+
let token;
|
|
938
|
+
try {
|
|
939
|
+
token = appleCredentialToken(fields, material.files?.privateKey?.content || "");
|
|
940
|
+
} catch {
|
|
941
|
+
throw new ProbeFailure("invalid_credentials");
|
|
942
|
+
}
|
|
943
|
+
const url = new URL("https://api.appstoreconnect.apple.com/v1/apps");
|
|
944
|
+
url.search = new URLSearchParams({
|
|
945
|
+
"filter[bundleId]": input.packageName,
|
|
946
|
+
limit: "2",
|
|
947
|
+
"fields[apps]": "bundleId"
|
|
948
|
+
}).toString();
|
|
949
|
+
const response = await request(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
950
|
+
if (!Array.isArray(response.data)) throw new ProbeFailure("invalid_response");
|
|
951
|
+
result.authentication = "verified";
|
|
952
|
+
if (!response.data.length) throw new ProbeFailure("application_not_found");
|
|
953
|
+
if (response.data.length !== 1 || response.links?.next)
|
|
954
|
+
throw new ProbeFailure("ambiguous_application");
|
|
955
|
+
matchPackage(response.data[0]?.attributes?.bundleId);
|
|
956
|
+
matchId(response.data[0]?.id);
|
|
957
|
+
} else if (input.platform === "google_play") {
|
|
958
|
+
let account, assertion;
|
|
959
|
+
try {
|
|
960
|
+
account = parseGoogleServiceAccount(material.files?.serviceAccountJson?.content || "");
|
|
961
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
962
|
+
const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
|
|
963
|
+
const payload = Buffer.from(
|
|
964
|
+
JSON.stringify({
|
|
965
|
+
iss: account.client_email,
|
|
966
|
+
scope: "https://www.googleapis.com/auth/androidpublisher",
|
|
967
|
+
aud: "https://oauth2.googleapis.com/token",
|
|
968
|
+
iat: now,
|
|
969
|
+
exp: now + 600
|
|
970
|
+
})
|
|
971
|
+
).toString("base64url");
|
|
972
|
+
const body = `${header}.${payload}`;
|
|
973
|
+
assertion = `${body}.${sign2("RSA-SHA256", Buffer.from(body), account.private_key).toString("base64url")}`;
|
|
974
|
+
} catch {
|
|
975
|
+
throw new ProbeFailure("invalid_credentials");
|
|
976
|
+
}
|
|
977
|
+
const auth = await request(new URL("https://oauth2.googleapis.com/token"), {
|
|
978
|
+
method: "POST",
|
|
979
|
+
body: new URLSearchParams({
|
|
980
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
981
|
+
assertion
|
|
982
|
+
})
|
|
983
|
+
});
|
|
984
|
+
if (typeof auth.access_token !== "string" || !auth.access_token || /[\r\n\0]/u.test(auth.access_token))
|
|
985
|
+
throw new ProbeFailure("invalid_credentials");
|
|
986
|
+
result.authentication = "verified";
|
|
987
|
+
const response = await jsonRequest(
|
|
988
|
+
transport,
|
|
989
|
+
new URL(
|
|
990
|
+
`https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(input.packageName)}/deviceTierConfigs?pageSize=1`
|
|
991
|
+
),
|
|
992
|
+
{ headers: { Authorization: `Bearer ${auth.access_token}` }, signal },
|
|
993
|
+
// 此固定包名 GET 的真实成功空结果使用 204;不能放宽 token 或其他平台的正文校验。
|
|
994
|
+
true
|
|
995
|
+
);
|
|
996
|
+
if (Object.keys(response).some((key) => !["deviceTierConfigs", "nextPageToken"].includes(key)) || response.deviceTierConfigs !== void 0 && !Array.isArray(response.deviceTierConfigs))
|
|
997
|
+
throw new ProbeFailure("invalid_response");
|
|
998
|
+
} else throw new ProbeFailure("invalid_response");
|
|
999
|
+
if (signal.aborted) throw new ProbeFailure("network_unavailable");
|
|
1000
|
+
return { ...result, verified: true, code: "verified", application: "matched" };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/store-publishing.ts
|
|
1004
|
+
import { createHash as createHash2, createPrivateKey as createPrivateKey2, randomUUID, sign as sign3 } from "crypto";
|
|
1005
|
+
var StorePublishingError = class extends Error {
|
|
1006
|
+
constructor(code) {
|
|
1007
|
+
super(code);
|
|
1008
|
+
this.code = code;
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
var storeRecord = (value) => {
|
|
1012
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1013
|
+
throw new StorePublishingError("invalid_response");
|
|
1014
|
+
return value;
|
|
1015
|
+
};
|
|
1016
|
+
function storeIdentifier(value) {
|
|
1017
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return String(value);
|
|
1018
|
+
if (typeof value === "string" && /^[A-Za-z0-9._-]{1,160}$/u.test(value)) return value;
|
|
1019
|
+
throw new StorePublishingError("invalid_response");
|
|
1020
|
+
}
|
|
1021
|
+
function credentialField(value) {
|
|
1022
|
+
if (typeof value !== "string" || !value || value.length > 8192 || /[\r\n\0]/u.test(value))
|
|
1023
|
+
throw new StorePublishingError("invalid_credentials");
|
|
1024
|
+
return value;
|
|
1025
|
+
}
|
|
1026
|
+
function createSamsungJwt(accountId, privateKey, now = Date.now()) {
|
|
1027
|
+
credentialField(accountId);
|
|
1028
|
+
let key;
|
|
1029
|
+
try {
|
|
1030
|
+
key = createPrivateKey2(privateKey);
|
|
1031
|
+
} catch {
|
|
1032
|
+
throw new StorePublishingError("invalid_credentials");
|
|
1033
|
+
}
|
|
1034
|
+
if (key.asymmetricKeyType !== "rsa" || (key.asymmetricKeyDetails?.modulusLength ?? 0) < 2048)
|
|
1035
|
+
throw new StorePublishingError("invalid_credentials");
|
|
1036
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
1037
|
+
const iat = Math.floor(now / 1e3);
|
|
1038
|
+
const unsigned = `${encode({ alg: "RS256", typ: "JWT" })}.${encode({ iss: accountId, scopes: ["publishing"], iat, exp: iat + 1200 })}`;
|
|
1039
|
+
return `${unsigned}.${sign3("RSA-SHA256", Buffer.from(unsigned), key).toString("base64url")}`;
|
|
1040
|
+
}
|
|
1041
|
+
function createMeizuHeaders(clientId, clientSecret, uri, accessToken, timestamp = String(Date.now()), traceId = randomUUID()) {
|
|
1042
|
+
const fields = { traceId, clientId, timestamp, uri };
|
|
1043
|
+
const unsigned = Object.entries(fields).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("&");
|
|
1044
|
+
return {
|
|
1045
|
+
traceId,
|
|
1046
|
+
clientId,
|
|
1047
|
+
timestamp,
|
|
1048
|
+
accessToken,
|
|
1049
|
+
sign: createHash2("sha256").update(`${unsigned}:${clientSecret}`).digest("hex")
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
var StorePublishingClient = class {
|
|
1053
|
+
constructor(platform, material, transport = globalThis.fetch, signal) {
|
|
1054
|
+
this.platform = platform;
|
|
1055
|
+
this.material = material;
|
|
1056
|
+
this.transport = transport;
|
|
1057
|
+
this.signal = signal;
|
|
1058
|
+
}
|
|
1059
|
+
token = "";
|
|
1060
|
+
authenticated = false;
|
|
1061
|
+
get authenticationVerified() {
|
|
1062
|
+
return this.authenticated;
|
|
1063
|
+
}
|
|
1064
|
+
headers(uri) {
|
|
1065
|
+
if (!this.token) throw new StorePublishingError("invalid_credentials");
|
|
1066
|
+
const fields = this.material.fields;
|
|
1067
|
+
if (this.platform === "meizu")
|
|
1068
|
+
return createMeizuHeaders(
|
|
1069
|
+
credentialField(fields.clientId),
|
|
1070
|
+
credentialField(fields.clientSecret),
|
|
1071
|
+
uri,
|
|
1072
|
+
this.token
|
|
1073
|
+
);
|
|
1074
|
+
return {
|
|
1075
|
+
Authorization: `Bearer ${this.token}`,
|
|
1076
|
+
...this.platform === "samsung" ? { "service-account-id": credentialField(fields.serviceAccountId) } : {}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
url(uri, query = {}) {
|
|
1080
|
+
const origin = this.platform === "honor" ? "https://appmarket-openapi-drcn.cloud.honor.com" : this.platform === "meizu" ? "https://developer.meizu.com" : "https://devapi.samsungapps.com";
|
|
1081
|
+
const url = new URL(uri, origin);
|
|
1082
|
+
if (url.origin !== origin) throw new StorePublishingError("invalid_response");
|
|
1083
|
+
for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value);
|
|
1084
|
+
return url;
|
|
1085
|
+
}
|
|
1086
|
+
async request(url, init) {
|
|
1087
|
+
this.signal?.throwIfAborted();
|
|
1088
|
+
const controller = new AbortController();
|
|
1089
|
+
const abort = () => controller.abort();
|
|
1090
|
+
this.signal?.addEventListener("abort", abort, { once: true });
|
|
1091
|
+
const timer = setTimeout(abort, 2e4);
|
|
1092
|
+
try {
|
|
1093
|
+
const response = await this.transport(url, {
|
|
1094
|
+
...init,
|
|
1095
|
+
redirect: "error",
|
|
1096
|
+
signal: controller.signal
|
|
1097
|
+
});
|
|
1098
|
+
if (response.status === 401) throw new StorePublishingError("invalid_credentials");
|
|
1099
|
+
if (response.status === 403) throw new StorePublishingError("rejected");
|
|
1100
|
+
if (!response.ok || !response.body) throw new StorePublishingError("network_unavailable");
|
|
1101
|
+
const reader = response.body.getReader();
|
|
1102
|
+
const chunks = [];
|
|
1103
|
+
let size = 0;
|
|
1104
|
+
try {
|
|
1105
|
+
for (; ; ) {
|
|
1106
|
+
const part = await reader.read();
|
|
1107
|
+
if (part.done) break;
|
|
1108
|
+
size += part.value.length;
|
|
1109
|
+
if (size > 1048576) {
|
|
1110
|
+
await reader.cancel();
|
|
1111
|
+
throw new StorePublishingError("invalid_response");
|
|
1112
|
+
}
|
|
1113
|
+
chunks.push(part.value);
|
|
1114
|
+
}
|
|
1115
|
+
} finally {
|
|
1116
|
+
reader.releaseLock();
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1120
|
+
} catch {
|
|
1121
|
+
throw new StorePublishingError("invalid_response");
|
|
1122
|
+
}
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
if (error instanceof StorePublishingError) throw error;
|
|
1125
|
+
throw new StorePublishingError(this.signal?.aborted ? "cancelled" : "network_unavailable");
|
|
1126
|
+
} finally {
|
|
1127
|
+
clearTimeout(timer);
|
|
1128
|
+
this.signal?.removeEventListener("abort", abort);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async authenticate() {
|
|
1132
|
+
const fields = this.material.fields;
|
|
1133
|
+
if (this.platform === "honor") {
|
|
1134
|
+
const response = storeRecord(
|
|
1135
|
+
await this.request(new URL("https://iam.developer.honor.com/auth/token"), {
|
|
1136
|
+
method: "POST",
|
|
1137
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1138
|
+
body: new URLSearchParams({
|
|
1139
|
+
grant_type: "client_credentials",
|
|
1140
|
+
client_id: credentialField(fields.clientId),
|
|
1141
|
+
client_secret: credentialField(fields.clientSecret)
|
|
1142
|
+
})
|
|
1143
|
+
})
|
|
1144
|
+
);
|
|
1145
|
+
this.token = credentialField(response.access_token);
|
|
1146
|
+
} else if (this.platform === "meizu") {
|
|
1147
|
+
const response = storeRecord(
|
|
1148
|
+
await this.request(this.url("/open/api/v1/token"), {
|
|
1149
|
+
method: "GET",
|
|
1150
|
+
headers: {
|
|
1151
|
+
clientId: credentialField(fields.clientId),
|
|
1152
|
+
clientSecret: credentialField(fields.clientSecret)
|
|
1153
|
+
}
|
|
1154
|
+
})
|
|
1155
|
+
);
|
|
1156
|
+
if (response.code !== 200) throw new StorePublishingError("invalid_credentials");
|
|
1157
|
+
this.token = credentialField(storeRecord(response.value).accessToken);
|
|
1158
|
+
} else {
|
|
1159
|
+
const privateKey = this.material.files?.privateKey?.content;
|
|
1160
|
+
if (!privateKey) throw new StorePublishingError("invalid_credentials");
|
|
1161
|
+
const jwt = createSamsungJwt(credentialField(fields.serviceAccountId), privateKey);
|
|
1162
|
+
const response = storeRecord(
|
|
1163
|
+
await this.request(this.url("/auth/accessToken"), {
|
|
1164
|
+
method: "POST",
|
|
1165
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}` }
|
|
1166
|
+
})
|
|
1167
|
+
);
|
|
1168
|
+
if (response.ok !== true) throw new StorePublishingError("invalid_credentials");
|
|
1169
|
+
this.token = credentialField(storeRecord(response.createdItem).accessToken);
|
|
1170
|
+
}
|
|
1171
|
+
this.authenticated = true;
|
|
1172
|
+
}
|
|
1173
|
+
/** 通用请求仅接受本适配器固定域名上的相对路径。 */
|
|
1174
|
+
async json(uri, query = {}, body) {
|
|
1175
|
+
return this.request(this.url(uri, query), {
|
|
1176
|
+
method: body === void 0 ? "GET" : "POST",
|
|
1177
|
+
headers: {
|
|
1178
|
+
...this.headers(uri),
|
|
1179
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
1180
|
+
},
|
|
1181
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
/** 精确包名匹配;列表不完整、多候选或既有应用 ID 冲突时均停止。 */
|
|
1185
|
+
async application(packageName, expectedAppId) {
|
|
1186
|
+
let matches = [];
|
|
1187
|
+
if (this.platform === "honor") {
|
|
1188
|
+
const response = storeRecord(
|
|
1189
|
+
await this.json("/openapi/v1/publish/get-app-id", { pkgName: packageName })
|
|
1190
|
+
);
|
|
1191
|
+
if (response.code !== 0) throw new StorePublishingError("rejected");
|
|
1192
|
+
if (!Array.isArray(response.data)) throw new StorePublishingError("invalid_response");
|
|
1193
|
+
matches = response.data.map(storeRecord).filter((item) => item.packageName === packageName).map((item) => ({ appId: storeIdentifier(item.appId), packageName }));
|
|
1194
|
+
} else if (this.platform === "meizu") {
|
|
1195
|
+
for (let start = 0; ; start += 10) {
|
|
1196
|
+
if (start >= 1e3) throw new StorePublishingError("lookup_limit_exceeded");
|
|
1197
|
+
const response = storeRecord(
|
|
1198
|
+
await this.json("/open/api/v1/app/list", { start: String(start), limit: "10" })
|
|
1199
|
+
);
|
|
1200
|
+
if (response.code !== 200) throw new StorePublishingError("rejected");
|
|
1201
|
+
const value = storeRecord(response.value);
|
|
1202
|
+
if (!Array.isArray(value.data) || !Number.isSafeInteger(value.total) || value.total < 0 || value.data.length > 10)
|
|
1203
|
+
throw new StorePublishingError("invalid_response");
|
|
1204
|
+
matches.push(
|
|
1205
|
+
...value.data.map(storeRecord).filter((item) => item.pkgName === packageName).map((item) => ({ appId: storeIdentifier(item.id), packageName }))
|
|
1206
|
+
);
|
|
1207
|
+
if (start + value.data.length >= value.total) break;
|
|
1208
|
+
if (!value.data.length) throw new StorePublishingError("invalid_response");
|
|
1209
|
+
}
|
|
1210
|
+
} else {
|
|
1211
|
+
const list = expectedAppId ? [{ contentId: expectedAppId }] : await this.json("/seller/contentList");
|
|
1212
|
+
if (!Array.isArray(list)) throw new StorePublishingError("invalid_response");
|
|
1213
|
+
if (list.length > 100) throw new StorePublishingError("lookup_limit_exceeded");
|
|
1214
|
+
for (const item of list) {
|
|
1215
|
+
const contentId = storeIdentifier(storeRecord(item).contentId);
|
|
1216
|
+
if (!/^\d{12}$/u.test(contentId)) throw new StorePublishingError("invalid_response");
|
|
1217
|
+
const details = await this.json("/seller/contentInfo", { contentId });
|
|
1218
|
+
if (!Array.isArray(details) || details.length !== 1)
|
|
1219
|
+
throw new StorePublishingError("invalid_response");
|
|
1220
|
+
const detail = storeRecord(details[0]);
|
|
1221
|
+
if (storeIdentifier(detail.contentId) !== contentId || !Array.isArray(detail.binaryList))
|
|
1222
|
+
throw new StorePublishingError("invalid_response");
|
|
1223
|
+
const packages = new Set(
|
|
1224
|
+
detail.binaryList.map(storeRecord).map((binary) => binary.packageName)
|
|
1225
|
+
);
|
|
1226
|
+
if (packages.has(packageName)) {
|
|
1227
|
+
if (packages.size !== 1) throw new StorePublishingError("application_mismatch");
|
|
1228
|
+
matches.push({ appId: contentId, packageName, detail });
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (!matches.length)
|
|
1233
|
+
throw new StorePublishingError(
|
|
1234
|
+
expectedAppId && this.platform === "samsung" ? "application_mismatch" : "application_not_found"
|
|
1235
|
+
);
|
|
1236
|
+
if (matches.length !== 1) throw new StorePublishingError("ambiguous_application");
|
|
1237
|
+
if (!/^\d{1,32}$/u.test(matches[0].appId)) throw new StorePublishingError("invalid_response");
|
|
1238
|
+
if (expectedAppId && matches[0].appId !== expectedAppId)
|
|
1239
|
+
throw new StorePublishingError("application_mismatch");
|
|
1240
|
+
return matches[0];
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
// src/huawei-authorization.ts
|
|
1245
|
+
import { constants as constants2, createPrivateKey as createPrivateKey3, sign as sign4 } from "crypto";
|
|
1246
|
+
function parseHuaweiServiceAccount(content) {
|
|
1247
|
+
if (Buffer.byteLength(content) > 65536) throw new Error("invalid_service_account");
|
|
1248
|
+
let account;
|
|
1249
|
+
try {
|
|
1250
|
+
account = JSON.parse(content);
|
|
1251
|
+
} catch {
|
|
1252
|
+
throw new Error("invalid_service_account");
|
|
1253
|
+
}
|
|
1254
|
+
if (!account || typeof account !== "object" || Array.isArray(account))
|
|
1255
|
+
throw new Error("invalid_service_account");
|
|
1256
|
+
const value = account;
|
|
1257
|
+
for (const key of ["key_id", "sub_account"]) {
|
|
1258
|
+
if (typeof value[key] !== "string" || !/^[A-Za-z0-9_-]{1,160}$/u.test(value[key]))
|
|
1259
|
+
throw new Error("invalid_service_account");
|
|
1260
|
+
}
|
|
1261
|
+
if (typeof value.private_key !== "string" || !value.private_key.startsWith("-----BEGIN PRIVATE KEY-----"))
|
|
1262
|
+
throw new Error("invalid_service_account");
|
|
1263
|
+
try {
|
|
1264
|
+
const key = createPrivateKey3(value.private_key);
|
|
1265
|
+
if (key.asymmetricKeyType !== "rsa" || (key.asymmetricKeyDetails?.modulusLength ?? 0) < 2048)
|
|
1266
|
+
throw new Error();
|
|
1267
|
+
} catch {
|
|
1268
|
+
throw new Error("invalid_service_account");
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
key_id: value.key_id,
|
|
1272
|
+
sub_account: value.sub_account,
|
|
1273
|
+
private_key: value.private_key
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function createHuaweiServiceAccountToken(content, now = Date.now()) {
|
|
1277
|
+
const account = parseHuaweiServiceAccount(content);
|
|
1278
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
1279
|
+
const iat = Math.floor(now / 1e3);
|
|
1280
|
+
const unsigned = `${encode({ kid: account.key_id, typ: "JWT", alg: "PS256" })}.${encode({
|
|
1281
|
+
aud: "https://oauth-login.cloud.huawei.com/oauth2/v3/token",
|
|
1282
|
+
iss: account.sub_account,
|
|
1283
|
+
iat,
|
|
1284
|
+
exp: iat + 3600
|
|
1285
|
+
})}`;
|
|
1286
|
+
const signature = sign4("sha256", Buffer.from(unsigned), {
|
|
1287
|
+
key: account.private_key,
|
|
1288
|
+
padding: constants2.RSA_PKCS1_PSS_PADDING,
|
|
1289
|
+
saltLength: 32
|
|
1290
|
+
}).toString("base64url");
|
|
1291
|
+
return `${unsigned}.${signature}`;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/publish-preflight.ts
|
|
1295
|
+
var PREFLIGHT_PLATFORMS = [
|
|
1296
|
+
"huawei",
|
|
1297
|
+
"harmony",
|
|
1298
|
+
"pgyer",
|
|
1299
|
+
"honor",
|
|
1300
|
+
"meizu",
|
|
1301
|
+
"samsung",
|
|
1302
|
+
"xiaomi",
|
|
1303
|
+
"oppo",
|
|
1304
|
+
"vivo",
|
|
1305
|
+
"tencent",
|
|
1306
|
+
"app_store",
|
|
1307
|
+
"google_play"
|
|
1308
|
+
];
|
|
1309
|
+
var record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
1310
|
+
var safeField = (value) => typeof value === "string" && value.length > 0 && value.length <= 8192 && !/[\r\n\0]/u.test(value);
|
|
1311
|
+
function assertPublishPackageName(value, platform) {
|
|
1312
|
+
const pattern = platform === "app_store" ? /^[A-Za-z0-9][A-Za-z0-9-]*(?:\.[A-Za-z0-9-]+)+$/u : /^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/u;
|
|
1313
|
+
if (value.length > 256 || !pattern.test(value)) {
|
|
1314
|
+
throw new Error("invalid_package_name");
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
async function preflightPublish(input, store, signal, transport = globalThis.fetch) {
|
|
1318
|
+
assertCredentialProfileName(input.credentialProfile);
|
|
1319
|
+
assertPublishPackageName(input.packageName, input.platform);
|
|
1320
|
+
if (!PREFLIGHT_PLATFORMS.includes(input.platform)) throw new Error("unsupported_platform");
|
|
1321
|
+
if (input.appId !== void 0 && !/^\d{1,32}$/u.test(input.appId))
|
|
1322
|
+
throw new Error("invalid_app_id");
|
|
1323
|
+
const result = {
|
|
1324
|
+
platform: input.platform,
|
|
1325
|
+
credentialProfile: input.credentialProfile,
|
|
1326
|
+
packageName: input.packageName,
|
|
1327
|
+
verified: false,
|
|
1328
|
+
code: "credentials_unavailable",
|
|
1329
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1330
|
+
authentication: "not_verified",
|
|
1331
|
+
application: input.platform === "pgyer" ? "not_required" : "not_verified",
|
|
1332
|
+
uploadPermission: "not_verified"
|
|
1333
|
+
};
|
|
1334
|
+
if (signal?.aborted) return { ...result, code: "cancelled" };
|
|
1335
|
+
const controller = new AbortController();
|
|
1336
|
+
const cancel = () => controller.abort();
|
|
1337
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
1338
|
+
const deadline = setTimeout(cancel, 2e4);
|
|
1339
|
+
try {
|
|
1340
|
+
if (["xiaomi", "oppo", "vivo", "tencent", "app_store", "google_play"].includes(input.platform)) {
|
|
1341
|
+
return await preflightRemainingStore(input, store, result, transport, controller.signal);
|
|
1342
|
+
}
|
|
1343
|
+
if (input.platform === "honor" || input.platform === "meizu" || input.platform === "samsung") {
|
|
1344
|
+
let material;
|
|
1345
|
+
try {
|
|
1346
|
+
material = await store.resolveForLocalExecution({
|
|
1347
|
+
credentialProfile: input.credentialProfile,
|
|
1348
|
+
platform: input.platform,
|
|
1349
|
+
purpose: "verify"
|
|
1350
|
+
});
|
|
1351
|
+
} catch {
|
|
1352
|
+
return result;
|
|
1353
|
+
}
|
|
1354
|
+
const client = new StorePublishingClient(
|
|
1355
|
+
input.platform,
|
|
1356
|
+
material,
|
|
1357
|
+
transport,
|
|
1358
|
+
controller.signal
|
|
1359
|
+
);
|
|
1360
|
+
try {
|
|
1361
|
+
await client.authenticate();
|
|
1362
|
+
result.authentication = "verified";
|
|
1363
|
+
const app = await client.application(input.packageName, input.appId);
|
|
1364
|
+
return {
|
|
1365
|
+
...result,
|
|
1366
|
+
verified: true,
|
|
1367
|
+
code: "verified",
|
|
1368
|
+
appId: app.appId,
|
|
1369
|
+
application: "matched"
|
|
1370
|
+
};
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
return {
|
|
1373
|
+
...result,
|
|
1374
|
+
code: signal?.aborted ? "cancelled" : error instanceof StorePublishingError ? error.code : "network_unavailable"
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
let fields;
|
|
1379
|
+
let serviceAccountJson;
|
|
1380
|
+
try {
|
|
1381
|
+
const material = await store.resolveForLocalExecution({
|
|
1382
|
+
credentialProfile: input.credentialProfile,
|
|
1383
|
+
platform: input.platform,
|
|
1384
|
+
purpose: "verify"
|
|
1385
|
+
});
|
|
1386
|
+
fields = material.fields;
|
|
1387
|
+
serviceAccountJson = material.files?.serviceAccountJson?.content;
|
|
1388
|
+
} catch {
|
|
1389
|
+
return result;
|
|
1390
|
+
}
|
|
1391
|
+
if (controller.signal.aborted)
|
|
1392
|
+
throw new ProbeFailure(signal?.aborted ? "cancelled" : "network_unavailable");
|
|
1393
|
+
if (input.platform === "pgyer") {
|
|
1394
|
+
if (!safeField(fields.apiKey)) return result;
|
|
1395
|
+
const response = await jsonRequest(
|
|
1396
|
+
transport,
|
|
1397
|
+
new URL("https://www.pgyer.com/apiv2/app/listMy"),
|
|
1398
|
+
{
|
|
1399
|
+
method: "POST",
|
|
1400
|
+
signal: controller.signal,
|
|
1401
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1402
|
+
body: new URLSearchParams({ _api_key: fields.apiKey, page: "1" })
|
|
1403
|
+
}
|
|
1404
|
+
);
|
|
1405
|
+
if (!Number.isSafeInteger(response.code)) throw new ProbeFailure("invalid_response");
|
|
1406
|
+
if (response.code !== 0)
|
|
1407
|
+
throw new ProbeFailure(
|
|
1408
|
+
[1001, 1002].includes(Number(response.code)) ? "invalid_credentials" : "rejected"
|
|
1409
|
+
);
|
|
1410
|
+
result.authentication = "verified";
|
|
1411
|
+
} else {
|
|
1412
|
+
let token;
|
|
1413
|
+
if (serviceAccountJson) {
|
|
1414
|
+
try {
|
|
1415
|
+
token = createHuaweiServiceAccountToken(serviceAccountJson);
|
|
1416
|
+
} catch {
|
|
1417
|
+
return result;
|
|
1418
|
+
}
|
|
1419
|
+
} else {
|
|
1420
|
+
if (!safeField(fields.clientId) || !safeField(fields.clientSecret)) return result;
|
|
1421
|
+
const auth = await jsonRequest(
|
|
1422
|
+
transport,
|
|
1423
|
+
new URL("https://connect-api.cloud.huawei.com/api/oauth2/v1/token"),
|
|
1424
|
+
{
|
|
1425
|
+
method: "POST",
|
|
1426
|
+
signal: controller.signal,
|
|
1427
|
+
headers: { "Content-Type": "application/json" },
|
|
1428
|
+
body: JSON.stringify({
|
|
1429
|
+
grant_type: "client_credentials",
|
|
1430
|
+
client_id: fields.clientId,
|
|
1431
|
+
client_secret: fields.clientSecret
|
|
1432
|
+
})
|
|
1433
|
+
}
|
|
1434
|
+
);
|
|
1435
|
+
token = auth.access_token ?? auth.token;
|
|
1436
|
+
}
|
|
1437
|
+
if (!safeField(token)) throw new ProbeFailure("invalid_credentials");
|
|
1438
|
+
if (!serviceAccountJson) result.authentication = "verified";
|
|
1439
|
+
const url = new URL("https://connect-api.cloud.huawei.com/api/publish/v2/appid-list");
|
|
1440
|
+
url.searchParams.set("packageName", input.packageName);
|
|
1441
|
+
url.searchParams.set("packageTypes", input.platform === "harmony" ? "7" : "1");
|
|
1442
|
+
const response = await jsonRequest(transport, url, {
|
|
1443
|
+
method: "GET",
|
|
1444
|
+
signal: controller.signal,
|
|
1445
|
+
headers: {
|
|
1446
|
+
Authorization: `Bearer ${token}`,
|
|
1447
|
+
...!serviceAccountJson ? { client_id: fields.clientId } : {}
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
let ret = response.ret;
|
|
1451
|
+
if (typeof ret === "string") {
|
|
1452
|
+
try {
|
|
1453
|
+
ret = JSON.parse(ret);
|
|
1454
|
+
} catch {
|
|
1455
|
+
throw new ProbeFailure("invalid_response");
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (!record(ret) || ret.code !== 0) throw new ProbeFailure("rejected");
|
|
1459
|
+
result.authentication = "verified";
|
|
1460
|
+
const entries = response.appids ?? [];
|
|
1461
|
+
if (!Array.isArray(entries) || entries.length > 1e3)
|
|
1462
|
+
throw new ProbeFailure("invalid_response");
|
|
1463
|
+
if (!entries.length) throw new ProbeFailure("application_not_found");
|
|
1464
|
+
if (entries.length !== 1) throw new ProbeFailure("ambiguous_application");
|
|
1465
|
+
if (!record(entries[0])) throw new ProbeFailure("invalid_response");
|
|
1466
|
+
const appId = entries[0].value;
|
|
1467
|
+
if (typeof appId !== "string" || !/^\d{1,32}$/u.test(appId))
|
|
1468
|
+
throw new ProbeFailure("invalid_response");
|
|
1469
|
+
if (input.appId && input.appId !== appId) throw new ProbeFailure("application_mismatch");
|
|
1470
|
+
result.appId = appId;
|
|
1471
|
+
result.application = "matched";
|
|
1472
|
+
}
|
|
1473
|
+
if (controller.signal.aborted)
|
|
1474
|
+
throw new ProbeFailure(signal?.aborted ? "cancelled" : "network_unavailable");
|
|
1475
|
+
return { ...result, verified: true, code: "verified" };
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
return {
|
|
1478
|
+
...result,
|
|
1479
|
+
code: signal?.aborted ? "cancelled" : error instanceof ProbeFailure ? error.code : "network_unavailable"
|
|
1480
|
+
};
|
|
1481
|
+
} finally {
|
|
1482
|
+
clearTimeout(deadline);
|
|
1483
|
+
signal?.removeEventListener("abort", cancel);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// src/credential-setup.ts
|
|
1488
|
+
var CREDENTIAL_CONSOLES = {
|
|
1489
|
+
huawei: "https://developer.huawei.com/consumer/cn/hdc-console/index.html#/home/101653523124950083",
|
|
1490
|
+
harmony: "https://developer.huawei.com/consumer/cn/hdc-console/index.html#/home/101653523124950083",
|
|
1491
|
+
pgyer: "https://www.pgyer.com/account/api",
|
|
1492
|
+
honor: "https://developer.honor.com/cn/manageCenter",
|
|
1493
|
+
meizu: "https://developer.meizu.com/",
|
|
1494
|
+
samsung: "https://seller.samsungapps.com/",
|
|
1495
|
+
xiaomi: "https://dev.mi.com/distribute",
|
|
1496
|
+
oppo: "https://open.oppomobile.com/new/ecology/myApi",
|
|
1497
|
+
vivo: "https://dev.vivo.com.cn/personalCenter",
|
|
1498
|
+
tencent: "https://app.open.qq.com/p/app/list",
|
|
1499
|
+
app_store: "https://appstoreconnect.apple.com/access/integrations/api",
|
|
1500
|
+
google_play: "https://console.cloud.google.com/iam-admin/serviceaccounts"
|
|
1501
|
+
};
|
|
1502
|
+
var digest = (value) => createHash3("sha256").update(value).digest("hex");
|
|
1503
|
+
var object = (value) => {
|
|
1504
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1505
|
+
throw new Error("invalid_setup_config");
|
|
1506
|
+
return value;
|
|
1507
|
+
};
|
|
1508
|
+
var setupIdPattern = /^[a-f0-9-]{36}$/u;
|
|
1509
|
+
function credentialPagePlatform(value) {
|
|
1510
|
+
let url;
|
|
1511
|
+
try {
|
|
1512
|
+
url = new URL(value);
|
|
1513
|
+
} catch {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
if (url.protocol !== "https:" || url.port || url.username || url.password) return;
|
|
1517
|
+
if (url.hostname === "dev.mi.com" && /^\/(?:distribute|console)(?:\/|$)/u.test(url.pathname))
|
|
1518
|
+
return "xiaomi";
|
|
1519
|
+
if (url.hostname === "open.oppomobile.com" && /^\/new\/ecology\/myApi(?:\/|$)/u.test(url.pathname))
|
|
1520
|
+
return "oppo";
|
|
1521
|
+
if (url.hostname === "dev.vivo.com.cn" && /^\/personalCenter(?:\/|$)/u.test(url.pathname))
|
|
1522
|
+
return "vivo";
|
|
1523
|
+
if (url.hostname === "app.open.qq.com" && /^\/p\/app(?:\/|$)/u.test(url.pathname))
|
|
1524
|
+
return "tencent";
|
|
1525
|
+
if (url.hostname === "developer.honor.com" && /^\/cn\/(?:manageCenter|console)(?:\/|$)/u.test(url.pathname))
|
|
1526
|
+
return "honor";
|
|
1527
|
+
if (url.hostname === "developer.meizu.com" && !/^\/(?:docs?|open\/api)(?:\/|$)/u.test(url.pathname) || url.hostname === "open.flyme.cn" && /^\/console(?:\/|$)/u.test(url.pathname))
|
|
1528
|
+
return "meizu";
|
|
1529
|
+
if (url.hostname === "seller.samsungapps.com" && url.pathname !== "/galaxyapi/fileUpload")
|
|
1530
|
+
return "samsung";
|
|
1531
|
+
if (["www.pgyer.com", "pgyer.com"].includes(url.hostname) && !url.hash) {
|
|
1532
|
+
if (/^\/account\/api\/?$/u.test(url.pathname) && !url.search) return "pgyer";
|
|
1533
|
+
if (/^\/dashboard\/?$/u.test(url.pathname) && url.searchParams.getAll("account").length === 1 && url.searchParams.get("account") === "api" && [...url.searchParams.keys()].every((key) => ["account", "agKey"].includes(key)) && url.searchParams.getAll("agKey").length <= 1 && (!url.searchParams.has("agKey") || /^[a-f0-9]{32}$/iu.test(url.searchParams.get("agKey"))))
|
|
1534
|
+
return "pgyer";
|
|
1535
|
+
}
|
|
1536
|
+
if (url.hostname === "developer.huawei.com" && /^\/consumer\/(?:cn|en)\/hdc-console\/(?:index\.html)?$/u.test(url.pathname) && !url.search && url.hash === "#/home/101653523124950083")
|
|
1537
|
+
return "huawei";
|
|
1538
|
+
if (url.hostname === "developer.huawei.com" && /^\/consumer\/(?:cn|en)\/(?:service\/josp\/agc\/index\.html|console(?:\/[^?#]*)?)$/u.test(
|
|
1539
|
+
url.pathname
|
|
1540
|
+
))
|
|
1541
|
+
return "huawei";
|
|
1542
|
+
}
|
|
1543
|
+
var CredentialSetupService = class {
|
|
1544
|
+
constructor(store, directory = path2.join(
|
|
1545
|
+
path2.dirname(DEFAULT_CREDENTIAL_VAULT_PATH),
|
|
1546
|
+
"browser-requests"
|
|
1547
|
+
), transport = globalThis.fetch, now = Date.now) {
|
|
1548
|
+
this.store = store;
|
|
1549
|
+
this.directory = directory;
|
|
1550
|
+
this.transport = transport;
|
|
1551
|
+
this.now = now;
|
|
1552
|
+
}
|
|
1553
|
+
async privateDirectory() {
|
|
1554
|
+
await mkdir2(this.directory, { recursive: true, mode: 448 });
|
|
1555
|
+
const info = await lstat2(this.directory);
|
|
1556
|
+
if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== process.getuid?.() || info.mode & 63 || await realpath(this.directory) !== this.directory)
|
|
1557
|
+
throw new Error("unsafe_setup_directory");
|
|
1558
|
+
}
|
|
1559
|
+
async readFile(file) {
|
|
1560
|
+
const handle = await open2(file, constants3.O_RDONLY | constants3.O_NOFOLLOW);
|
|
1561
|
+
try {
|
|
1562
|
+
const info = await handle.stat();
|
|
1563
|
+
if (!info.isFile() || info.size > 262144 || info.uid !== process.getuid?.())
|
|
1564
|
+
throw new Error("unsafe_setup_file");
|
|
1565
|
+
return await handle.readFile("utf8");
|
|
1566
|
+
} finally {
|
|
1567
|
+
await handle.close();
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
filename(id) {
|
|
1571
|
+
if (!setupIdPattern.test(id)) throw new Error("invalid_setup_id");
|
|
1572
|
+
return path2.join(this.directory, `${id}.json`);
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* 从私有目录读取并校验本机任务,不读取凭据。
|
|
1576
|
+
* @param id - 当前任务的 UUID;不接受任意路径。
|
|
1577
|
+
*/
|
|
1578
|
+
async load(id) {
|
|
1579
|
+
await this.privateDirectory();
|
|
1580
|
+
const raw = object(JSON.parse(await this.readFile(this.filename(id))));
|
|
1581
|
+
if (raw.setupId !== id || !PREFLIGHT_PLATFORMS.includes(raw.platform) || !Number.isSafeInteger(raw.expiresAt) || !["waiting_for_browser", "captured", "configured", "cancelled"].includes(raw.status) || typeof raw.configPath !== "string" || !path2.isAbsolute(raw.configPath) || typeof raw.configDigest !== "string" || typeof raw.credentialProfile !== "string" || raw.previousProfile !== void 0 && typeof raw.previousProfile !== "string" || typeof raw.packageName !== "string" || !/^[a-f0-9]{64}$/u.test(raw.configDigest)) {
|
|
1582
|
+
throw new Error("invalid_setup_record");
|
|
1583
|
+
}
|
|
1584
|
+
assertCredentialProfileName(raw.credentialProfile);
|
|
1585
|
+
if (raw.previousProfile !== void 0) assertCredentialProfileName(raw.previousProfile);
|
|
1586
|
+
assertPublishPackageName(raw.packageName, raw.platform);
|
|
1587
|
+
return raw;
|
|
1588
|
+
}
|
|
1589
|
+
status(task) {
|
|
1590
|
+
return {
|
|
1591
|
+
setupId: task.setupId,
|
|
1592
|
+
platform: task.platform,
|
|
1593
|
+
credentialProfile: task.credentialProfile,
|
|
1594
|
+
packageName: task.packageName,
|
|
1595
|
+
status: task.status === "configured" || task.status === "cancelled" ? task.status : task.expiresAt <= this.now() ? "expired" : task.status,
|
|
1596
|
+
expiresAt: task.expiresAt,
|
|
1597
|
+
consoleUrl: CREDENTIAL_CONSOLES[task.platform]
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
async save(task, exclusive = false) {
|
|
1601
|
+
const filename = this.filename(task.setupId);
|
|
1602
|
+
if (exclusive) {
|
|
1603
|
+
const handle2 = await open2(filename, "wx", 384);
|
|
1604
|
+
try {
|
|
1605
|
+
await handle2.writeFile(JSON.stringify(task));
|
|
1606
|
+
} finally {
|
|
1607
|
+
await handle2.close();
|
|
1608
|
+
}
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const temp = `${filename}.${randomUUID2()}.tmp`;
|
|
1612
|
+
const handle = await open2(temp, "wx", 384);
|
|
1613
|
+
try {
|
|
1614
|
+
await handle.writeFile(JSON.stringify(task));
|
|
1615
|
+
} finally {
|
|
1616
|
+
await handle.close();
|
|
1617
|
+
}
|
|
1618
|
+
try {
|
|
1619
|
+
await rename2(temp, filename);
|
|
1620
|
+
} finally {
|
|
1621
|
+
await unlink2(temp).catch(() => {
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
async locked(id, operation) {
|
|
1626
|
+
await this.privateDirectory();
|
|
1627
|
+
const file = `${this.filename(id)}.lock`;
|
|
1628
|
+
let handle;
|
|
1629
|
+
try {
|
|
1630
|
+
handle = await open2(file, "wx", 384);
|
|
1631
|
+
} catch {
|
|
1632
|
+
throw new Error("credential_setup_busy");
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
return await operation();
|
|
1636
|
+
} finally {
|
|
1637
|
+
await handle.close();
|
|
1638
|
+
await unlink2(file);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* 创建十分钟任务或复用已有 Profile;不读取浏览器、秘密或修改配置。
|
|
1643
|
+
* @param input - 已保存的目标渠道与配置身份,不要求启用上传;更换账号须显式 refreshExisting。
|
|
1644
|
+
*/
|
|
1645
|
+
async begin(input) {
|
|
1646
|
+
if (!PREFLIGHT_PLATFORMS.includes(input.platform)) throw new Error("unsupported_platform");
|
|
1647
|
+
assertPublishPackageName(input.packageName, input.platform);
|
|
1648
|
+
const configPath = path2.resolve(input.configPath);
|
|
1649
|
+
if (!["flu_release.yaml", "flu_release.yml", "app-ship.yaml", "app-ship.yml"].includes(
|
|
1650
|
+
path2.basename(configPath)
|
|
1651
|
+
)) {
|
|
1652
|
+
throw new Error("unsupported_setup_config");
|
|
1653
|
+
}
|
|
1654
|
+
if (await realpath(path2.dirname(configPath)) !== path2.dirname(configPath))
|
|
1655
|
+
throw new Error("unsafe_setup_config");
|
|
1656
|
+
const raw = await this.readFile(configPath);
|
|
1657
|
+
const config = object(yaml.load(raw));
|
|
1658
|
+
const publish = object(config.publish);
|
|
1659
|
+
const target = object(publish[input.platform]);
|
|
1660
|
+
if (target.packageName !== input.packageName) throw new Error("setup_target_mismatch");
|
|
1661
|
+
if (target.credentialProfile !== void 0) {
|
|
1662
|
+
if (typeof target.credentialProfile !== "string") throw new Error("invalid_setup_config");
|
|
1663
|
+
assertCredentialProfileName(target.credentialProfile);
|
|
1664
|
+
}
|
|
1665
|
+
if (target.auth || [
|
|
1666
|
+
"apiKey",
|
|
1667
|
+
"token",
|
|
1668
|
+
"userName",
|
|
1669
|
+
"privateKey",
|
|
1670
|
+
"publicKeyPath",
|
|
1671
|
+
"serviceAccountPath",
|
|
1672
|
+
"privateKeyPath",
|
|
1673
|
+
"appleId",
|
|
1674
|
+
"appPassword",
|
|
1675
|
+
"issuerId",
|
|
1676
|
+
"keyId",
|
|
1677
|
+
"clientId",
|
|
1678
|
+
"clientSecret",
|
|
1679
|
+
"accessKey",
|
|
1680
|
+
"accessSecret",
|
|
1681
|
+
"userId"
|
|
1682
|
+
].some((key) => target[key]))
|
|
1683
|
+
throw new Error("legacy_credentials_require_migration");
|
|
1684
|
+
await this.privateDirectory();
|
|
1685
|
+
const setupId = randomUUID2();
|
|
1686
|
+
const credentialProfile = input.refreshExisting ? `browser-${input.platform}-${setupId}` : input.credentialProfile ?? target.credentialProfile ?? `browser-${input.platform}-${setupId}`;
|
|
1687
|
+
assertCredentialProfileName(credentialProfile);
|
|
1688
|
+
if (!input.refreshExisting && target.credentialProfile && target.credentialProfile !== credentialProfile)
|
|
1689
|
+
throw new Error("credential_profile_conflict");
|
|
1690
|
+
const existing = await this.store.getMetadata(credentialProfile);
|
|
1691
|
+
if (existing && existing.platform !== input.platform)
|
|
1692
|
+
throw new Error("credential_profile_platform_mismatch");
|
|
1693
|
+
const task = {
|
|
1694
|
+
setupId,
|
|
1695
|
+
platform: input.platform,
|
|
1696
|
+
credentialProfile,
|
|
1697
|
+
packageName: input.packageName,
|
|
1698
|
+
status: existing?.configured ? "captured" : "waiting_for_browser",
|
|
1699
|
+
expiresAt: this.now() + 10 * 6e4,
|
|
1700
|
+
configPath,
|
|
1701
|
+
configDigest: digest(raw),
|
|
1702
|
+
...target.credentialProfile ? { previousProfile: target.credentialProfile } : {}
|
|
1703
|
+
};
|
|
1704
|
+
await this.save(task, true);
|
|
1705
|
+
return this.status(task);
|
|
1706
|
+
}
|
|
1707
|
+
/** 状态读取没有隐式保存、网络请求或补配副作用。 */
|
|
1708
|
+
async inspect(id) {
|
|
1709
|
+
return this.status(await this.load(id));
|
|
1710
|
+
}
|
|
1711
|
+
/** 扩展只处理当前站点的唯一待采集请求,多请求时要求调用方明确选择。 */
|
|
1712
|
+
async pending(pageUrl) {
|
|
1713
|
+
const platform = credentialPagePlatform(pageUrl);
|
|
1714
|
+
if (!platform) throw new Error("unsupported_credential_page");
|
|
1715
|
+
return (await this.waiting()).filter(
|
|
1716
|
+
(task) => task.platform === platform || platform === "huawei" && task.platform === "harmony"
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
/** 仅列出需要已有文件的六渠道待处理任务,不读取网页或密钥文件。 */
|
|
1720
|
+
async pendingImports() {
|
|
1721
|
+
return (await this.waiting()).filter(
|
|
1722
|
+
(task) => ["huawei", "harmony", "samsung", "xiaomi", "app_store", "google_play"].includes(
|
|
1723
|
+
task.platform
|
|
1724
|
+
)
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
async waiting() {
|
|
1728
|
+
await this.privateDirectory();
|
|
1729
|
+
const files = await readdir(this.directory);
|
|
1730
|
+
if (files.length > 1e3) throw new Error("setup_request_limit");
|
|
1731
|
+
const tasks = [];
|
|
1732
|
+
for (const file of files.filter((name) => /^[a-f0-9-]{36}\.json$/u.test(name))) {
|
|
1733
|
+
const task = await this.inspect(file.slice(0, -5));
|
|
1734
|
+
if (task.status === "waiting_for_browser") tasks.push(task);
|
|
1735
|
+
}
|
|
1736
|
+
return tasks;
|
|
1737
|
+
}
|
|
1738
|
+
/** 仅供受信任 Native Host;材料不进入 MCP,且不能覆盖已有 Profile 或创建商店密钥。 */
|
|
1739
|
+
async capture(id, pageUrl, input) {
|
|
1740
|
+
return this.locked(id, async () => {
|
|
1741
|
+
const task = await this.load(id);
|
|
1742
|
+
if (this.status(task).status !== "waiting_for_browser")
|
|
1743
|
+
throw new Error("credential_setup_not_pending");
|
|
1744
|
+
const pagePlatform = credentialPagePlatform(pageUrl);
|
|
1745
|
+
if (pagePlatform !== task.platform && !(pagePlatform === "huawei" && task.platform === "harmony"))
|
|
1746
|
+
throw new Error("credential_origin_mismatch");
|
|
1747
|
+
return this.saveCaptured(task, input);
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
/** 用户在扩展自身的文件选择器选择已有 JSON;材料不会进入商店页面或模型参数。 */
|
|
1751
|
+
async importServiceAccount(id, content) {
|
|
1752
|
+
return this.locked(id, async () => {
|
|
1753
|
+
const task = await this.load(id);
|
|
1754
|
+
if (!["huawei", "harmony"].includes(task.platform) || this.status(task).status !== "waiting_for_browser")
|
|
1755
|
+
throw new Error("credential_setup_not_pending");
|
|
1756
|
+
return this.saveCaptured(task, { serviceAccountJson: content });
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
/** 三星账号 ID 与已有私钥必须在同一任务中一起导入;不在网页上生成新密钥。 */
|
|
1760
|
+
async importSamsungPrivateKey(id, serviceAccountId, content) {
|
|
1761
|
+
return this.locked(id, async () => {
|
|
1762
|
+
const task = await this.load(id);
|
|
1763
|
+
if (task.platform !== "samsung" || this.status(task).status !== "waiting_for_browser")
|
|
1764
|
+
throw new Error("credential_setup_not_pending");
|
|
1765
|
+
if (Buffer.byteLength(content) > 65536) throw new Error("invalid_private_key");
|
|
1766
|
+
createSamsungJwt(serviceAccountId, content);
|
|
1767
|
+
await this.store.saveProfile({
|
|
1768
|
+
platform: "samsung",
|
|
1769
|
+
credentialProfile: task.credentialProfile,
|
|
1770
|
+
material: { fields: { serviceAccountId }, files: { privateKey: { content } } }
|
|
1771
|
+
});
|
|
1772
|
+
task.status = "captured";
|
|
1773
|
+
await this.save(task);
|
|
1774
|
+
return this.status(task);
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* 导入 Apple/Google/小米已有文件并配对账号字段;仅受信任 Native Host 调用。
|
|
1779
|
+
* @param id - 当前等待采集的本机任务,平台不能由文件覆盖。
|
|
1780
|
+
* @param content - 用户已下载文件的内存内容,不是路径或网络地址。
|
|
1781
|
+
* @param fields - 当前平台所需的配对字段;Google 必须为空对象。
|
|
1782
|
+
*/
|
|
1783
|
+
async importCredentialFile(id, content, fields) {
|
|
1784
|
+
return this.locked(id, async () => {
|
|
1785
|
+
const task = await this.load(id);
|
|
1786
|
+
if (this.status(task).status !== "waiting_for_browser")
|
|
1787
|
+
throw new Error("credential_setup_not_pending");
|
|
1788
|
+
const material = importedCredentialMaterial(task.platform, content, fields);
|
|
1789
|
+
await this.store.saveProfile({
|
|
1790
|
+
platform: task.platform,
|
|
1791
|
+
credentialProfile: task.credentialProfile,
|
|
1792
|
+
material
|
|
1793
|
+
});
|
|
1794
|
+
task.status = "captured";
|
|
1795
|
+
await this.save(task);
|
|
1796
|
+
return this.status(task);
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* 校验平台专属字段并写入不可覆盖的安全档案,文件型渠道须使用导入入口。
|
|
1801
|
+
* @param task - 已锁定且仍在等待采集的当前任务。
|
|
1802
|
+
* @param input - 浏览器经本机管道提交的材料,不允许额外或被遮盖字段。
|
|
1803
|
+
*/
|
|
1804
|
+
async saveCaptured(task, input) {
|
|
1805
|
+
const fields = object(input);
|
|
1806
|
+
if (["huawei", "harmony"].includes(task.platform) && Object.keys(fields).join() === "serviceAccountJson" && typeof fields.serviceAccountJson === "string") {
|
|
1807
|
+
const account = parseHuaweiServiceAccount(fields.serviceAccountJson);
|
|
1808
|
+
await this.store.saveProfile({
|
|
1809
|
+
platform: task.platform,
|
|
1810
|
+
credentialProfile: task.credentialProfile,
|
|
1811
|
+
material: {
|
|
1812
|
+
fields: {},
|
|
1813
|
+
files: { serviceAccountJson: { content: JSON.stringify(account) } }
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
task.status = "captured";
|
|
1817
|
+
await this.save(task);
|
|
1818
|
+
return this.status(task);
|
|
1819
|
+
}
|
|
1820
|
+
if (["samsung", "xiaomi", "app_store", "google_play"].includes(task.platform))
|
|
1821
|
+
throw new Error("credential_file_required");
|
|
1822
|
+
const expected = task.platform === "vivo" ? ["accessKey", "accessSecret"] : task.platform === "tencent" ? ["userId", "appId", "accessSecret"] : task.platform !== "pgyer" ? ["clientId", "clientSecret"] : ["apiKey"];
|
|
1823
|
+
if (Object.keys(fields).length !== expected.length || expected.some(
|
|
1824
|
+
(key) => typeof fields[key] !== "string" || !fields[key].trim() || fields[key].length > 8192 || /[\r\n\0*•●]/u.test(fields[key])
|
|
1825
|
+
))
|
|
1826
|
+
throw new Error("credentials_not_visible");
|
|
1827
|
+
await this.store.saveProfile({
|
|
1828
|
+
platform: task.platform,
|
|
1829
|
+
credentialProfile: task.credentialProfile,
|
|
1830
|
+
material: { fields: Object.fromEntries(expected.map((key) => [key, fields[key]])) }
|
|
1831
|
+
});
|
|
1832
|
+
task.status = "captured";
|
|
1833
|
+
await this.save(task);
|
|
1834
|
+
return this.status(task);
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* 核验成功后补入 Profile/App ID 并同步已有表单快照;配置变化、冲突或取消停止写入。
|
|
1838
|
+
* @param id - 本机采集任务标识,必须处于已采集状态或已经完成。
|
|
1839
|
+
* @param signal - 可选取消信号,核验及配置落盘前再次检查。
|
|
1840
|
+
* @param assertCurrent - 可选宿主检查,在原子替换前确认当前项目未改变。
|
|
1841
|
+
*/
|
|
1842
|
+
async complete(id, signal, assertCurrent) {
|
|
1843
|
+
return this.locked(id, async () => {
|
|
1844
|
+
const task = await this.load(id);
|
|
1845
|
+
if (await realpath(path2.dirname(task.configPath)) !== path2.dirname(task.configPath))
|
|
1846
|
+
throw new Error("unsafe_setup_config");
|
|
1847
|
+
if (this.status(task).status === "configured") {
|
|
1848
|
+
const target2 = object(
|
|
1849
|
+
object(object(yaml.load(await this.readFile(task.configPath))).publish)[task.platform]
|
|
1850
|
+
);
|
|
1851
|
+
if (target2.credentialProfile !== task.credentialProfile || target2.packageName !== task.packageName) {
|
|
1852
|
+
throw new Error("configuration_changed");
|
|
1853
|
+
}
|
|
1854
|
+
return { ...this.status(task), code: "already_configured" };
|
|
1855
|
+
}
|
|
1856
|
+
if (this.status(task).status !== "captured") throw new Error("credential_setup_not_captured");
|
|
1857
|
+
const raw = await this.readFile(task.configPath);
|
|
1858
|
+
if (digest(raw) !== task.configDigest) throw new Error("configuration_changed");
|
|
1859
|
+
const config = object(yaml.load(raw));
|
|
1860
|
+
const target = object(object(config.publish)[task.platform]);
|
|
1861
|
+
const probe = await preflightPublish(
|
|
1862
|
+
{
|
|
1863
|
+
platform: task.platform,
|
|
1864
|
+
credentialProfile: task.credentialProfile,
|
|
1865
|
+
packageName: task.packageName,
|
|
1866
|
+
...task.platform !== "pgyer" && target.appId ? { appId: String(target.appId) } : {}
|
|
1867
|
+
},
|
|
1868
|
+
this.store,
|
|
1869
|
+
signal,
|
|
1870
|
+
this.transport
|
|
1871
|
+
);
|
|
1872
|
+
if (!probe.verified) return { ...this.status(task), preflight: probe };
|
|
1873
|
+
if (signal?.aborted || task.expiresAt <= this.now())
|
|
1874
|
+
throw new Error("credential_setup_expired");
|
|
1875
|
+
const lockFile = `${task.configPath}.credential-setup.lock`;
|
|
1876
|
+
const lock = await open2(lockFile, "wx", 384);
|
|
1877
|
+
let temporary;
|
|
1878
|
+
try {
|
|
1879
|
+
if (digest(await this.readFile(task.configPath)) !== task.configDigest)
|
|
1880
|
+
throw new Error("configuration_changed");
|
|
1881
|
+
if (target.credentialProfile && target.credentialProfile !== task.credentialProfile && target.credentialProfile !== task.previousProfile)
|
|
1882
|
+
throw new Error("credential_profile_conflict");
|
|
1883
|
+
target.credentialProfile = task.credentialProfile;
|
|
1884
|
+
if (probe.appId) target.appId = probe.appId;
|
|
1885
|
+
if (config.publishCenter?.channelOptions) {
|
|
1886
|
+
const options = config.publishCenter.channelOptions[task.platform] ||= {};
|
|
1887
|
+
options.credentialProfile = task.credentialProfile;
|
|
1888
|
+
options.packageName = task.packageName;
|
|
1889
|
+
if (probe.appId) options.appId = probe.appId;
|
|
1890
|
+
}
|
|
1891
|
+
temporary = `${task.configPath}.${randomUUID2()}.tmp`;
|
|
1892
|
+
const handle = await open2(temporary, "wx", 384);
|
|
1893
|
+
try {
|
|
1894
|
+
await handle.writeFile(yaml.dump(config, { noRefs: true, lineWidth: -1 }));
|
|
1895
|
+
} finally {
|
|
1896
|
+
await handle.close();
|
|
1897
|
+
}
|
|
1898
|
+
if (signal?.aborted || task.expiresAt <= this.now() || digest(await this.readFile(task.configPath)) !== task.configDigest) {
|
|
1899
|
+
throw new Error("configuration_changed");
|
|
1900
|
+
}
|
|
1901
|
+
assertCurrent?.();
|
|
1902
|
+
signal?.throwIfAborted();
|
|
1903
|
+
await rename2(temporary, task.configPath);
|
|
1904
|
+
task.status = "configured";
|
|
1905
|
+
await this.save(task);
|
|
1906
|
+
} finally {
|
|
1907
|
+
if (temporary) await unlink2(temporary).catch(() => {
|
|
1908
|
+
});
|
|
1909
|
+
await lock.close();
|
|
1910
|
+
await unlink2(lockFile);
|
|
1911
|
+
}
|
|
1912
|
+
return { ...this.status(task), preflight: probe };
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
/** 取消只关闭请求,已保存的 Profile 保留;绝不隐式删除用户凭据。 */
|
|
1916
|
+
async cancel(id) {
|
|
1917
|
+
return this.locked(id, async () => {
|
|
1918
|
+
const task = await this.load(id);
|
|
1919
|
+
if (task.status !== "configured") {
|
|
1920
|
+
task.status = "cancelled";
|
|
1921
|
+
await this.save(task);
|
|
1922
|
+
}
|
|
1923
|
+
return this.status(task);
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
// src/browser-native.ts
|
|
1929
|
+
async function trustedOrigin(origin) {
|
|
1930
|
+
const file = path3.join(path3.dirname(DEFAULT_CREDENTIAL_VAULT_PATH), "browser-connector.v1.json");
|
|
1931
|
+
const info = await lstat3(file);
|
|
1932
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 4096 || info.uid !== process.getuid?.() || info.mode & 63)
|
|
1933
|
+
throw new Error("connector_not_registered");
|
|
1934
|
+
const config = JSON.parse(await readFile2(file, "utf8"));
|
|
1935
|
+
if (!/^[a-p]{32}$/u.test(config.extensionId) || origin !== `chrome-extension://${config.extensionId}/`) {
|
|
1936
|
+
throw new Error("connector_origin_rejected");
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
async function main() {
|
|
1940
|
+
await trustedOrigin(process.argv[2]);
|
|
1941
|
+
const service = new CredentialSetupService(createDefaultCredentialProfileStore());
|
|
1942
|
+
const message = await new Promise((resolve, reject) => {
|
|
1943
|
+
let buffer = Buffer.alloc(0);
|
|
1944
|
+
const timer = setTimeout(() => reject(new Error("native_message_timeout")), 5e3);
|
|
1945
|
+
process.stdin.on("data", (chunk) => {
|
|
1946
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
1947
|
+
if (buffer.length > 65540) {
|
|
1948
|
+
clearTimeout(timer);
|
|
1949
|
+
reject(new Error("native_message_too_large"));
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (buffer.length < 4) return;
|
|
1953
|
+
const size = buffer.readUInt32LE(0);
|
|
1954
|
+
if (!size || size > 65536) {
|
|
1955
|
+
clearTimeout(timer);
|
|
1956
|
+
reject(new Error("invalid_native_message"));
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (buffer.length < size + 4) return;
|
|
1960
|
+
clearTimeout(timer);
|
|
1961
|
+
process.stdin.pause();
|
|
1962
|
+
try {
|
|
1963
|
+
if (buffer.length !== size + 4) throw new Error("invalid_native_message");
|
|
1964
|
+
resolve(JSON.parse(buffer.subarray(4).toString("utf8")));
|
|
1965
|
+
} catch {
|
|
1966
|
+
reject(new Error("invalid_native_message"));
|
|
1967
|
+
} finally {
|
|
1968
|
+
buffer.fill(0);
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
process.stdin.once("end", () => {
|
|
1972
|
+
clearTimeout(timer);
|
|
1973
|
+
reject(new Error("incomplete_native_message"));
|
|
1974
|
+
});
|
|
1975
|
+
process.stdin.once("error", () => {
|
|
1976
|
+
clearTimeout(timer);
|
|
1977
|
+
reject(new Error("invalid_native_message"));
|
|
1978
|
+
});
|
|
1979
|
+
});
|
|
1980
|
+
if (!message || typeof message !== "object" || Array.isArray(message))
|
|
1981
|
+
throw new Error("invalid_native_message");
|
|
1982
|
+
const input = message;
|
|
1983
|
+
const keys = input.operation === "pending_imports" ? ["operation"] : input.operation === "import_file" ? ["operation", "setupId", "content", "serviceAccountId", "fields"] : input.operation === "pending" ? ["operation", "pageUrl"] : ["operation", "pageUrl", "setupId", "fields"];
|
|
1984
|
+
if (Object.keys(input).some((key) => !keys.includes(key))) {
|
|
1985
|
+
throw new Error("invalid_native_message");
|
|
1986
|
+
}
|
|
1987
|
+
if (input.operation === "pending_imports") return { requests: await service.pendingImports() };
|
|
1988
|
+
if (input.operation === "import_file" && typeof input.setupId === "string" && typeof input.content === "string") {
|
|
1989
|
+
if (input.fields !== void 0) {
|
|
1990
|
+
if (!input.fields || typeof input.fields !== "object" || Array.isArray(input.fields) || input.serviceAccountId !== void 0)
|
|
1991
|
+
throw new Error("invalid_native_message");
|
|
1992
|
+
return service.importCredentialFile(
|
|
1993
|
+
input.setupId,
|
|
1994
|
+
input.content,
|
|
1995
|
+
input.fields
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
if (typeof input.serviceAccountId === "string")
|
|
1999
|
+
return service.importSamsungPrivateKey(input.setupId, input.serviceAccountId, input.content);
|
|
2000
|
+
return service.importServiceAccount(input.setupId, input.content);
|
|
2001
|
+
}
|
|
2002
|
+
if (typeof input.pageUrl !== "string" || input.pageUrl.length > 4096)
|
|
2003
|
+
throw new Error("invalid_native_message");
|
|
2004
|
+
if (input.operation === "pending") return { requests: await service.pending(input.pageUrl) };
|
|
2005
|
+
if (input.operation === "capture" && typeof input.setupId === "string") {
|
|
2006
|
+
return service.capture(input.setupId, input.pageUrl, input.fields);
|
|
2007
|
+
}
|
|
2008
|
+
throw new Error("invalid_native_message");
|
|
2009
|
+
}
|
|
2010
|
+
function respond(value) {
|
|
2011
|
+
const body = Buffer.from(JSON.stringify(value));
|
|
2012
|
+
const header = Buffer.alloc(4);
|
|
2013
|
+
header.writeUInt32LE(body.length);
|
|
2014
|
+
process.stdout.end(Buffer.concat([header, body]));
|
|
2015
|
+
}
|
|
2016
|
+
main().then(
|
|
2017
|
+
(result) => respond({ ok: true, result }),
|
|
2018
|
+
(error) => respond({
|
|
2019
|
+
ok: false,
|
|
2020
|
+
error: [
|
|
2021
|
+
"invalid_credential_file",
|
|
2022
|
+
"invalid_service_account",
|
|
2023
|
+
"invalid_apple_key",
|
|
2024
|
+
"invalid_public_key",
|
|
2025
|
+
"invalid_import_fields",
|
|
2026
|
+
"credential_file_required",
|
|
2027
|
+
"credential_setup_not_pending"
|
|
2028
|
+
].includes(error?.message) ? error.message : "credential_capture_failed"
|
|
2029
|
+
})
|
|
2030
|
+
).finally(() => process.stdin.destroy());
|
|
2031
|
+
//# sourceMappingURL=browser-native.js.map
|