@gpc-cli/api 1.0.19 → 1.0.21

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/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // src/errors.ts
2
- var ApiError = class extends Error {
2
+ var PlayApiError = class extends Error {
3
3
  constructor(message, code, statusCode, suggestion) {
4
4
  super(message);
5
5
  this.code = code;
6
6
  this.statusCode = statusCode;
7
7
  this.suggestion = suggestion;
8
- this.name = "ApiError";
8
+ this.name = "PlayApiError";
9
9
  }
10
10
  exitCode = 4;
11
11
  toJSON() {
@@ -21,8 +21,269 @@ var ApiError = class extends Error {
21
21
  };
22
22
 
23
23
  // src/http.ts
24
- import { readFile } from "fs/promises";
24
+ import { readFile, stat as stat2 } from "fs/promises";
25
25
  import { resolve, isAbsolute } from "path";
26
+
27
+ // src/resumable-upload.ts
28
+ import { open, stat } from "fs/promises";
29
+ var CHUNK_ALIGNMENT = 256 * 1024;
30
+ var DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024;
31
+ var RESUMABLE_THRESHOLD = 5 * 1024 * 1024;
32
+ function envInt(name) {
33
+ const val = process.env[name];
34
+ if (val === void 0) return void 0;
35
+ const n = Number(val);
36
+ return Number.isFinite(n) ? n : void 0;
37
+ }
38
+ function resolveChunkSize(explicit) {
39
+ const size = explicit ?? envInt("GPC_UPLOAD_CHUNK_SIZE") ?? DEFAULT_CHUNK_SIZE;
40
+ if (size < CHUNK_ALIGNMENT || size % CHUNK_ALIGNMENT !== 0) {
41
+ throw new PlayApiError(
42
+ `Chunk size must be a multiple of 256 KB (got ${size} bytes)`,
43
+ "UPLOAD_INVALID_CHUNK_SIZE",
44
+ void 0,
45
+ `Use a multiple of 262144 (256 KB). Common values: 1048576 (1 MB), 8388608 (8 MB), 16777216 (16 MB).`
46
+ );
47
+ }
48
+ return size;
49
+ }
50
+ function jitteredDelay(base, attempt, max) {
51
+ const exponential = base * 2 ** attempt;
52
+ const capped = Math.min(exponential, max);
53
+ return capped * (0.5 + Math.random() * 0.5);
54
+ }
55
+ async function resumableUpload(uploadUrl, filePath, contentType, ctx, options) {
56
+ const chunkSize = resolveChunkSize(options?.chunkSize);
57
+ const maxResumeAttempts = options?.maxResumeAttempts ?? 5;
58
+ const onProgress = options?.onProgress;
59
+ const fileStats = await stat(filePath);
60
+ const totalBytes = fileStats.size;
61
+ let sessionUri = options?.resumeSessionUri;
62
+ if (!sessionUri) {
63
+ sessionUri = await initiateSession(uploadUrl, contentType, totalBytes, ctx);
64
+ }
65
+ const startTime = Date.now();
66
+ let offset = 0;
67
+ if (options?.resumeSessionUri) {
68
+ offset = await queryProgress(sessionUri, totalBytes, ctx);
69
+ }
70
+ let fh;
71
+ try {
72
+ fh = await open(filePath, "r");
73
+ const chunkBuffer = Buffer.alloc(chunkSize);
74
+ while (offset < totalBytes) {
75
+ const remaining = totalBytes - offset;
76
+ const bytesToRead = Math.min(chunkSize, remaining);
77
+ const { bytesRead } = await fh.read(chunkBuffer, 0, bytesToRead, offset);
78
+ if (bytesRead === 0) break;
79
+ const chunk = Buffer.from(chunkBuffer.buffer, chunkBuffer.byteOffset, bytesRead);
80
+ const rangeEnd = offset + bytesRead - 1;
81
+ const contentRange = `bytes ${offset}-${rangeEnd}/${totalBytes}`;
82
+ let result;
83
+ for (let attempt = 0; attempt <= maxResumeAttempts; attempt++) {
84
+ if (attempt > 0) {
85
+ const delay = jitteredDelay(1e3, attempt - 1, 3e4);
86
+ await new Promise((r) => setTimeout(r, delay));
87
+ try {
88
+ const serverOffset = await queryProgress(sessionUri, totalBytes, ctx);
89
+ if (serverOffset >= offset + bytesRead) {
90
+ result = { complete: false };
91
+ break;
92
+ }
93
+ if (serverOffset > offset) {
94
+ }
95
+ } catch {
96
+ }
97
+ ctx.onRetry?.({
98
+ attempt,
99
+ method: "PUT",
100
+ path: sessionUri,
101
+ error: `Chunk upload failed at offset ${offset}, retrying`,
102
+ delayMs: Math.round(delay),
103
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
104
+ });
105
+ }
106
+ result = await sendChunk(sessionUri, chunk, contentRange, ctx);
107
+ if (result) break;
108
+ }
109
+ if (!result) {
110
+ throw new PlayApiError(
111
+ `Upload failed: chunk at offset ${offset} could not be sent after ${maxResumeAttempts + 1} attempts`,
112
+ "UPLOAD_CHUNK_FAILED",
113
+ void 0,
114
+ `The upload session is still valid for up to 1 week. Resume with: --resume-uri "${sessionUri}"`
115
+ );
116
+ }
117
+ offset += bytesRead;
118
+ if (onProgress) {
119
+ const elapsed = (Date.now() - startTime) / 1e3;
120
+ const bytesPerSecond = elapsed > 0 ? offset / elapsed : 0;
121
+ const remainingBytes = totalBytes - offset;
122
+ const etaSeconds = bytesPerSecond > 0 ? remainingBytes / bytesPerSecond : 0;
123
+ onProgress({
124
+ bytesUploaded: offset,
125
+ totalBytes,
126
+ percent: Math.round(offset / totalBytes * 100),
127
+ bytesPerSecond: Math.round(bytesPerSecond),
128
+ etaSeconds: Math.round(etaSeconds)
129
+ });
130
+ }
131
+ if (result.complete && result.response) {
132
+ return result.response;
133
+ }
134
+ }
135
+ throw new PlayApiError(
136
+ "Upload finished sending all bytes but did not receive a completion response",
137
+ "UPLOAD_NO_COMPLETION",
138
+ void 0,
139
+ "This is unexpected. Try uploading again."
140
+ );
141
+ } finally {
142
+ await fh?.close();
143
+ }
144
+ }
145
+ async function initiateSession(uploadUrl, contentType, totalBytes, ctx) {
146
+ const token = await ctx.getAccessToken();
147
+ const url = uploadUrl.includes("?") ? `${uploadUrl}&uploadType=resumable` : `${uploadUrl}?uploadType=resumable`;
148
+ const controller = new AbortController();
149
+ const timer = setTimeout(() => controller.abort(), 6e4);
150
+ let response;
151
+ try {
152
+ response = await fetch(url, {
153
+ method: "POST",
154
+ headers: {
155
+ Authorization: `Bearer ${token}`,
156
+ "X-Upload-Content-Type": contentType,
157
+ "X-Upload-Content-Length": String(totalBytes),
158
+ "Content-Length": "0"
159
+ },
160
+ signal: controller.signal
161
+ });
162
+ } finally {
163
+ clearTimeout(timer);
164
+ }
165
+ if (!response.ok) {
166
+ const body = await response.text();
167
+ throw new PlayApiError(
168
+ `Failed to initiate resumable upload: ${response.status} ${body.slice(0, 200)}`,
169
+ "UPLOAD_INITIATE_FAILED",
170
+ response.status,
171
+ "Check that the package name, edit ID, and credentials are correct."
172
+ );
173
+ }
174
+ const location = response.headers.get("Location");
175
+ if (!location) {
176
+ throw new PlayApiError(
177
+ "Resumable upload initiation did not return a session URI (Location header missing)",
178
+ "UPLOAD_NO_SESSION_URI",
179
+ response.status,
180
+ "This is a Google API issue. Try again."
181
+ );
182
+ }
183
+ return location;
184
+ }
185
+ async function sendChunk(sessionUri, chunk, contentRange, ctx) {
186
+ const token = await ctx.getAccessToken();
187
+ const chunkTimeoutMs = 3e4 + Math.ceil(chunk.byteLength / (1024 * 1024)) * 1e3;
188
+ const controller = new AbortController();
189
+ const timer = setTimeout(() => controller.abort(), chunkTimeoutMs);
190
+ let response;
191
+ try {
192
+ response = await fetch(sessionUri, {
193
+ method: "PUT",
194
+ headers: {
195
+ Authorization: `Bearer ${token}`,
196
+ "Content-Length": String(chunk.byteLength),
197
+ "Content-Range": contentRange
198
+ },
199
+ body: chunk,
200
+ signal: controller.signal
201
+ });
202
+ } catch {
203
+ return void 0;
204
+ } finally {
205
+ clearTimeout(timer);
206
+ }
207
+ if (response.status === 200 || response.status === 201) {
208
+ const text = await response.text();
209
+ const data = text ? JSON.parse(text) : {};
210
+ return { complete: true, response: { data, status: response.status } };
211
+ }
212
+ if (response.status === 308) {
213
+ await response.body?.cancel();
214
+ return { complete: false };
215
+ }
216
+ if (response.status === 404) {
217
+ throw new PlayApiError(
218
+ "Upload session not found. The session may have expired.",
219
+ "UPLOAD_SESSION_NOT_FOUND",
220
+ 404,
221
+ "Start a new upload. Resumable upload sessions are valid for up to 1 week."
222
+ );
223
+ }
224
+ if (response.status === 410) {
225
+ throw new PlayApiError(
226
+ "Upload session has expired.",
227
+ "UPLOAD_SESSION_EXPIRED",
228
+ 410,
229
+ "Start a new upload from the beginning."
230
+ );
231
+ }
232
+ if (response.status === 401) {
233
+ await response.body?.cancel();
234
+ return void 0;
235
+ }
236
+ if (response.status === 429 || response.status >= 500) {
237
+ await response.body?.cancel();
238
+ return void 0;
239
+ }
240
+ const body = await response.text();
241
+ throw new PlayApiError(
242
+ `Upload chunk failed with status ${response.status}: ${body.slice(0, 200)}`,
243
+ `UPLOAD_HTTP_${response.status}`,
244
+ response.status,
245
+ "The upload encountered an unexpected error."
246
+ );
247
+ }
248
+ async function queryProgress(sessionUri, totalBytes, ctx) {
249
+ const token = await ctx.getAccessToken();
250
+ const response = await fetch(sessionUri, {
251
+ method: "PUT",
252
+ headers: {
253
+ Authorization: `Bearer ${token}`,
254
+ "Content-Length": "0",
255
+ "Content-Range": `bytes */${totalBytes}`
256
+ }
257
+ });
258
+ if (response.status === 308) {
259
+ await response.body?.cancel();
260
+ const range = response.headers.get("Range");
261
+ if (range) {
262
+ const match = range.match(/bytes=0-(\d+)/);
263
+ if (match) {
264
+ return Number(match[1]) + 1;
265
+ }
266
+ }
267
+ return 0;
268
+ }
269
+ if (response.status === 200 || response.status === 201) {
270
+ await response.body?.cancel();
271
+ return totalBytes;
272
+ }
273
+ if (response.status === 404 || response.status === 410) {
274
+ await response.body?.cancel();
275
+ throw new PlayApiError(
276
+ "Upload session has expired while querying progress.",
277
+ "UPLOAD_SESSION_EXPIRED",
278
+ response.status,
279
+ "Start a new upload from the beginning."
280
+ );
281
+ }
282
+ await response.body?.cancel();
283
+ return 0;
284
+ }
285
+
286
+ // src/http.ts
26
287
  function stripHtml(text) {
27
288
  return text.replace(/<[^>]*>/g, " ").replace(/&[a-z]+;/gi, " ").replace(/\s+/g, " ").trim();
28
289
  }
@@ -40,7 +301,7 @@ function sanitizeErrorBody(body) {
40
301
  function validateFilePath(filePath) {
41
302
  const resolved = resolve(filePath);
42
303
  if (!isAbsolute(resolved)) {
43
- throw new ApiError(
304
+ throw new PlayApiError(
44
305
  "Invalid file path",
45
306
  "API_INVALID_PATH",
46
307
  void 0,
@@ -48,21 +309,26 @@ function validateFilePath(filePath) {
48
309
  );
49
310
  }
50
311
  if (filePath.includes("\0")) {
51
- throw new ApiError("Invalid file path: null bytes not allowed", "API_INVALID_PATH", void 0, "Provide a valid file path without null bytes.");
312
+ throw new PlayApiError(
313
+ "Invalid file path: null bytes not allowed",
314
+ "API_INVALID_PATH",
315
+ void 0,
316
+ "Provide a valid file path without null bytes."
317
+ );
52
318
  }
53
319
  return resolved;
54
320
  }
55
321
  var BASE_URL = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications";
56
322
  var UPLOAD_BASE_URL = "https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications";
57
323
  var INTERNAL_SHARING_UPLOAD_BASE_URL = "https://androidpublisher.googleapis.com/upload/internalappsharing/v3/applications";
58
- function envInt(name) {
324
+ function envInt2(name) {
59
325
  const val = process.env[name];
60
326
  if (val === void 0) return void 0;
61
327
  const n = Number(val);
62
328
  return Number.isFinite(n) ? n : void 0;
63
329
  }
64
330
  function resolveOption(explicit, envName, fallback) {
65
- return explicit ?? envInt(envName) ?? fallback;
331
+ return explicit ?? envInt2(envName) ?? fallback;
66
332
  }
67
333
  function mapStatusToError(status, body) {
68
334
  switch (status) {
@@ -115,17 +381,17 @@ function mapStatusToError(status, body) {
115
381
  }
116
382
  }
117
383
  function isRetryable(status) {
118
- return status === 429 || status >= 500;
384
+ return status === 408 || status === 429 || status >= 500;
119
385
  }
120
- function jitteredDelay(base, attempt, max) {
386
+ function jitteredDelay2(base, attempt, max) {
121
387
  const exponential = base * 2 ** attempt;
122
388
  const capped = Math.min(exponential, max);
123
389
  return capped * (0.5 + Math.random() * 0.5);
124
390
  }
125
391
  function createHttpClient(options) {
126
- const maxRetries = resolveOption(options.maxRetries, "GPC_MAX_RETRIES", 3);
392
+ const maxRetries = resolveOption(options.maxRetries, "GPC_MAX_RETRIES", 5);
127
393
  const timeout = resolveOption(options.timeout, "GPC_TIMEOUT", 3e4);
128
- const uploadTimeoutExplicit = options.uploadTimeout ?? envInt("GPC_UPLOAD_TIMEOUT");
394
+ const uploadTimeoutExplicit = options.uploadTimeout ?? envInt2("GPC_UPLOAD_TIMEOUT");
129
395
  const baseDelay = resolveOption(options.baseDelay, "GPC_BASE_DELAY", 1e3);
130
396
  const maxDelay = resolveOption(options.maxDelay, "GPC_MAX_DELAY", 6e4);
131
397
  const onRetry = options.onRetry;
@@ -139,7 +405,7 @@ function createHttpClient(options) {
139
405
  let lastError;
140
406
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
141
407
  if (attempt > 0) {
142
- const delay = jitteredDelay(baseDelay, attempt - 1, maxDelay);
408
+ const delay = jitteredDelay2(baseDelay, attempt - 1, maxDelay);
143
409
  await new Promise((r) => setTimeout(r, delay));
144
410
  }
145
411
  const controller = new AbortController();
@@ -168,7 +434,7 @@ function createHttpClient(options) {
168
434
  }
169
435
  const errorBody = await response.text();
170
436
  const { code, suggestion } = mapStatusToError(response.status, errorBody);
171
- const err = new ApiError(
437
+ const err = new PlayApiError(
172
438
  `${method} ${path} failed with status ${response.status}: ${sanitizeErrorBody(errorBody)}`,
173
439
  code,
174
440
  response.status,
@@ -176,7 +442,7 @@ function createHttpClient(options) {
176
442
  );
177
443
  if (isRetryable(response.status) && attempt < maxRetries) {
178
444
  lastError = err;
179
- const delay = jitteredDelay(baseDelay, attempt, maxDelay);
445
+ const delay = jitteredDelay2(baseDelay, attempt, maxDelay);
180
446
  onRetry?.({
181
447
  attempt: attempt + 1,
182
448
  method,
@@ -195,11 +461,11 @@ function createHttpClient(options) {
195
461
  }
196
462
  throw err;
197
463
  } catch (error) {
198
- if (error instanceof ApiError) {
464
+ if (error instanceof PlayApiError) {
199
465
  throw error;
200
466
  }
201
467
  if (error instanceof DOMException && error.name === "AbortError") {
202
- const timeoutErr = new ApiError(
468
+ const timeoutErr = new PlayApiError(
203
469
  `${method} ${path} timed out after ${timeout}ms`,
204
470
  "API_TIMEOUT",
205
471
  void 0,
@@ -212,14 +478,14 @@ function createHttpClient(options) {
212
478
  method,
213
479
  path,
214
480
  error: timeoutErr.message,
215
- delayMs: Math.round(jitteredDelay(baseDelay, attempt, maxDelay)),
481
+ delayMs: Math.round(jitteredDelay2(baseDelay, attempt, maxDelay)),
216
482
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
217
483
  });
218
484
  continue;
219
485
  }
220
486
  throw timeoutErr;
221
487
  }
222
- const networkErr = new ApiError(
488
+ const networkErr = new PlayApiError(
223
489
  `${method} ${path} failed: ${error instanceof Error ? error.message : String(error)}`,
224
490
  "API_NETWORK_ERROR",
225
491
  void 0,
@@ -232,7 +498,7 @@ function createHttpClient(options) {
232
498
  method,
233
499
  path,
234
500
  error: networkErr.message,
235
- delayMs: Math.round(jitteredDelay(baseDelay, attempt, maxDelay)),
501
+ delayMs: Math.round(jitteredDelay2(baseDelay, attempt, maxDelay)),
236
502
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
237
503
  });
238
504
  continue;
@@ -242,7 +508,12 @@ function createHttpClient(options) {
242
508
  clearTimeout(timer);
243
509
  }
244
510
  }
245
- throw lastError ?? new ApiError("Request failed", "API_NETWORK_ERROR", void 0, "Check your network connection and try again. Use --verbose for details.");
511
+ throw lastError ?? new PlayApiError(
512
+ "Request failed",
513
+ "API_NETWORK_ERROR",
514
+ void 0,
515
+ "Check your network connection and try again. Use --verbose for details."
516
+ );
246
517
  }
247
518
  function computeUploadTimeout(fileSizeBytes) {
248
519
  if (uploadTimeoutExplicit !== void 0) return uploadTimeoutExplicit;
@@ -258,7 +529,7 @@ function createHttpClient(options) {
258
529
  let lastError;
259
530
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
260
531
  if (attempt > 0) {
261
- const delay = jitteredDelay(baseDelay, attempt - 1, maxDelay);
532
+ const delay = jitteredDelay2(baseDelay, attempt - 1, maxDelay);
262
533
  await new Promise((r) => setTimeout(r, delay));
263
534
  }
264
535
  const controller = new AbortController();
@@ -284,7 +555,7 @@ function createHttpClient(options) {
284
555
  }
285
556
  const errorBody = await response.text();
286
557
  const { code, suggestion } = mapStatusToError(response.status, errorBody);
287
- const err = new ApiError(
558
+ const err = new PlayApiError(
288
559
  `POST upload ${path} failed with status ${response.status}: ${sanitizeErrorBody(errorBody)}`,
289
560
  code,
290
561
  response.status,
@@ -292,7 +563,7 @@ function createHttpClient(options) {
292
563
  );
293
564
  if (isRetryable(response.status) && attempt < maxRetries) {
294
565
  lastError = err;
295
- const delay = jitteredDelay(baseDelay, attempt, maxDelay);
566
+ const delay = jitteredDelay2(baseDelay, attempt, maxDelay);
296
567
  onRetry?.({
297
568
  attempt: attempt + 1,
298
569
  method: "POST",
@@ -311,12 +582,12 @@ function createHttpClient(options) {
311
582
  }
312
583
  throw err;
313
584
  } catch (error) {
314
- if (error instanceof ApiError) {
585
+ if (error instanceof PlayApiError) {
315
586
  throw error;
316
587
  }
317
588
  if (error instanceof DOMException && error.name === "AbortError") {
318
589
  const sizeMb = Math.round(fileBuffer.byteLength / (1024 * 1024));
319
- const timeoutErr = new ApiError(
590
+ const timeoutErr = new PlayApiError(
320
591
  `POST upload ${path} timed out after ${effectiveTimeout}ms (file: ${sizeMb} MB)`,
321
592
  "API_TIMEOUT",
322
593
  void 0,
@@ -329,14 +600,14 @@ function createHttpClient(options) {
329
600
  method: "POST",
330
601
  path: `upload ${path}`,
331
602
  error: timeoutErr.message,
332
- delayMs: Math.round(jitteredDelay(baseDelay, attempt, maxDelay)),
603
+ delayMs: Math.round(jitteredDelay2(baseDelay, attempt, maxDelay)),
333
604
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
334
605
  });
335
606
  continue;
336
607
  }
337
608
  throw timeoutErr;
338
609
  }
339
- const networkErr = new ApiError(
610
+ const networkErr = new PlayApiError(
340
611
  `POST upload ${path} failed: ${error instanceof Error ? error.message : String(error)}`,
341
612
  "API_NETWORK_ERROR",
342
613
  void 0,
@@ -349,7 +620,7 @@ function createHttpClient(options) {
349
620
  method: "POST",
350
621
  path: `upload ${path}`,
351
622
  error: networkErr.message,
352
- delayMs: Math.round(jitteredDelay(baseDelay, attempt, maxDelay)),
623
+ delayMs: Math.round(jitteredDelay2(baseDelay, attempt, maxDelay)),
353
624
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
354
625
  });
355
626
  continue;
@@ -359,7 +630,12 @@ function createHttpClient(options) {
359
630
  clearTimeout(timer);
360
631
  }
361
632
  }
362
- throw lastError ?? new ApiError("Upload request failed", "API_NETWORK_ERROR", void 0, "Check your network connection and try again. Use --verbose for details.");
633
+ throw lastError ?? new PlayApiError(
634
+ "Upload request failed",
635
+ "API_NETWORK_ERROR",
636
+ void 0,
637
+ "Check your network connection and try again. Use --verbose for details."
638
+ );
363
639
  }
364
640
  return {
365
641
  get(path, params) {
@@ -380,6 +656,25 @@ function createHttpClient(options) {
380
656
  upload(path, filePath, contentType) {
381
657
  return uploadRequest(path, filePath, contentType);
382
658
  },
659
+ async uploadResumable(path, filePath, contentType, uploadOptions) {
660
+ const safeFilePath = validateFilePath(filePath);
661
+ const fileStats = await stat2(safeFilePath);
662
+ const threshold = envInt2("GPC_UPLOAD_RESUMABLE_THRESHOLD") ?? RESUMABLE_THRESHOLD;
663
+ if (fileStats.size < threshold && !uploadOptions?.resumeSessionUri) {
664
+ uploadOptions?.onProgress?.({ bytesUploaded: 0, totalBytes: fileStats.size, percent: 0, bytesPerSecond: 0, etaSeconds: 0 });
665
+ const result = await uploadRequest(path, safeFilePath, contentType);
666
+ uploadOptions?.onProgress?.({ bytesUploaded: fileStats.size, totalBytes: fileStats.size, percent: 100, bytesPerSecond: 0, etaSeconds: 0 });
667
+ return result;
668
+ }
669
+ const uploadUrl = `${UPLOAD_BASE_URL}${path}`;
670
+ return resumableUpload(uploadUrl, safeFilePath, contentType, {
671
+ getAccessToken: () => options.auth.getAccessToken(),
672
+ maxRetries,
673
+ baseDelay,
674
+ maxDelay,
675
+ onRetry
676
+ }, uploadOptions);
677
+ },
383
678
  uploadInternal(path, filePath, contentType) {
384
679
  return uploadRequest(path, filePath, contentType, INTERNAL_SHARING_UPLOAD_BASE_URL);
385
680
  },
@@ -402,7 +697,7 @@ function createHttpClient(options) {
402
697
  if (!response.ok) {
403
698
  const errorBody = await response.text();
404
699
  const { code, suggestion } = mapStatusToError(response.status, errorBody);
405
- throw new ApiError(
700
+ throw new PlayApiError(
406
701
  `GET ${path} failed with status ${response.status}: ${sanitizeErrorBody(errorBody)}`,
407
702
  code,
408
703
  response.status,
@@ -473,14 +768,15 @@ function createApiClient(options) {
473
768
  );
474
769
  return data.bundles;
475
770
  },
476
- async upload(packageName, editId, filePath) {
477
- const { data } = await http.upload(
771
+ async upload(packageName, editId, filePath, uploadOptions) {
772
+ const { data } = await http.uploadResumable(
478
773
  `/${packageName}/edits/${editId}/bundles`,
479
774
  filePath,
480
- "application/octet-stream"
775
+ "application/octet-stream",
776
+ uploadOptions
481
777
  );
482
778
  if (!data || !data.versionCode) {
483
- throw new ApiError(
779
+ throw new PlayApiError(
484
780
  "Upload succeeded but no bundle data returned",
485
781
  "API_EMPTY_RESPONSE",
486
782
  200,
@@ -572,7 +868,7 @@ function createApiClient(options) {
572
868
  filePath.endsWith(".png") ? "image/png" : "image/jpeg"
573
869
  );
574
870
  if (!data.image) {
575
- throw new ApiError(
871
+ throw new PlayApiError(
576
872
  "Upload succeeded but no image data returned",
577
873
  "API_EMPTY_RESPONSE",
578
874
  200,
@@ -603,16 +899,11 @@ function createApiClient(options) {
603
899
  },
604
900
  dataSafety: {
605
901
  async get(packageName) {
606
- const { data } = await http.get(
607
- `/${packageName}/dataSafety`
608
- );
902
+ const { data } = await http.get(`/${packageName}/dataSafety`);
609
903
  return data;
610
904
  },
611
905
  async update(packageName, body) {
612
- const { data } = await http.put(
613
- `/${packageName}/dataSafety`,
614
- body
615
- );
906
+ const { data } = await http.put(`/${packageName}/dataSafety`, body);
616
907
  return data;
617
908
  }
618
909
  },
@@ -665,9 +956,7 @@ function createApiClient(options) {
665
956
  return data;
666
957
  },
667
958
  async get(packageName, productId) {
668
- const { data } = await http.get(
669
- `/${packageName}/subscriptions/${productId}`
670
- );
959
+ const { data } = await http.get(`/${packageName}/subscriptions/${productId}`);
671
960
  return data;
672
961
  },
673
962
  async create(packageName, body, productId) {
@@ -702,9 +991,7 @@ function createApiClient(options) {
702
991
  return data;
703
992
  },
704
993
  async deleteBasePlan(packageName, productId, basePlanId) {
705
- await http.delete(
706
- `/${packageName}/subscriptions/${productId}/basePlans/${basePlanId}`
707
- );
994
+ await http.delete(`/${packageName}/subscriptions/${productId}/basePlans/${basePlanId}`);
708
995
  },
709
996
  async migratePrices(packageName, productId, basePlanId, body) {
710
997
  const { data } = await http.post(
@@ -763,7 +1050,6 @@ function createApiClient(options) {
763
1050
  async list(packageName, options2) {
764
1051
  const params = {};
765
1052
  if (options2?.token) params["token"] = options2.token;
766
- if (options2?.maxResults) params["maxResults"] = String(options2.maxResults);
767
1053
  const hasParams = Object.keys(params).length > 0;
768
1054
  const { data } = await http.get(
769
1055
  `/${packageName}/inappproducts`,
@@ -837,6 +1123,12 @@ function createApiClient(options) {
837
1123
  return data;
838
1124
  },
839
1125
  async getSubscriptionV1(packageName, subscriptionId, token) {
1126
+ if (typeof process !== "undefined" && process.emitWarning) {
1127
+ process.emitWarning(
1128
+ "purchases.subscriptions.get (v1) is deprecated by Google (shutdown Aug 2027). Use getSubscriptionV2() instead.",
1129
+ "DeprecationWarning"
1130
+ );
1131
+ }
840
1132
  const { data } = await http.get(
841
1133
  `/${packageName}/purchases/subscriptions/${subscriptionId}/tokens/${token}`
842
1134
  );
@@ -922,7 +1214,7 @@ function createApiClient(options) {
922
1214
  "application/octet-stream"
923
1215
  );
924
1216
  if (!data.deobfuscationFile) {
925
- throw new ApiError(
1217
+ throw new PlayApiError(
926
1218
  "Upload succeeded but no deobfuscation file data returned",
927
1219
  "API_EMPTY_RESPONSE",
928
1220
  200,
@@ -1084,10 +1376,7 @@ function createApiClient(options) {
1084
1376
  return data;
1085
1377
  },
1086
1378
  async create(packageName, body) {
1087
- const { data } = await http.post(
1088
- `/${packageName}/purchaseOptions`,
1089
- body
1090
- );
1379
+ const { data } = await http.post(`/${packageName}/purchaseOptions`, body);
1091
1380
  return data;
1092
1381
  },
1093
1382
  async activate(packageName, purchaseOptionId) {
@@ -1129,10 +1418,71 @@ function createApiClient(options) {
1129
1418
  return data.generatedApks || [];
1130
1419
  },
1131
1420
  async download(packageName, versionCode, id) {
1132
- return http.download(
1133
- `/${packageName}/generatedApks/${versionCode}/download/${id}`
1134
- );
1421
+ return http.download(`/${packageName}/generatedApks/${versionCode}/download/${id}`);
1422
+ }
1423
+ }
1424
+ };
1425
+ }
1426
+
1427
+ // src/rate-limiter.ts
1428
+ var RATE_LIMIT_BUCKETS = {
1429
+ default: { name: "default", maxTokens: 200, refillRate: 200, refillIntervalMs: 1e3 },
1430
+ reviewsGet: { name: "reviewsGet", maxTokens: 200, refillRate: 200, refillIntervalMs: 36e5 },
1431
+ reviewsPost: {
1432
+ name: "reviewsPost",
1433
+ maxTokens: 2e3,
1434
+ refillRate: 2e3,
1435
+ refillIntervalMs: 864e5
1436
+ },
1437
+ voidedBurst: { name: "voidedBurst", maxTokens: 30, refillRate: 30, refillIntervalMs: 3e4 },
1438
+ voidedDaily: {
1439
+ name: "voidedDaily",
1440
+ maxTokens: 6e3,
1441
+ refillRate: 6e3,
1442
+ refillIntervalMs: 864e5
1443
+ },
1444
+ reporting: { name: "reporting", maxTokens: 10, refillRate: 10, refillIntervalMs: 1e3 }
1445
+ };
1446
+ function createRateLimiter(buckets) {
1447
+ const states = /* @__PURE__ */ new Map();
1448
+ if (buckets) {
1449
+ for (const bucket of buckets) {
1450
+ states.set(bucket.name, {
1451
+ tokens: bucket.maxTokens,
1452
+ lastRefillTime: Date.now(),
1453
+ config: bucket
1454
+ });
1455
+ }
1456
+ }
1457
+ return {
1458
+ async acquire(bucket) {
1459
+ const state = states.get(bucket);
1460
+ if (!state) return;
1461
+ const now = Date.now();
1462
+ const elapsed = now - state.lastRefillTime;
1463
+ const refill = Math.floor(
1464
+ elapsed / state.config.refillIntervalMs * state.config.refillRate
1465
+ );
1466
+ if (refill > 0) {
1467
+ state.tokens = Math.min(state.config.maxTokens, state.tokens + refill);
1468
+ state.lastRefillTime = now;
1135
1469
  }
1470
+ if (state.tokens > 0) {
1471
+ state.tokens--;
1472
+ return;
1473
+ }
1474
+ const tokensNeeded = 1;
1475
+ const waitMs = Math.ceil(
1476
+ tokensNeeded / state.config.refillRate * state.config.refillIntervalMs
1477
+ );
1478
+ await new Promise((r) => setTimeout(r, waitMs));
1479
+ const afterWait = Date.now();
1480
+ const totalElapsed = afterWait - state.lastRefillTime;
1481
+ const newTokens = Math.floor(
1482
+ totalElapsed / state.config.refillIntervalMs * state.config.refillRate
1483
+ );
1484
+ state.tokens = Math.min(state.config.maxTokens, newTokens) - 1;
1485
+ state.lastRefillTime = afterWait;
1136
1486
  }
1137
1487
  };
1138
1488
  }
@@ -1141,8 +1491,10 @@ function createApiClient(options) {
1141
1491
  var REPORTING_BASE_URL = "https://playdeveloperreporting.googleapis.com/v1beta1";
1142
1492
  function createReportingClient(options) {
1143
1493
  const http = createHttpClient({ ...options, baseUrl: REPORTING_BASE_URL });
1494
+ const limiter = options.rateLimiter ?? createRateLimiter([RATE_LIMIT_BUCKETS["reporting"]]);
1144
1495
  return {
1145
1496
  async queryMetricSet(packageName, metricSet, query) {
1497
+ await limiter.acquire("reporting");
1146
1498
  const { data } = await http.post(
1147
1499
  `/apps/${packageName}/${metricSet}:query`,
1148
1500
  query
@@ -1150,10 +1502,12 @@ function createReportingClient(options) {
1150
1502
  return data;
1151
1503
  },
1152
1504
  async getAnomalies(packageName) {
1505
+ await limiter.acquire("reporting");
1153
1506
  const { data } = await http.get(`/apps/${packageName}/anomalies`);
1154
1507
  return data;
1155
1508
  },
1156
1509
  async searchErrorIssues(packageName, filter, pageSize, pageToken) {
1510
+ await limiter.acquire("reporting");
1157
1511
  const params = {};
1158
1512
  if (filter) params["filter"] = filter;
1159
1513
  if (pageSize) params["pageSize"] = String(pageSize);
@@ -1165,6 +1519,7 @@ function createReportingClient(options) {
1165
1519
  return data;
1166
1520
  },
1167
1521
  async searchErrorReports(packageName, issueName, pageSize, pageToken) {
1522
+ await limiter.acquire("reporting");
1168
1523
  const params = {};
1169
1524
  if (pageSize) params["pageSize"] = String(pageSize);
1170
1525
  if (pageToken) params["pageToken"] = pageToken;
@@ -1301,84 +1656,17 @@ function createEnterpriseClient(options) {
1301
1656
  return {
1302
1657
  apps: {
1303
1658
  async create(organizationId, app) {
1304
- const { data } = await http.post(
1305
- `/${organizationId}/apps`,
1306
- app
1307
- );
1659
+ const { data } = await http.post(`/${organizationId}/apps`, app);
1308
1660
  return data;
1309
1661
  },
1310
1662
  async list(organizationId) {
1311
- const { data } = await http.get(
1312
- `/${organizationId}/apps`
1313
- );
1663
+ const { data } = await http.get(`/${organizationId}/apps`);
1314
1664
  return data;
1315
1665
  }
1316
1666
  }
1317
1667
  };
1318
1668
  }
1319
1669
 
1320
- // src/rate-limiter.ts
1321
- var RATE_LIMIT_BUCKETS = {
1322
- default: { name: "default", maxTokens: 200, refillRate: 200, refillIntervalMs: 1e3 },
1323
- reviewsGet: { name: "reviewsGet", maxTokens: 200, refillRate: 200, refillIntervalMs: 36e5 },
1324
- reviewsPost: {
1325
- name: "reviewsPost",
1326
- maxTokens: 2e3,
1327
- refillRate: 2e3,
1328
- refillIntervalMs: 864e5
1329
- },
1330
- voidedBurst: { name: "voidedBurst", maxTokens: 30, refillRate: 30, refillIntervalMs: 3e4 },
1331
- voidedDaily: {
1332
- name: "voidedDaily",
1333
- maxTokens: 6e3,
1334
- refillRate: 6e3,
1335
- refillIntervalMs: 864e5
1336
- }
1337
- };
1338
- function createRateLimiter(buckets) {
1339
- const states = /* @__PURE__ */ new Map();
1340
- if (buckets) {
1341
- for (const bucket of buckets) {
1342
- states.set(bucket.name, {
1343
- tokens: bucket.maxTokens,
1344
- lastRefillTime: Date.now(),
1345
- config: bucket
1346
- });
1347
- }
1348
- }
1349
- return {
1350
- async acquire(bucket) {
1351
- const state = states.get(bucket);
1352
- if (!state) return;
1353
- const now = Date.now();
1354
- const elapsed = now - state.lastRefillTime;
1355
- const refill = Math.floor(
1356
- elapsed / state.config.refillIntervalMs * state.config.refillRate
1357
- );
1358
- if (refill > 0) {
1359
- state.tokens = Math.min(state.config.maxTokens, state.tokens + refill);
1360
- state.lastRefillTime = now;
1361
- }
1362
- if (state.tokens > 0) {
1363
- state.tokens--;
1364
- return;
1365
- }
1366
- const tokensNeeded = 1;
1367
- const waitMs = Math.ceil(
1368
- tokensNeeded / state.config.refillRate * state.config.refillIntervalMs
1369
- );
1370
- await new Promise((r) => setTimeout(r, waitMs));
1371
- const afterWait = Date.now();
1372
- const totalElapsed = afterWait - state.lastRefillTime;
1373
- const newTokens = Math.floor(
1374
- totalElapsed / state.config.refillIntervalMs * state.config.refillRate
1375
- );
1376
- state.tokens = Math.min(state.config.maxTokens, newTokens) - 1;
1377
- state.lastRefillTime = afterWait;
1378
- }
1379
- };
1380
- }
1381
-
1382
1670
  // src/paginate.ts
1383
1671
  async function* paginate(fetchPage, options) {
1384
1672
  let pageToken = options?.startPageToken;
@@ -1431,8 +1719,9 @@ async function paginateParallel(fetchPage, pageTokens, concurrency = 4) {
1431
1719
  return { items: allItems, nextPageToken: lastNextPageToken };
1432
1720
  }
1433
1721
  export {
1434
- ApiError,
1722
+ PlayApiError,
1435
1723
  RATE_LIMIT_BUCKETS,
1724
+ RESUMABLE_THRESHOLD,
1436
1725
  createApiClient,
1437
1726
  createEnterpriseClient,
1438
1727
  createGamesClient,