@gpc-cli/core 0.9.72 → 0.9.74

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
@@ -4659,19 +4659,12 @@ async function diffAchievementConfig(client, achievementId, localData) {
4659
4659
  validateResourceId(achievementId, "achievement");
4660
4660
  const remote = await client.achievements.get(achievementId);
4661
4661
  const diffs = [];
4662
- const fields = [
4663
- "achievementType",
4664
- "initialState",
4665
- "stepsToUnlock",
4666
- "draft"
4667
- ];
4662
+ const fields = ["achievementType", "initialState", "stepsToUnlock", "draft"];
4668
4663
  for (const field of fields) {
4669
4664
  const localVal = JSON.stringify(
4670
4665
  localData[field] ?? null
4671
4666
  );
4672
- const remoteVal = JSON.stringify(
4673
- remote[field] ?? null
4674
- );
4667
+ const remoteVal = JSON.stringify(remote[field] ?? null);
4675
4668
  if (localVal !== remoteVal) {
4676
4669
  diffs.push({ field, local: localVal, remote: remoteVal });
4677
4670
  }
@@ -4707,9 +4700,7 @@ async function diffLeaderboardConfig(client, leaderboardId, localData) {
4707
4700
  const localVal = JSON.stringify(
4708
4701
  localData[field] ?? null
4709
4702
  );
4710
- const remoteVal = JSON.stringify(
4711
- remote[field] ?? null
4712
- );
4703
+ const remoteVal = JSON.stringify(remote[field] ?? null);
4713
4704
  if (localVal !== remoteVal) {
4714
4705
  diffs.push({ field, local: localVal, remote: remoteVal });
4715
4706
  }
@@ -4964,7 +4955,7 @@ async function initProject(options) {
4964
4955
  const preflightrc = JSON.stringify(
4965
4956
  {
4966
4957
  failOn: "error",
4967
- targetSdkMinimum: 35,
4958
+ targetSdkMinimum: 36,
4968
4959
  maxDownloadSizeMb: 150,
4969
4960
  allowedPermissions: [],
4970
4961
  disabledRules: [],
@@ -5141,7 +5132,8 @@ async function loadPreflightConfig(configPath) {
5141
5132
  }
5142
5133
 
5143
5134
  // src/preflight/aab-reader.ts
5144
- import { open as yauzlOpen } from "yauzl";
5135
+ import { readFile as readFile10 } from "fs/promises";
5136
+ import { fromBuffer } from "yauzl";
5145
5137
 
5146
5138
  // src/preflight/manifest-parser.ts
5147
5139
  import protobuf from "protobufjs";
@@ -5330,12 +5322,20 @@ var AAB_MANIFEST_PATH = "base/manifest/AndroidManifest.xml";
5330
5322
  var APK_MANIFEST_PATH = "AndroidManifest.xml";
5331
5323
  var SO_HEADER_BYTES = 4096;
5332
5324
  var SO_PATH_RE = /(?:^|\/)lib\/[^/]+\/[^/]+\.so$/;
5325
+ var SCAN_TIMEOUT_MS = 6e4;
5333
5326
  function detectManifestPath(filePath) {
5334
5327
  return filePath.toLowerCase().endsWith(".apk") ? APK_MANIFEST_PATH : AAB_MANIFEST_PATH;
5335
5328
  }
5336
5329
  async function readAab(aabPath) {
5337
5330
  const manifestPath = detectManifestPath(aabPath);
5338
- const { zipfile, entries, manifestBuf, soHeaders } = await openAndScan(aabPath, manifestPath);
5331
+ let buffer;
5332
+ try {
5333
+ buffer = await readFile10(aabPath);
5334
+ } catch (err) {
5335
+ const msg = err instanceof Error ? err.message : String(err);
5336
+ throw new Error(`Could not read ${aabPath}: ${msg}`);
5337
+ }
5338
+ const { zipfile, entries, manifestBuf, soHeaders } = await openAndScan(buffer, manifestPath);
5339
5339
  zipfile.close();
5340
5340
  if (!manifestBuf) {
5341
5341
  const fileType = aabPath.toLowerCase().endsWith(".apk") ? "APK" : "AAB";
@@ -5372,98 +5372,125 @@ function createFallbackManifest() {
5372
5372
  providers: []
5373
5373
  };
5374
5374
  }
5375
- function openAndScan(aabPath, manifestPath = AAB_MANIFEST_PATH) {
5375
+ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
5376
5376
  return new Promise((resolve2, reject) => {
5377
- yauzlOpen(aabPath, { lazyEntries: true, autoClose: false }, (err, zipfile) => {
5378
- if (err || !zipfile) {
5379
- reject(err ?? new Error("Failed to open AAB file"));
5380
- return;
5377
+ let settled = false;
5378
+ const timer = setTimeout(() => {
5379
+ if (!settled) {
5380
+ settled = true;
5381
+ reject(new Error("Timed out reading the archive. The file may be corrupt or truncated."));
5381
5382
  }
5382
- const entries = [];
5383
- const soHeaders = /* @__PURE__ */ new Map();
5384
- let manifestBuf = null;
5385
- let rejected = false;
5386
- let pendingStreams = 0;
5387
- let entrysDone = false;
5388
- function fail(error) {
5389
- if (!rejected) {
5390
- rejected = true;
5391
- zipfile.close();
5392
- reject(error);
5383
+ }, SCAN_TIMEOUT_MS);
5384
+ function settleResolve(value) {
5385
+ if (settled) return;
5386
+ settled = true;
5387
+ clearTimeout(timer);
5388
+ resolve2(value);
5389
+ }
5390
+ function settleReject(error) {
5391
+ if (settled) return;
5392
+ settled = true;
5393
+ clearTimeout(timer);
5394
+ reject(error);
5395
+ }
5396
+ try {
5397
+ fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
5398
+ if (err || !zipfile) {
5399
+ settleReject(err ?? new Error("Failed to open AAB file"));
5400
+ return;
5393
5401
  }
5394
- }
5395
- function maybeResolve() {
5396
- if (!rejected && entrysDone && pendingStreams === 0) {
5397
- resolve2({ zipfile, entries, manifestBuf, soHeaders });
5402
+ const entries = [];
5403
+ const soHeaders = /* @__PURE__ */ new Map();
5404
+ let manifestBuf = null;
5405
+ let pendingStreams = 0;
5406
+ let entrysDone = false;
5407
+ function fail(error) {
5408
+ if (settled) return;
5409
+ try {
5410
+ zipfile.close();
5411
+ } catch {
5412
+ }
5413
+ settleReject(error);
5398
5414
  }
5399
- }
5400
- function readEntryStream(entry, onData, maxBytes) {
5401
- pendingStreams++;
5402
- zipfile.openReadStream(entry, (streamErr, stream) => {
5403
- if (streamErr || !stream) {
5404
- fail(streamErr ?? new Error(`Failed to read ${entry.fileName}`));
5405
- return;
5415
+ function maybeResolve() {
5416
+ if (!settled && entrysDone && pendingStreams === 0) {
5417
+ settleResolve({ zipfile, entries, manifestBuf, soHeaders });
5406
5418
  }
5407
- const chunks = [];
5408
- let totalBytes = 0;
5409
- let done = false;
5410
- stream.on("data", (chunk) => {
5411
- chunks.push(chunk);
5412
- totalBytes += chunk.length;
5413
- if (maxBytes && totalBytes >= maxBytes) {
5414
- stream.destroy();
5415
- }
5416
- });
5417
- stream.on("error", (e) => {
5418
- if (maxBytes && totalBytes >= maxBytes) return;
5419
- fail(e);
5420
- });
5421
- const finish = () => {
5422
- if (done) return;
5423
- done = true;
5424
- const buf = Buffer.concat(chunks);
5425
- onData(maxBytes ? buf.subarray(0, maxBytes) : buf);
5426
- pendingStreams--;
5427
- maybeResolve();
5428
- };
5429
- stream.on("end", finish);
5430
- stream.on("close", () => {
5431
- if (totalBytes > 0 && maxBytes) finish();
5432
- });
5433
- });
5434
- }
5435
- zipfile.on("error", fail);
5436
- zipfile.on("entry", (entry) => {
5437
- if (rejected) return;
5438
- const path = entry.fileName;
5439
- if (!path.endsWith("/")) {
5440
- entries.push({
5441
- path,
5442
- compressedSize: entry.compressedSize,
5443
- uncompressedSize: entry.uncompressedSize
5444
- });
5445
5419
  }
5446
- if (path === manifestPath) {
5447
- readEntryStream(entry, (buf) => {
5448
- manifestBuf = buf;
5420
+ function readEntryStream(entry, onData, maxBytes) {
5421
+ pendingStreams++;
5422
+ zipfile.openReadStream(entry, (streamErr, stream) => {
5423
+ if (streamErr || !stream) {
5424
+ fail(streamErr ?? new Error(`Failed to read ${entry.fileName}`));
5425
+ return;
5426
+ }
5427
+ const chunks = [];
5428
+ let totalBytes = 0;
5429
+ let done = false;
5430
+ stream.on("data", (chunk) => {
5431
+ chunks.push(chunk);
5432
+ totalBytes += chunk.length;
5433
+ if (maxBytes && totalBytes >= maxBytes) {
5434
+ stream.destroy();
5435
+ }
5436
+ });
5437
+ stream.on("error", (e) => {
5438
+ if (maxBytes && totalBytes >= maxBytes) return;
5439
+ fail(e);
5440
+ });
5441
+ const finish = () => {
5442
+ if (done) return;
5443
+ done = true;
5444
+ const buf = Buffer.concat(chunks);
5445
+ onData(maxBytes ? buf.subarray(0, maxBytes) : buf);
5446
+ pendingStreams--;
5447
+ maybeResolve();
5448
+ };
5449
+ stream.on("end", finish);
5450
+ stream.on("close", () => {
5451
+ if (totalBytes > 0 && maxBytes) finish();
5452
+ });
5449
5453
  });
5450
- } else if (SO_PATH_RE.test(path)) {
5451
- readEntryStream(
5452
- entry,
5453
- (buf) => {
5454
- soHeaders.set(path, buf);
5455
- },
5456
- SO_HEADER_BYTES
5457
- );
5458
5454
  }
5455
+ zipfile.on("error", fail);
5456
+ zipfile.on("entry", (entry) => {
5457
+ if (settled) return;
5458
+ try {
5459
+ const path = entry.fileName;
5460
+ if (!path.endsWith("/")) {
5461
+ entries.push({
5462
+ path,
5463
+ compressedSize: entry.compressedSize,
5464
+ uncompressedSize: entry.uncompressedSize
5465
+ });
5466
+ }
5467
+ if (path === manifestPath) {
5468
+ readEntryStream(entry, (buf) => {
5469
+ manifestBuf = buf;
5470
+ });
5471
+ } else if (SO_PATH_RE.test(path)) {
5472
+ readEntryStream(
5473
+ entry,
5474
+ (buf) => {
5475
+ soHeaders.set(path, buf);
5476
+ },
5477
+ SO_HEADER_BYTES
5478
+ );
5479
+ }
5480
+ zipfile.readEntry();
5481
+ } catch (e) {
5482
+ fail(e instanceof Error ? e : new Error(String(e)));
5483
+ }
5484
+ });
5485
+ zipfile.on("end", () => {
5486
+ entrysDone = true;
5487
+ maybeResolve();
5488
+ });
5459
5489
  zipfile.readEntry();
5460
5490
  });
5461
- zipfile.on("end", () => {
5462
- entrysDone = true;
5463
- maybeResolve();
5464
- });
5465
- zipfile.readEntry();
5466
- });
5491
+ } catch (err) {
5492
+ settleReject(err instanceof Error ? err : new Error(String(err)));
5493
+ }
5467
5494
  });
5468
5495
  }
5469
5496
 
@@ -5582,8 +5609,8 @@ var manifestScanner = {
5582
5609
  ruleId: "geofencing-foreground-service",
5583
5610
  severity: "warning",
5584
5611
  title: `Possible geofencing via foreground service "${service.name}"`,
5585
- message: `Service "${service.name}" uses foregroundServiceType "location" and the app declares ACCESS_BACKGROUND_LOCATION. Google Play no longer approves geofencing as a foreground service use case (April 2026 policy). Compliance deadline: May 15, 2026.`,
5586
- suggestion: 'If this service performs geofencing, migrate to WorkManager or AlarmManager. If this is legitimate background location tracking (navigation, fitness), suppress this rule via .preflightrc.json: "disabledRules": ["geofencing-foreground-service"].',
5612
+ message: `Service "${service.name}" uses foregroundServiceType "location" and the app declares ACCESS_BACKGROUND_LOCATION. Google Play no longer approves geofencing as a foreground service use case (sensitive-permissions policy announced April 15, 2026); enforcement for apps targeting Android 17 (API 37+) begins October 28, 2026.`,
5613
+ suggestion: 'If this service performs geofencing, migrate to the Geofencing API (LocationServices.getGeofencingClient) or WorkManager/AlarmManager. If this is legitimate background location tracking (navigation, fitness), suppress this rule via .preflightrc.json: "disabledRules": ["geofencing-foreground-service"].',
5587
5614
  policyUrl: "https://support.google.com/googleplay/android-developer/answer/16926792"
5588
5615
  });
5589
5616
  }
@@ -5812,14 +5839,28 @@ var permissionsScanner = {
5812
5839
  ].filter((p) => manifest.permissions.includes(p) && !allowed.has(p));
5813
5840
  if (contactsPerms.length > 0) {
5814
5841
  const names = contactsPerms.map((p) => p.split(".").pop()).join(", ");
5842
+ const enforced = manifest.targetSdk >= 37 ? ` Your app targets API ${manifest.targetSdk}, so this is enforced.` : "";
5815
5843
  findings.push({
5816
5844
  scanner: "permissions",
5817
5845
  ruleId: "contacts-permission-broad",
5818
5846
  severity: "warning",
5819
5847
  title: "Broad contacts access requires migration to Contact Picker",
5820
- message: `Your app declares ${names}. Google Play now requires the Android Contact Picker instead of broad contacts access. Compliance deadline: May 15, 2026.`,
5821
- suggestion: "Migrate to the Android Contact Picker API for user-initiated contact selection. Remove READ_CONTACTS/WRITE_CONTACTS unless your app is a dialer, messaging, or contacts management app.",
5822
- policyUrl: "https://support.google.com/googleplay/android-developer/answer/16926792"
5848
+ message: `Your app declares ${names}. Google Play's Contacts Permissions policy requires the Android Contact Picker (Intent.ACTION_PICK_CONTACTS) unless broad access is core to your app. Mandatory for apps targeting Android 17 (API 37+) on October 28, 2026.${enforced}`,
5849
+ suggestion: "Migrate to the Android Contact Picker (Intent.ACTION_PICK_CONTACTS) for user-initiated contact selection, or file a Permissions Declaration Form before October 28, 2026 if the picker cannot serve your core functionality. Remove READ_CONTACTS/WRITE_CONTACTS unless your app is a dialer, messaging, or contacts management app.",
5850
+ policyUrl: "https://support.google.com/googleplay/android-developer/answer/16935362"
5851
+ });
5852
+ }
5853
+ const fineLocation = "android.permission.ACCESS_FINE_LOCATION";
5854
+ if (manifest.permissions.includes(fineLocation) && !allowed.has(fineLocation)) {
5855
+ const targetsApi37 = manifest.targetSdk >= 37;
5856
+ findings.push({
5857
+ scanner: "permissions",
5858
+ ruleId: "location-minimal-scope",
5859
+ severity: targetsApi37 ? "warning" : "info",
5860
+ title: "Precise location \u2014 consider the location button (minimum scope)",
5861
+ message: `Your app declares ACCESS_FINE_LOCATION. Google Play's Location Permissions policy makes the Android location button the recommended minimum scope for transactional (one-time) precise location. Mandatory for apps targeting Android 17 (API 37+) on October 28, 2026.${targetsApi37 ? ` Your app targets API ${manifest.targetSdk}.` : ""}`,
5862
+ suggestion: 'For one-time/transactional precise location, use the system location button instead of requesting ACCESS_FINE_LOCATION up front. Only keep continuous fine location if navigation, fitness, or a similar feature is core functionality. Suppress via .preflightrc.json "disabledRules": ["location-minimal-scope"] if fine location is essential.',
5863
+ policyUrl: "https://support.google.com/googleplay/android-developer/answer/17033915"
5823
5864
  });
5824
5865
  }
5825
5866
  const broadHealthPerm = "android.permission.health.READ_ALL_HEALTH_DATA";
@@ -6013,7 +6054,7 @@ ${fileList}` + (hasPageSizeCompat ? "\nandroid:pageSizeCompat is set, so the app
6013
6054
  }
6014
6055
 
6015
6056
  // src/preflight/scanners/metadata-scanner.ts
6016
- import { readdir as readdir4, stat as stat6, readFile as readFile10 } from "fs/promises";
6057
+ import { readdir as readdir4, stat as stat6, readFile as readFile11 } from "fs/promises";
6017
6058
  import { join as join8 } from "path";
6018
6059
  var SAFE_LANG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
6019
6060
  var FILE_MAP2 = {
@@ -6072,7 +6113,7 @@ var metadataScanner = {
6072
6113
  for (const [fileName, field] of Object.entries(FILE_MAP2)) {
6073
6114
  const filePath = join8(langDir, fileName);
6074
6115
  try {
6075
- const content = await readFile10(filePath, "utf-8");
6116
+ const content = await readFile11(filePath, "utf-8");
6076
6117
  fields[field] = content.trimEnd();
6077
6118
  } catch {
6078
6119
  }
@@ -6145,7 +6186,7 @@ var metadataScanner = {
6145
6186
  const defaultLang = locales.includes("en-US") ? "en-US" : locales[0];
6146
6187
  const privacyPath = join8(dir, defaultLang, "privacy_policy_url.txt");
6147
6188
  try {
6148
- const url = await readFile10(privacyPath, "utf-8");
6189
+ const url = await readFile11(privacyPath, "utf-8");
6149
6190
  if (!url.trim()) throw new Error("empty");
6150
6191
  } catch {
6151
6192
  findings.push({
@@ -6170,7 +6211,7 @@ var metadataScanner = {
6170
6211
  };
6171
6212
 
6172
6213
  // src/preflight/scanners/secrets-scanner.ts
6173
- import { readFile as readFile11 } from "fs/promises";
6214
+ import { readFile as readFile12 } from "fs/promises";
6174
6215
 
6175
6216
  // src/preflight/scan-files.ts
6176
6217
  import { readdir as readdir5, stat as stat7 } from "fs/promises";
@@ -6290,7 +6331,7 @@ var secretsScanner = {
6290
6331
  for (const filePath of files) {
6291
6332
  let content;
6292
6333
  try {
6293
- content = await readFile11(filePath, "utf-8");
6334
+ content = await readFile12(filePath, "utf-8");
6294
6335
  } catch {
6295
6336
  continue;
6296
6337
  }
@@ -6318,7 +6359,7 @@ var secretsScanner = {
6318
6359
  };
6319
6360
 
6320
6361
  // src/preflight/scanners/billing-scanner.ts
6321
- import { readFile as readFile12 } from "fs/promises";
6362
+ import { readFile as readFile13 } from "fs/promises";
6322
6363
  var BILLING_PATTERNS = [
6323
6364
  {
6324
6365
  ruleId: "billing-stripe-sdk",
@@ -6379,7 +6420,7 @@ var billingScanner = {
6379
6420
  for (const filePath of files) {
6380
6421
  let content;
6381
6422
  try {
6382
- content = await readFile12(filePath, "utf-8");
6423
+ content = await readFile13(filePath, "utf-8");
6383
6424
  } catch {
6384
6425
  continue;
6385
6426
  }
@@ -6405,7 +6446,7 @@ var billingScanner = {
6405
6446
  };
6406
6447
 
6407
6448
  // src/preflight/scanners/privacy-scanner.ts
6408
- import { readFile as readFile13 } from "fs/promises";
6449
+ import { readFile as readFile14 } from "fs/promises";
6409
6450
  var TRACKING_SDKS = [
6410
6451
  {
6411
6452
  name: "Facebook SDK",
@@ -6445,7 +6486,7 @@ var privacyScanner = {
6445
6486
  for (const filePath of files) {
6446
6487
  let content;
6447
6488
  try {
6448
- content = await readFile13(filePath, "utf-8");
6489
+ content = await readFile14(filePath, "utf-8");
6449
6490
  } catch {
6450
6491
  continue;
6451
6492
  }
@@ -7145,7 +7186,7 @@ async function downloadGeneratedApk(client, packageName, versionCode, apkId, out
7145
7186
  }
7146
7187
 
7147
7188
  // src/commands/bundle-analysis.ts
7148
- import { readFile as readFile14, stat as stat8 } from "fs/promises";
7189
+ import { readFile as readFile15, stat as stat8 } from "fs/promises";
7149
7190
  var EOCD_SIGNATURE = 101010256;
7150
7191
  var CD_SIGNATURE = 33639248;
7151
7192
  var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
@@ -7225,7 +7266,7 @@ async function analyzeBundle(filePath) {
7225
7266
  if (!fileInfo || !fileInfo.isFile()) {
7226
7267
  throw new Error(`File not found: ${filePath}`);
7227
7268
  }
7228
- const buf = await readFile14(filePath);
7269
+ const buf = await readFile15(filePath);
7229
7270
  const cdEntries = parseCentralDirectory(buf);
7230
7271
  const fileType = detectFileType2(filePath);
7231
7272
  const isAab = fileType === "aab";
@@ -7311,7 +7352,7 @@ function topFiles(analysis, n = 20) {
7311
7352
  async function checkBundleSize(analysis, configPath = ".bundlesize.json") {
7312
7353
  let config;
7313
7354
  try {
7314
- const raw = await readFile14(configPath, "utf-8");
7355
+ const raw = await readFile15(configPath, "utf-8");
7315
7356
  config = JSON.parse(raw);
7316
7357
  } catch {
7317
7358
  return { passed: true, violations: [] };