rn-update-cli 1.0.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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/README.zh-CN.md +167 -0
  4. package/cli.json +525 -0
  5. package/lib/api.d.ts +56 -0
  6. package/lib/api.js +671 -0
  7. package/lib/app.d.ts +107 -0
  8. package/lib/app.js +329 -0
  9. package/lib/auto-update.d.ts +35 -0
  10. package/lib/auto-update.js +507 -0
  11. package/lib/bin.d.ts +2 -0
  12. package/lib/bin.js +133 -0
  13. package/lib/bundle-pack.d.ts +6 -0
  14. package/lib/bundle-pack.js +91 -0
  15. package/lib/bundle-runner.d.ts +168 -0
  16. package/lib/bundle-runner.js +1305 -0
  17. package/lib/bundle.d.ts +45 -0
  18. package/lib/bundle.js +226 -0
  19. package/lib/cache.d.ts +10 -0
  20. package/lib/cache.js +25 -0
  21. package/lib/commands.d.ts +11 -0
  22. package/lib/commands.js +72 -0
  23. package/lib/diff.d.ts +18 -0
  24. package/lib/diff.js +690 -0
  25. package/lib/exports.d.ts +11 -0
  26. package/lib/exports.js +20 -0
  27. package/lib/install.d.ts +9 -0
  28. package/lib/install.js +51 -0
  29. package/lib/locales/en.d.ts +187 -0
  30. package/lib/locales/en.js +194 -0
  31. package/lib/locales/zh.d.ts +187 -0
  32. package/lib/locales/zh.js +194 -0
  33. package/lib/native-package.d.ts +21 -0
  34. package/lib/native-package.js +298 -0
  35. package/lib/package.d.ts +68 -0
  36. package/lib/package.js +487 -0
  37. package/lib/provider.d.ts +24 -0
  38. package/lib/provider.js +244 -0
  39. package/lib/standalone-publish.d.ts +17 -0
  40. package/lib/standalone-publish.js +433 -0
  41. package/lib/symbolicate.d.ts +35 -0
  42. package/lib/symbolicate.js +130 -0
  43. package/lib/types.d.ts +152 -0
  44. package/lib/types.js +2 -0
  45. package/lib/user.d.ts +8 -0
  46. package/lib/user.js +50 -0
  47. package/lib/utils/add-gitignore.d.ts +6 -0
  48. package/lib/utils/add-gitignore.js +44 -0
  49. package/lib/utils/app-info-parser/aab.d.ts +27 -0
  50. package/lib/utils/app-info-parser/aab.js +256 -0
  51. package/lib/utils/app-info-parser/apk.d.ts +19 -0
  52. package/lib/utils/app-info-parser/apk.js +95 -0
  53. package/lib/utils/app-info-parser/app.d.ts +9 -0
  54. package/lib/utils/app-info-parser/app.js +15 -0
  55. package/lib/utils/app-info-parser/index.d.ts +21 -0
  56. package/lib/utils/app-info-parser/index.js +51 -0
  57. package/lib/utils/app-info-parser/ipa.d.ts +19 -0
  58. package/lib/utils/app-info-parser/ipa.js +101 -0
  59. package/lib/utils/app-info-parser/resource-finder.d.ts +49 -0
  60. package/lib/utils/app-info-parser/resource-finder.js +429 -0
  61. package/lib/utils/app-info-parser/utils.d.ts +36 -0
  62. package/lib/utils/app-info-parser/utils.js +140 -0
  63. package/lib/utils/app-info-parser/xml-parser/binary.d.ts +61 -0
  64. package/lib/utils/app-info-parser/xml-parser/binary.js +577 -0
  65. package/lib/utils/app-info-parser/xml-parser/manifest.d.ts +15 -0
  66. package/lib/utils/app-info-parser/xml-parser/manifest.js +207 -0
  67. package/lib/utils/app-info-parser/zip.d.ts +29 -0
  68. package/lib/utils/app-info-parser/zip.js +97 -0
  69. package/lib/utils/check-lockfile.d.ts +6 -0
  70. package/lib/utils/check-lockfile.js +93 -0
  71. package/lib/utils/check-plugin.d.ts +12 -0
  72. package/lib/utils/check-plugin.js +36 -0
  73. package/lib/utils/command-result.d.ts +8 -0
  74. package/lib/utils/command-result.js +27 -0
  75. package/lib/utils/constants.d.ts +10 -0
  76. package/lib/utils/constants.js +39 -0
  77. package/lib/utils/dep-versions.d.ts +20 -0
  78. package/lib/utils/dep-versions.js +129 -0
  79. package/lib/utils/git.d.ts +14 -0
  80. package/lib/utils/git.js +64 -0
  81. package/lib/utils/hbcTransform.d.ts +111 -0
  82. package/lib/utils/hbcTransform.js +561 -0
  83. package/lib/utils/hermes-base.d.ts +227 -0
  84. package/lib/utils/hermes-base.js +1287 -0
  85. package/lib/utils/hermes-literals.d.ts +64 -0
  86. package/lib/utils/hermes-literals.js +260 -0
  87. package/lib/utils/http-helper.d.ts +10 -0
  88. package/lib/utils/http-helper.js +150 -0
  89. package/lib/utils/i18n.d.ts +22 -0
  90. package/lib/utils/i18n.js +46 -0
  91. package/lib/utils/index.d.ts +67 -0
  92. package/lib/utils/index.js +545 -0
  93. package/lib/utils/latest-version/index.d.ts +154 -0
  94. package/lib/utils/latest-version/index.js +331 -0
  95. package/lib/utils/options.d.ts +10 -0
  96. package/lib/utils/options.js +68 -0
  97. package/lib/utils/plugin-config.d.ts +14 -0
  98. package/lib/utils/plugin-config.js +66 -0
  99. package/lib/utils/runtime.d.ts +54 -0
  100. package/lib/utils/runtime.js +263 -0
  101. package/lib/utils/slim-sourcemap.d.ts +28 -0
  102. package/lib/utils/slim-sourcemap.js +106 -0
  103. package/lib/utils/zip-entries.d.ts +10 -0
  104. package/lib/utils/zip-entries.js +211 -0
  105. package/lib/utils/zip-options.d.ts +17 -0
  106. package/lib/utils/zip-options.js +190 -0
  107. package/lib/utils/zip-range.d.ts +148 -0
  108. package/lib/utils/zip-range.js +581 -0
  109. package/lib/utils/zip-raw-writer.d.ts +52 -0
  110. package/lib/utils/zip-raw-writer.js +209 -0
  111. package/lib/version-operations.d.ts +21 -0
  112. package/lib/version-operations.js +320 -0
  113. package/lib/versions.d.ts +66 -0
  114. package/lib/versions.js +402 -0
  115. package/package.json +113 -0
  116. package/proto/Configuration.proto +183 -0
  117. package/proto/Resources.proto +569 -0
package/lib/api.js ADDED
@@ -0,0 +1,671 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getAllPackages = exports.doDelete = exports.put = exports.post = exports.get = exports.closeSession = exports.saveSession = exports.loadSession = exports.replaceSession = exports.setApiToken = exports.getApiToken = exports.getSession = void 0;
7
+ exports.unwrapData = unwrapData;
8
+ exports.servicePath = servicePath;
9
+ exports.redactRequestUrl = redactRequestUrl;
10
+ exports.getMembershipEntitlement = getMembershipEntitlement;
11
+ exports.uploadTimeoutMs = uploadTimeoutMs;
12
+ exports.isTransientUploadError = isTransientUploadError;
13
+ exports.uploadFile = uploadFile;
14
+ exports.getHermesBase = getHermesBase;
15
+ exports.getChannels = getChannels;
16
+ exports.createChannel = createChannel;
17
+ /**
18
+ * [INPUT]: 依赖运行时 HTTP、独立服务 multipart 协议与 CLI 会话凭据
19
+ * [OUTPUT]: 对外提供认证请求、当前应用会员权益读取、按字节显示进度的对象上传、应用渠道(含创建)、原生包、不可变热更新包/投放策略与 Hermes base 查询 API
20
+ * [POS]: CLI 传输边界,统一旧服务兼容路径与独立 Go 服务管理路径,确保 standalone 包体和 Deployment 请求不落回旧 /app 路由
21
+ */
22
+ const filesize_parser_1 = __importDefault(require("filesize-parser"));
23
+ const fs_1 = __importDefault(require("fs"));
24
+ const path_1 = __importDefault(require("path"));
25
+ const stream_1 = require("stream");
26
+ const package_json_1 = __importDefault(require("../package.json"));
27
+ const constants_1 = require("./utils/constants");
28
+ const http_helper_1 = require("./utils/http-helper");
29
+ const i18n_1 = require("./utils/i18n");
30
+ const runtime_1 = require("./utils/runtime");
31
+ const http_helper_2 = require("./utils/http-helper");
32
+ let session;
33
+ let savedSession;
34
+ let apiToken;
35
+ function credentialPath() {
36
+ return (0, constants_1.credentialFileForService)(process.env.RNU_SERVICE_URL || process.env.RNU_API_MODE === 'standalone'
37
+ ? process.env.RNU_SERVICE_URL
38
+ : undefined);
39
+ }
40
+ /** Unwrap the standalone service's `{ data: ... }` envelope. */
41
+ function unwrapData(response) {
42
+ if (response && typeof response === 'object' && 'data' in response) {
43
+ return response.data;
44
+ }
45
+ return response;
46
+ }
47
+ /** Prefix management routes only when talking to the independent Go service. */
48
+ function servicePath(route) {
49
+ if (!route.startsWith('/')) {
50
+ route = `/${route}`;
51
+ }
52
+ if (!(0, http_helper_2.isStandaloneService)())
53
+ return route;
54
+ const configured = process.env.RNU_SERVICE_URL?.trim().replace(/\/+$/, '');
55
+ return configured?.endsWith('/admin/api/v1') ? route : `/admin/api/v1${route}`;
56
+ }
57
+ const userAgent = `rn-update-cli/${package_json_1.default.version}`;
58
+ const getSession = () => session;
59
+ exports.getSession = getSession;
60
+ const getApiToken = () => apiToken;
61
+ exports.getApiToken = getApiToken;
62
+ const setApiToken = (token) => {
63
+ apiToken = token;
64
+ };
65
+ exports.setApiToken = setApiToken;
66
+ const loadApiTokenFromEnv = () => {
67
+ const envToken = process.env.PAKTA_API_TOKEN;
68
+ if (envToken) {
69
+ apiToken = envToken;
70
+ }
71
+ };
72
+ const replaceSession = (newSession) => {
73
+ session = newSession;
74
+ };
75
+ exports.replaceSession = replaceSession;
76
+ const loadSession = async () => {
77
+ loadApiTokenFromEnv();
78
+ const file = credentialPath();
79
+ if (fs_1.default.existsSync(file)) {
80
+ try {
81
+ (0, exports.replaceSession)(JSON.parse(fs_1.default.readFileSync(file, 'utf8')));
82
+ savedSession = session;
83
+ }
84
+ catch (e) {
85
+ console.error(`Failed to parse file ${file}. Try to remove it manually.`);
86
+ throw e;
87
+ }
88
+ }
89
+ };
90
+ exports.loadSession = loadSession;
91
+ const saveSession = () => {
92
+ // Only save on change.
93
+ if (session !== savedSession) {
94
+ const current = session;
95
+ const data = JSON.stringify(current, null, 4);
96
+ const file = credentialPath();
97
+ fs_1.default.writeFileSync(file, data, { encoding: 'utf8', mode: 0o600 });
98
+ try {
99
+ // mode above only applies on creation; tighten pre-existing files too
100
+ fs_1.default.chmodSync(file, 0o600);
101
+ }
102
+ catch {
103
+ // best-effort (e.g. exotic filesystems); the token is still saved
104
+ }
105
+ savedSession = current;
106
+ }
107
+ };
108
+ exports.saveSession = saveSession;
109
+ const closeSession = () => {
110
+ const file = credentialPath();
111
+ if (fs_1.default.existsSync(file)) {
112
+ fs_1.default.unlinkSync(file);
113
+ savedSession = undefined;
114
+ }
115
+ session = undefined;
116
+ };
117
+ exports.closeSession = closeSession;
118
+ /** Remove credentials, query signatures and fragments before logging a URL. */
119
+ function redactRequestUrl(requestUrl) {
120
+ try {
121
+ const parsed = new URL(requestUrl);
122
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname}${parsed.search ? '?<redacted>' : ''}`;
123
+ }
124
+ catch {
125
+ const secretStart = requestUrl.search(/[?#]/);
126
+ return secretStart < 0
127
+ ? requestUrl
128
+ : `${requestUrl.slice(0, secretStart)}?<redacted>`;
129
+ }
130
+ }
131
+ function createRequestError(error, requestUrl, status) {
132
+ const message = typeof error === 'string'
133
+ ? error
134
+ : error instanceof Error
135
+ ? error.message
136
+ : String(error);
137
+ const cause = error instanceof Error || (typeof error === 'object' && error !== null)
138
+ ? error
139
+ : undefined;
140
+ const requestError = new Error(`${message}\nURL: ${redactRequestUrl(requestUrl)}`, cause === undefined ? undefined : { cause });
141
+ requestError.status = status;
142
+ return requestError;
143
+ }
144
+ const PROXY_ERROR_PATTERNS = [
145
+ 'socket disconnected before secure TLS connection',
146
+ 'ECONNRESET',
147
+ 'ECONNREFUSED',
148
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
149
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
150
+ 'CERT_HAS_EXPIRED',
151
+ 'self signed certificate',
152
+ 'proxy',
153
+ 'ETIMEDOUT',
154
+ 'EHOSTUNREACH',
155
+ 'ENETUNREACH',
156
+ ];
157
+ function isProxyRelatedError(error) {
158
+ const seen = new Set();
159
+ const parts = [];
160
+ let current = error;
161
+ while (current !== undefined && current !== null && !seen.has(current)) {
162
+ seen.add(current);
163
+ if (current instanceof Error) {
164
+ const code = current.code;
165
+ parts.push(`${current.name} ${code ?? ''} ${current.message}`);
166
+ }
167
+ else {
168
+ parts.push(String(current));
169
+ }
170
+ current =
171
+ typeof current === 'object'
172
+ ? current.cause
173
+ : undefined;
174
+ }
175
+ const lower = parts.join('\n').toLowerCase();
176
+ return PROXY_ERROR_PATTERNS.some((p) => lower.includes(p.toLowerCase()));
177
+ }
178
+ async function query(url, options) {
179
+ const baseUrl = await (0, http_helper_1.getBaseUrl)();
180
+ const fullUrl = `${baseUrl}${url}`;
181
+ let resp;
182
+ try {
183
+ resp = await (0, runtime_1.runtimeFetch)(fullUrl, options);
184
+ }
185
+ catch (error) {
186
+ const baseError = createRequestError(error, fullUrl);
187
+ if (isProxyRelatedError(error)) {
188
+ throw new Error(`${baseError.message}\n\n${(0, i18n_1.t)('proxyNetworkError')}\n${(0, i18n_1.t)('proxyNetworkErrorTips')}`, { cause: baseError });
189
+ }
190
+ throw baseError;
191
+ }
192
+ const text = await resp.text();
193
+ let json;
194
+ try {
195
+ json = JSON.parse(text);
196
+ }
197
+ catch (_e) {
198
+ if (resp.status >= 200 && resp.status < 300) {
199
+ // a proxy/gateway likely replaced the response; surface it instead of
200
+ // returning undefined and crashing callers on destructuring
201
+ throw createRequestError(`API returned 200 with non-JSON body (${text.length} bytes)`, fullUrl, resp.status);
202
+ }
203
+ }
204
+ if (resp.status < 200 || resp.status >= 300) {
205
+ const message = json?.message || json?.error?.message || resp.statusText || `HTTP ${resp.status}`;
206
+ if (resp.status === 401) {
207
+ // 迁移后旧 JWT/PAT 会统一返回 401;浏览器之外的 CLI 也要立即清理旧会话文件。
208
+ if (session?.token) {
209
+ (0, exports.closeSession)();
210
+ }
211
+ throw createRequestError((0, i18n_1.t)('loginExpired'), fullUrl, resp.status);
212
+ }
213
+ throw createRequestError(message, fullUrl, resp.status);
214
+ }
215
+ return resp.status === 204 ? undefined : json;
216
+ }
217
+ function queryWithoutBody(method) {
218
+ return (api) => {
219
+ const headers = {
220
+ 'User-Agent': userAgent,
221
+ };
222
+ if (apiToken) {
223
+ headers['x-api-token'] = apiToken;
224
+ }
225
+ else if (session?.token) {
226
+ headers['X-AccessToken'] = session.token;
227
+ }
228
+ return query(api, {
229
+ method,
230
+ headers,
231
+ });
232
+ };
233
+ }
234
+ function queryWithBody(method) {
235
+ return (api, body) => {
236
+ const headers = {
237
+ 'User-Agent': userAgent,
238
+ 'Content-Type': 'application/json',
239
+ };
240
+ if (apiToken) {
241
+ headers['x-api-token'] = apiToken;
242
+ }
243
+ else if (session?.token) {
244
+ headers['X-AccessToken'] = session.token;
245
+ }
246
+ return query(api, {
247
+ method,
248
+ headers,
249
+ body: JSON.stringify(body),
250
+ });
251
+ };
252
+ }
253
+ exports.get = queryWithoutBody('GET');
254
+ exports.post = queryWithBody('POST');
255
+ exports.put = queryWithBody('PUT');
256
+ exports.doDelete = queryWithBody('DELETE');
257
+ async function getMembershipEntitlement(appId) {
258
+ return unwrapData(await (0, exports.get)(servicePath(`/membership/entitlement/${encodeURIComponent(appId)}`)));
259
+ }
260
+ // Upload deadline: generous for a slow link (1 s/MB on top of a 30 s base,
261
+ // never below 60 s) but bounded, so a stalled connection cannot hang a CI job
262
+ // forever. The bar keeps ticking while bytes flow; the deadline is absolute.
263
+ const UPLOAD_TIMEOUT_BASE_MS = 30_000;
264
+ const UPLOAD_TIMEOUT_PER_MB_MS = 1_000;
265
+ const UPLOAD_TIMEOUT_MIN_MS = 60_000;
266
+ const UPLOAD_MAX_RETRIES = 1;
267
+ function uploadTimeoutMs(fileSize) {
268
+ const megabytes = Math.ceil(Math.max(0, fileSize) / 1048576);
269
+ return Math.max(UPLOAD_TIMEOUT_MIN_MS, UPLOAD_TIMEOUT_BASE_MS + megabytes * UPLOAD_TIMEOUT_PER_MB_MS);
270
+ }
271
+ const TRANSIENT_UPLOAD_ERROR_CODES = ['ECONNRESET', 'ETIMEDOUT', 'EPIPE'];
272
+ /**
273
+ * A network-level failure worth one more attempt: a reset/timed-out/broken
274
+ * pipe connection, or our own deadline abort. HTTP responses (4xx/5xx) never
275
+ * get here — they are surfaced as-is.
276
+ */
277
+ function isTransientUploadError(error) {
278
+ if (!error || typeof error !== 'object') {
279
+ return false;
280
+ }
281
+ const { name, code, type, message } = error;
282
+ if (name === 'AbortError' || type === 'aborted' || code === 'ABORT_ERR') {
283
+ return true;
284
+ }
285
+ const text = `${code ?? ''} ${message ?? ''}`;
286
+ return TRANSIENT_UPLOAD_ERROR_CODES.some((c) => text.includes(c));
287
+ }
288
+ class UploadTimeoutError extends Error {
289
+ code = 'ETIMEDOUT';
290
+ constructor(timeoutMs) {
291
+ super(`Upload timed out after ${Math.round(timeoutMs / 1000)}s`);
292
+ this.name = 'UploadTimeoutError';
293
+ }
294
+ }
295
+ /**
296
+ * node-fetch is only needed for the streaming multipart / PUT upload below
297
+ * (the built-in fetch cannot stream a form-data body). Loading it eagerly
298
+ * pulled whatwg-url → punycode into every command, which Node ≥ 21 greets
299
+ * with a DEP0040 deprecation warning on stderr; so it is loaded on first use.
300
+ */
301
+ function loadNodeFetch() {
302
+ const mod = require('node-fetch');
303
+ return (mod.default ?? mod);
304
+ }
305
+ /**
306
+ * Send the file with a size-scaled deadline and one retry on a transient
307
+ * network error. `buildRequest` is invoked per attempt with a fresh file
308
+ * stream (a consumed stream/form cannot be replayed).
309
+ */
310
+ async function sendUpload(fn, realUrl, fileSize, bar, buildRequest) {
311
+ const timeoutMs = uploadTimeoutMs(fileSize);
312
+ const nodeFetch = loadNodeFetch();
313
+ // HTTP(S)_PROXY / NO_PROXY, like every other request of the CLI
314
+ const agent = (0, runtime_1.proxyAgentFor)(realUrl);
315
+ for (let attempt = 0;; attempt++) {
316
+ const controller = new AbortController();
317
+ let timedOut = false;
318
+ const timer = setTimeout(() => {
319
+ timedOut = true;
320
+ controller.abort();
321
+ }, timeoutMs);
322
+ const fileStream = fs_1.default.createReadStream(fn);
323
+ trackUploadProgress(fileStream, bar);
324
+ try {
325
+ return await nodeFetch(realUrl, {
326
+ ...buildRequest(fileStream),
327
+ agent,
328
+ signal: controller.signal,
329
+ });
330
+ }
331
+ catch (rawError) {
332
+ fileStream.destroy();
333
+ const error = timedOut ? new UploadTimeoutError(timeoutMs) : rawError;
334
+ if (attempt < UPLOAD_MAX_RETRIES && isTransientUploadError(error)) {
335
+ const reason = error instanceof Error ? error.message : String(error);
336
+ console.warn(`\nUpload interrupted (${reason}), retrying...`);
337
+ // restart the bar from zero for the second pass
338
+ bar.curr = 0;
339
+ continue;
340
+ }
341
+ throw error;
342
+ }
343
+ finally {
344
+ clearTimeout(timer);
345
+ }
346
+ }
347
+ }
348
+ /**
349
+ * 创建与旧 CLI 一致的上传进度条,并把文件流读取量映射为已上传字节。
350
+ */
351
+ function createUploadProgressBar(fileSize) {
352
+ const progressModule = require('progress');
353
+ const ProgressBarImpl = progressModule.default ??
354
+ progressModule;
355
+ return new ProgressBarImpl(' Uploading [:bar] :percent :etas', {
356
+ complete: '=',
357
+ incomplete: ' ',
358
+ total: Math.max(fileSize, 1),
359
+ });
360
+ }
361
+ /**
362
+ * 监听上传流的数据事件,保持旧服务与 standalone multipart 的进度语义一致。
363
+ */
364
+ function trackUploadProgress(fileStream, bar) {
365
+ fileStream.on('data', (data) => {
366
+ bar.tick(Buffer.byteLength(data));
367
+ });
368
+ }
369
+ async function uploadFile(fn, key, appId) {
370
+ if ((0, http_helper_2.isStandaloneService)()) {
371
+ return uploadMultipartFile(fn, appId);
372
+ }
373
+ // appId 用于服务端路由:绑定了自托管节点(rnu-node)的应用,
374
+ // 上传指令会指向节点或其对象存储
375
+ const resp = await (0, exports.post)('/upload', {
376
+ ext: path_1.default.extname(fn),
377
+ ...(appId ? { appId: Number(appId) } : {}),
378
+ });
379
+ const { url, backupUrl, formData, maxSize } = resp;
380
+ let realUrl = url;
381
+ if (backupUrl) {
382
+ if (global.USE_ACC_OSS) {
383
+ realUrl = backupUrl;
384
+ }
385
+ else if (!(0, runtime_1.resolveProxy)(url)) {
386
+ // a direct TCP probe says nothing about the path through a proxy (and
387
+ // usually cannot connect at all): the primary url stays in that case
388
+ const latency = await (0, runtime_1.measureTcpLatency)(url, {
389
+ attempts: 4,
390
+ timeout: 1000,
391
+ });
392
+ if (!Number.isFinite(latency) || latency > 150) {
393
+ realUrl = backupUrl;
394
+ }
395
+ }
396
+ // console.log({realUrl});
397
+ }
398
+ const fileSize = fs_1.default.statSync(fn).size;
399
+ if (maxSize && fileSize > (0, filesize_parser_1.default)(maxSize)) {
400
+ const readableFileSize = `${(fileSize / 1048576).toFixed(1)}m`;
401
+ throw new Error((0, i18n_1.t)('fileSizeExceeded', {
402
+ fileSize: readableFileSize,
403
+ maxSize,
404
+ pricingPageUrl: constants_1.pricingPageUrl,
405
+ }));
406
+ }
407
+ // progress/form-data are only needed here; keep them off the startup path
408
+ const bar = createUploadProgressBar(fileSize);
409
+ const rethrowUploadError = (error) => {
410
+ const baseError = createRequestError(error, realUrl);
411
+ if (isProxyRelatedError(error)) {
412
+ throw new Error(`${baseError.message}\n\n${(0, i18n_1.t)('proxyNetworkError')}\n${(0, i18n_1.t)('proxyNetworkErrorTips')}`, { cause: baseError });
413
+ }
414
+ throw baseError;
415
+ };
416
+ // 自托管节点的 s3 直传:服务端下发预签名 PUT,字节直达用户的对象存储
417
+ if (resp.method === 'PUT') {
418
+ let putRes;
419
+ try {
420
+ putRes = await sendUpload(fn, realUrl, fileSize, bar, (fileStream) => ({
421
+ method: 'PUT',
422
+ body: fileStream,
423
+ // 预签名 PUT 不接受 chunked 传输,必须显式声明长度
424
+ headers: {
425
+ 'content-length': String(fileSize),
426
+ ...(resp.headers || {}),
427
+ },
428
+ }));
429
+ }
430
+ catch (error) {
431
+ return rethrowUploadError(error);
432
+ }
433
+ if (putRes.status > 299) {
434
+ throw createRequestError(`${putRes.status}: ${putRes.statusText || 'Upload failed'}`, realUrl);
435
+ }
436
+ return { hash: resp.key };
437
+ }
438
+ const FormData = require('form-data');
439
+ let res;
440
+ try {
441
+ res = await sendUpload(fn, realUrl, fileSize, bar, (fileStream) => {
442
+ const form = new FormData();
443
+ for (const [k, v] of Object.entries(formData)) {
444
+ form.append(k, v);
445
+ }
446
+ if (key) {
447
+ form.append('key', key);
448
+ }
449
+ // With every part's length known node-fetch sends Content-Length instead
450
+ // of a chunked body: what object stores expect, and the only framing
451
+ // that survives http-proxy-agent's rewrite of the buffered request head.
452
+ form.append('file', fileStream, { knownLength: fileSize });
453
+ return { method: 'POST', body: form };
454
+ });
455
+ }
456
+ catch (error) {
457
+ return rethrowUploadError(error);
458
+ }
459
+ if (res.status > 299) {
460
+ throw createRequestError(`${res.status}: ${res.statusText || 'Upload failed'}`, realUrl);
461
+ }
462
+ // const body = await response.json();
463
+ return { hash: key || formData.key };
464
+ }
465
+ /**
466
+ * 通过独立服务的四阶段 multipart API 上传文件。预签名 URL 直接接收文件流,
467
+ * 进度条按每个分片实际读取的字节数累加;管理 API 只负责元数据与最终 ETag。
468
+ */
469
+ async function uploadMultipartFile(fn, appId) {
470
+ if (!appId) {
471
+ throw new Error('独立服务上传必须提供 appId。');
472
+ }
473
+ const fileName = path_1.default.basename(fn);
474
+ const contentType = fileName.endsWith('.ppk')
475
+ ? 'application/octet-stream'
476
+ : fileName.endsWith('.map')
477
+ ? 'application/json'
478
+ : 'application/octet-stream';
479
+ const kind = /\.(apk|ipa|app|aab|hap)$/i.test(fileName) ? 'native' : 'hot';
480
+ const fileSize = fs_1.default.statSync(fn).size;
481
+ const created = unwrapData(await (0, exports.post)(servicePath('/uploads/multipart'), {
482
+ appId: String(appId),
483
+ fileName,
484
+ contentType,
485
+ kind,
486
+ declaredSize: fileSize,
487
+ }));
488
+ const partSize = 8 * 1024 * 1024;
489
+ const partCount = Math.max(1, Math.ceil(fileSize / partSize));
490
+ const partNumbers = Array.from({ length: partCount }, (_, index) => index + 1);
491
+ let presigned;
492
+ try {
493
+ presigned = unwrapData(await (0, exports.post)(servicePath('/uploads/multipart/parts'), {
494
+ key: created.key,
495
+ uploadId: created.uploadId,
496
+ partNumbers,
497
+ }));
498
+ }
499
+ catch (error) {
500
+ // 会话创建成功后,预签名请求也可能在网络层失败;此时立即回收会话。
501
+ await (0, exports.post)(servicePath('/uploads/multipart/abort'), {
502
+ key: created.key,
503
+ uploadId: created.uploadId,
504
+ }).catch(() => undefined);
505
+ throw error;
506
+ }
507
+ const nodeFetch = loadNodeFetch();
508
+ const bar = createUploadProgressBar(fileSize);
509
+ const completed = [];
510
+ try {
511
+ for (const part of presigned.parts) {
512
+ const start = (part.partNumber - 1) * partSize;
513
+ const length = Math.min(partSize, Math.max(0, fileSize - start));
514
+ // 预签名 PUT 需要已知 Content-Length;流式读取分片既满足该约束,
515
+ // 又让进度条随着真实上传数据持续推进,而不是在请求前一次性读入 Buffer。
516
+ const partStream = length === 0
517
+ ? stream_1.Readable.from([])
518
+ : fs_1.default.createReadStream(fn, {
519
+ start,
520
+ end: start + length - 1,
521
+ });
522
+ trackUploadProgress(partStream, bar);
523
+ let response;
524
+ try {
525
+ response = await nodeFetch(part.url, {
526
+ method: 'PUT',
527
+ body: partStream,
528
+ headers: { 'content-length': String(length) },
529
+ });
530
+ }
531
+ catch (error) {
532
+ partStream.destroy();
533
+ throw error;
534
+ }
535
+ if (!response.ok) {
536
+ throw new Error(`对象存储上传失败:HTTP ${response.status}`);
537
+ }
538
+ const eTag = response.headers.get('etag');
539
+ if (!eTag) {
540
+ throw new Error(`对象存储未返回第 ${part.partNumber} 个分片的 ETag。`);
541
+ }
542
+ completed.push({ partNumber: part.partNumber, eTag, size: length });
543
+ }
544
+ }
545
+ catch (error) {
546
+ await (0, exports.post)(servicePath('/uploads/multipart/abort'), {
547
+ key: created.key,
548
+ uploadId: created.uploadId,
549
+ }).catch(() => undefined);
550
+ throw error;
551
+ }
552
+ let result;
553
+ try {
554
+ result = unwrapData(await (0, exports.post)(servicePath('/uploads/multipart/complete'), {
555
+ key: created.key,
556
+ uploadId: created.uploadId,
557
+ parts: completed,
558
+ }));
559
+ }
560
+ catch (error) {
561
+ // 完成请求本身也可能在网络层失败;清理会话和对象,避免留下悬挂 multipart。
562
+ await (0, exports.post)(servicePath('/uploads/multipart/abort'), {
563
+ key: created.key,
564
+ uploadId: created.uploadId,
565
+ }).catch(() => undefined);
566
+ throw error;
567
+ }
568
+ return { hash: result.key, url: result.url, key: result.key };
569
+ }
570
+ /**
571
+ * Ask the active service for a Hermes base. The legacy service may resolve an
572
+ * HBC epoch directly; the standalone service returns its newest native package
573
+ * and leaves the downloaded bundle's HBC verification to hermes-base.ts.
574
+ */
575
+ async function getHermesBase(appId, bytecodeVersion) {
576
+ try {
577
+ const data = await (0, exports.get)((0, http_helper_2.isStandaloneService)()
578
+ ? servicePath(`/apps/${encodeURIComponent(appId)}/hermes-base?bytecodeVersion=${bytecodeVersion}`)
579
+ : `/app/${appId}/hermesBase?bytecodeVersion=${bytecodeVersion}`);
580
+ const record = data && typeof data === 'object' && 'data' in data ? data.data : data;
581
+ if (!record || typeof record !== 'object' || !record.url || !record.hash) {
582
+ return null;
583
+ }
584
+ return record;
585
+ }
586
+ catch (error) {
587
+ if (error?.status === 404) {
588
+ return null;
589
+ }
590
+ throw error;
591
+ }
592
+ }
593
+ const getAllPackages = async (appId) => {
594
+ if ((0, http_helper_2.isStandaloneService)()) {
595
+ const packages = [];
596
+ let pageNumber = 1;
597
+ let total = Number.POSITIVE_INFINITY;
598
+ while (packages.length < total) {
599
+ const response = await (0, exports.get)(servicePath(`/apps/${encodeURIComponent(appId)}/versions?page=${pageNumber}&pageSize=100`));
600
+ const page = unwrapData(response);
601
+ const items = page?.items ?? [];
602
+ total = typeof page?.total === 'number' ? page.total : packages.length + items.length;
603
+ packages.push(...items.map((item) => ({
604
+ ...item,
605
+ name: item.name ?? item.packageVersion ?? '',
606
+ versionName: item.versionName ?? item.packageVersion,
607
+ hash: item.hash ?? item.bundleHash,
608
+ })));
609
+ if (items.length === 0)
610
+ break;
611
+ pageNumber += 1;
612
+ }
613
+ return packages;
614
+ }
615
+ // the server caps limit at 100, so page through with offset
616
+ const limit = 100;
617
+ let offset = 0;
618
+ let allPackages;
619
+ while (true) {
620
+ const { data, count } = await (0, exports.get)(`/app/${appId}/package/list?offset=${offset}&limit=${limit}`);
621
+ const packages = data;
622
+ if (allPackages === undefined || allPackages === null) {
623
+ allPackages = packages;
624
+ }
625
+ else if (packages) {
626
+ allPackages.push(...packages);
627
+ }
628
+ if (!packages || packages.length === 0) {
629
+ break;
630
+ }
631
+ offset += packages.length;
632
+ if (offset >= Number(count || 0)) {
633
+ break;
634
+ }
635
+ }
636
+ return allPackages;
637
+ };
638
+ exports.getAllPackages = getAllPackages;
639
+ /** Resolve the registered channel code to the UUID required by the Go API. */
640
+ async function getChannels(appId) {
641
+ if (!(0, http_helper_2.isStandaloneService)()) {
642
+ return [];
643
+ }
644
+ const pageSize = 100;
645
+ const channels = [];
646
+ for (let pageNumber = 1;; pageNumber += 1) {
647
+ const response = await (0, exports.get)(servicePath(`/apps/${encodeURIComponent(appId)}/channels?page=${pageNumber}&pageSize=${pageSize}`));
648
+ const page = unwrapData(response);
649
+ const items = page?.items ?? [];
650
+ channels.push(...items);
651
+ if (items.length === 0 ||
652
+ (page?.total !== undefined && channels.length >= page.total) ||
653
+ items.length < pageSize) {
654
+ break;
655
+ }
656
+ }
657
+ return channels;
658
+ }
659
+ /** Create one standalone-service channel with an explicit code/name identity. */
660
+ async function createChannel(appId, code, name = code) {
661
+ if (!(0, http_helper_2.isStandaloneService)()) {
662
+ throw new Error('渠道管理需要独立服务(请设置 RNU_SERVICE_URL)。');
663
+ }
664
+ const response = await (0, exports.post)(servicePath(`/apps/${encodeURIComponent(appId)}/channels`), {
665
+ code,
666
+ name,
667
+ nativePackageUrl: '',
668
+ paused: false,
669
+ });
670
+ return unwrapData(response);
671
+ }