@gpc-cli/core 0.9.54 → 0.9.56

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 CHANGED
@@ -45,23 +45,23 @@ const analysis = await analyzeBundle("./app.aab");
45
45
 
46
46
  ## Command Groups
47
47
 
48
- | Group | Functions |
49
- | ----------------- | ------------------------------------------------------------------------------------------------------------------- |
50
- | **Releases** | `uploadRelease`, `promoteRelease`, `updateRollout`, `getReleasesStatus`, `listTracks` |
51
- | **Listings** | `getListings`, `updateListing`, `pullListings`, `pushListings`, `diffListings` |
52
- | **Images** | `listImages`, `uploadImage`, `deleteImage` |
53
- | **Reviews** | `listReviews`, `getReview`, `replyToReview`, `exportReviews` |
54
- | **Vitals** | `getVitalsOverview`, `getVitalsCrashes`, `getVitalsAnr`, `getVitalsStartup`, `compareVitalsTrend`, `checkThreshold` |
55
- | **Subscriptions** | `listSubscriptions`, `createSubscription`, `updateSubscription`, `deleteSubscription`, `listOffers`, `createOffer` |
56
- | **IAP** | `listInAppProducts`, `createInAppProduct`, `syncInAppProducts` |
57
- | **Purchases** | `getProductPurchase`, `acknowledgeProductPurchase`, `refundOrder` |
58
- | **Reports** | `listReports`, `downloadReport` |
59
- | **Users** | `listUsers`, `inviteUser`, `updateUser`, `removeUser` |
60
- | **Testers** | `listTesters`, `addTesters`, `removeTesters`, `importTestersFromCsv` |
61
- | **Bundle** | `analyzeBundle`, `compareBundles` (zero-dependency AAB/APK size analysis) |
62
- | **Publishing** | `publish` (end-to-end: upload + track + notes + commit) |
48
+ | Group | Functions |
49
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
50
+ | **Releases** | `uploadRelease`, `promoteRelease`, `updateRollout`, `getReleasesStatus`, `listTracks` |
51
+ | **Listings** | `getListings`, `updateListing`, `pullListings`, `pushListings`, `diffListings` |
52
+ | **Images** | `listImages`, `uploadImage`, `deleteImage` |
53
+ | **Reviews** | `listReviews`, `getReview`, `replyToReview`, `exportReviews` |
54
+ | **Vitals** | `getVitalsOverview`, `getVitalsCrashes`, `getVitalsAnr`, `getVitalsStartup`, `compareVitalsTrend`, `checkThreshold` |
55
+ | **Subscriptions** | `listSubscriptions`, `createSubscription`, `updateSubscription`, `deleteSubscription`, `listOffers`, `createOffer` |
56
+ | **IAP** | `listInAppProducts`, `createInAppProduct`, `syncInAppProducts` |
57
+ | **Purchases** | `getProductPurchase`, `acknowledgeProductPurchase`, `refundOrder` |
58
+ | **Reports** | `listReports`, `downloadReport` |
59
+ | **Users** | `listUsers`, `inviteUser`, `updateUser`, `removeUser` |
60
+ | **Testers** | `listTesters`, `addTesters`, `removeTesters`, `importTestersFromCsv` |
61
+ | **Bundle** | `analyzeBundle`, `compareBundles` (zero-dependency AAB/APK size analysis) |
62
+ | **Publishing** | `publish` (end-to-end: upload + track + notes + commit) |
63
63
  | **Changelog** | `generateChangelog`, `fetchChangelog`, `formatChangelogEntry`, `buildLocaleBundle`, `renderPlayStore`, `renderMarkdown`, `renderJson`, `renderPrompt`, `translateBundle`, `resolveLocales` |
64
- | **Validation** | `validateUploadFile`, `validateImage`, `validatePreSubmission` |
64
+ | **Validation** | `validateUploadFile`, `validateImage`, `validatePreSubmission` |
65
65
 
66
66
  ## Utilities
67
67
 
package/dist/index.d.ts CHANGED
@@ -1472,4 +1472,60 @@ declare function decodeNotification(base64Payload: string): DecodedNotification;
1472
1472
  */
1473
1473
  declare function formatNotification(notification: DecodedNotification): Record<string, unknown>;
1474
1474
 
1475
- export { ApiError, type AppInfo, type AppStatus, type ApplyReleaseNotesResult, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type BundleSizeCheckResult, type BundleSizeConfig, type ChangelogEntry, type CommandContext, type CommitCluster, ConfigError, type CreateEnterpriseAppParams, DEFAULT_LIMITS, DEFAULT_MODELS, DEFAULT_PREFLIGHT_CONFIG, type DecodedNotification, type DiffToken, type DiscoverPluginsOptions, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ErrorReason, type ExportImagesOptions, type ExportImagesSummary, type FastlaneDetection, type FastlaneLane, type FetchChangelogOptions, type FieldLintResult, type FileValidationResult, type FindingSeverity, GOOGLE_PLAY_LANGUAGES, type GenerateOptions, type GeneratedChangelog, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, type GitRunner, GpcError, type ImageValidationResult, type InitOptions, type InitResult, type InternalSharingUploadResult, type ListIapOptions, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingFieldLimits, type ListingLintResult, type ListingsResult, type LoadedPlugin, type LocaleBundle, type LocaleEntry, type MigrationResult, NetworkError, type OneTimeProductDiff, type OutputMode, PERMISSION_PROPAGATION_WARNING, PLACEHOLDER_TEXT, PLAY_STORE_LIMIT, PROVIDER_WHITELIST, type ParsedCommit, type ParsedManifest, type ParsedMonth, type PlayStoreFormat, type PlayStoreRenderOptions, PluginManager, type PreflightConfig, type PreflightFinding, type PreflightOptions, type PreflightResult, type PreflightScanner, type Provider, type PublishOptions, type PublishResult, type PushResult, type QuotaUsage, RENDERERS, type RawCommit, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type Renderer, type ResolveAiConfigOptions, type ResolveLocalesOptions, type ReviewAnalysis, type ReviewExportOptions, type ReviewsFilterOptions, type RtdnStatus, SECTION_ORDER, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, SEVERITY_ORDER, type ScaffoldOptions, type ScaffoldResult, type Spinner, type StatusDiff, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionAnalytics, type SubscriptionDiff, type SyncResult, type ThresholdResult, type TrainConfig, type TrainState, type TranslateBundleOptions, type TranslatedBundle, type TranslationFailure, type TranslationPath, type TranslationResult, type Translator, type TranslatorConfig, type UploadResult, type ValidateCheck, type ValidateOptions, type ValidateResult, type VersionVitalsComparison, type VersionVitalsRow, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WatchOptions, type WatchVitalsOptions, type WebhookPayload, abortTrain, acknowledgeProductPurchase, activateBasePlan, activateOffer, addRecoveryTargeting, addTesters, advanceTrain, analyzeBundle, analyzeRemoteListings, analyzeReviews, applyReleaseNotes, batchGetOrders, batchSyncInAppProducts, buildLocaleBundle, bundleToReleaseNotes, cancelRecoveryAction, cancelSubscriptionPurchase, cancelSubscriptionV2, checkBundleSize, checkThreshold, classifyError, clearAuditLog, compareBundles, compareVersionVitals, compareVitalsTrend, computeStatusDiff, consumeProductPurchase, convertRegionPrices, createAuditEntry, createDeviceTier, createEnterpriseApp, createExternalTransaction, createGrant, createInAppProduct, createOffer, createOneTimeOffer, createOneTimeProduct, createRecoveryAction, createSpinner, createSubscription, createTrack, createTranslator, deactivateBasePlan, deactivateOffer, decodeNotification, defaultGitRunner, deferSubscriptionPurchase, deferSubscriptionV2, deleteBasePlan, deleteGrant, deleteImage, deleteInAppProduct, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffListings, diffListingsCommand, diffListingsEnhanced, diffOneTimeProduct, diffReleases, diffSubscription, discoverPlugins, downloadGeneratedApk, downloadReport, exportImages, exportReviews, fetchAggregateCost, fetchChangelog, fetchReleaseNotes, formatChangelogEntry, formatCustomPayload, formatDiscordPayload, formatJunit, formatNotification, formatOutput, formatPathLabel, formatSlackPayload, formatStatusDiff, formatStatusSummary, formatStatusTable, formatWordDiff, generateChangelog, generateMigrationPlan, generateNotesFromGit, getAllScannerNames, getAppInfo, getAppStatus, getCountryAvailability, getDeviceTier, getExternalTransaction, getInAppProduct, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getOrderDetails, getProductPurchase, getProductPurchaseV2, getQuotaUsage, getReleasesStatus, getReview, getRtdnStatus, getSubscription, getSubscriptionAnalytics, getSubscriptionPurchase, getTrainStatus, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsErrorCount, getVitalsLmk, getVitalsMemory, getVitalsOverview, getVitalsRendering, getVitalsStartup, importDataSafety, importTestersFromCsv, initAudit, initProject, inviteUser, isFinancialReportType, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, lintListing, lintListings, lintLocalListings, listAchievements, listAuditEvents, listDeviceTiers, listEvents, listGeneratedApks, listGrants, listImages, listInAppProducts, listLeaderboards, listOffers, listOneTimeOffers, listOneTimeProducts, listRecoveryActions, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadPreflightConfig, loadStatusCache, maybePaginate, migratePrices, parseAppfile, parseCommit, parseFastfile, parseGrantArg, parseMonth, parseRemoteUrl, pauseTrain, promoteRelease, publish, publishEnterpriseApp, pullListings, pushListings, readListingsFromDir, readReleaseNotesFromDir, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, relativeTime, removeTesters, removeUser, renderJson, renderMarkdown, renderPlayStore, renderPlayStoreJson, renderPlayStoreMd, renderPlayStorePrompt, renderPrompt, replyToReview, resolveAiConfig, resolveLocales, revokeSubscriptionPurchase, runPreflight, runWatchLoop, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, searchAuditEvents, searchVitalsErrors, sendNotification, sendWebhook, sortResults, startTrain, statusHasBreach, syncInAppProducts, topFiles, trackBreachState, translateBundle, updateAppDetails, updateDataSafety, updateGrant, updateInAppProduct, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateBundleForApply, validateImage, validateLanguageCode, validatePackageName, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, waitForBundleProcessing, watchVitalsWithAutoHalt, wordDiff, writeAuditLog, writeListingsToDir, writeMigrationOutput };
1475
+ interface KeystoreFingerprint {
1476
+ sha256: string;
1477
+ alias: string;
1478
+ keystorePath: string;
1479
+ }
1480
+ interface ApiSigningFingerprint {
1481
+ sha256: string;
1482
+ versionCode: number;
1483
+ }
1484
+ interface SigningKeyComparison {
1485
+ local?: KeystoreFingerprint;
1486
+ api?: ApiSigningFingerprint;
1487
+ match: boolean | null;
1488
+ }
1489
+ declare function normalizeFingerprint(fp: string): string;
1490
+ declare function parseKeytoolOutput(stdout: string): {
1491
+ sha256: string;
1492
+ alias: string;
1493
+ };
1494
+ declare function getKeystoreFingerprint(keystorePath: string, storePassword: string, keyAlias?: string): Promise<KeystoreFingerprint>;
1495
+ declare function getApiSigningFingerprint(accessToken: string, packageName: string, apiHost?: string): Promise<ApiSigningFingerprint | null>;
1496
+ declare function compareFingerprints(a: string, b: string): boolean;
1497
+
1498
+ interface SigningConsistencyResult {
1499
+ currentVersionCode: number;
1500
+ currentFingerprint: string;
1501
+ previousVersionCode?: number;
1502
+ previousFingerprint?: string;
1503
+ consistent: boolean;
1504
+ firstRelease: boolean;
1505
+ }
1506
+ declare function checkSigningConsistency(accessToken: string, packageName: string, apiHost?: string): Promise<SigningConsistencyResult>;
1507
+
1508
+ interface ChecklistItem {
1509
+ id: string;
1510
+ title: string;
1511
+ status: "done" | "action-needed" | "cannot-detect";
1512
+ detail?: string;
1513
+ actionUrl?: string;
1514
+ }
1515
+ interface ChecklistResult {
1516
+ items: ChecklistItem[];
1517
+ completed: number;
1518
+ total: number;
1519
+ }
1520
+ interface ChecklistInput {
1521
+ authenticated: boolean;
1522
+ accountEmail?: string;
1523
+ appAccessible?: boolean;
1524
+ bundleCount?: number;
1525
+ hasGeneratedApks?: boolean;
1526
+ interactiveAnswers?: Record<string, boolean>;
1527
+ }
1528
+ declare function buildChecklist(input: ChecklistInput): ChecklistResult;
1529
+ declare function renderChecklistMarkdown(result: ChecklistResult, accountEmail: string): string;
1530
+
1531
+ export { ApiError, type ApiSigningFingerprint, type AppInfo, type AppStatus, type ApplyReleaseNotesResult, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type BundleSizeCheckResult, type BundleSizeConfig, type ChangelogEntry, type ChecklistInput, type ChecklistItem, type ChecklistResult, type CommandContext, type CommitCluster, ConfigError, type CreateEnterpriseAppParams, DEFAULT_LIMITS, DEFAULT_MODELS, DEFAULT_PREFLIGHT_CONFIG, type DecodedNotification, type DiffToken, type DiscoverPluginsOptions, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ErrorReason, type ExportImagesOptions, type ExportImagesSummary, type FastlaneDetection, type FastlaneLane, type FetchChangelogOptions, type FieldLintResult, type FileValidationResult, type FindingSeverity, GOOGLE_PLAY_LANGUAGES, type GenerateOptions, type GeneratedChangelog, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, type GitRunner, GpcError, type ImageValidationResult, type InitOptions, type InitResult, type InternalSharingUploadResult, type KeystoreFingerprint, type ListIapOptions, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingFieldLimits, type ListingLintResult, type ListingsResult, type LoadedPlugin, type LocaleBundle, type LocaleEntry, type MigrationResult, NetworkError, type OneTimeProductDiff, type OutputMode, PERMISSION_PROPAGATION_WARNING, PLACEHOLDER_TEXT, PLAY_STORE_LIMIT, PROVIDER_WHITELIST, type ParsedCommit, type ParsedManifest, type ParsedMonth, type PlayStoreFormat, type PlayStoreRenderOptions, PluginManager, type PreflightConfig, type PreflightFinding, type PreflightOptions, type PreflightResult, type PreflightScanner, type Provider, type PublishOptions, type PublishResult, type PushResult, type QuotaUsage, RENDERERS, type RawCommit, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type Renderer, type ResolveAiConfigOptions, type ResolveLocalesOptions, type ReviewAnalysis, type ReviewExportOptions, type ReviewsFilterOptions, type RtdnStatus, SECTION_ORDER, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, SEVERITY_ORDER, type ScaffoldOptions, type ScaffoldResult, type SigningConsistencyResult, type SigningKeyComparison, type Spinner, type StatusDiff, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionAnalytics, type SubscriptionDiff, type SyncResult, type ThresholdResult, type TrainConfig, type TrainState, type TranslateBundleOptions, type TranslatedBundle, type TranslationFailure, type TranslationPath, type TranslationResult, type Translator, type TranslatorConfig, type UploadResult, type ValidateCheck, type ValidateOptions, type ValidateResult, type VersionVitalsComparison, type VersionVitalsRow, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WatchOptions, type WatchVitalsOptions, type WebhookPayload, abortTrain, acknowledgeProductPurchase, activateBasePlan, activateOffer, addRecoveryTargeting, addTesters, advanceTrain, analyzeBundle, analyzeRemoteListings, analyzeReviews, applyReleaseNotes, batchGetOrders, batchSyncInAppProducts, buildChecklist, buildLocaleBundle, bundleToReleaseNotes, cancelRecoveryAction, cancelSubscriptionPurchase, cancelSubscriptionV2, checkBundleSize, checkSigningConsistency, checkThreshold, classifyError, clearAuditLog, compareBundles, compareFingerprints, compareVersionVitals, compareVitalsTrend, computeStatusDiff, consumeProductPurchase, convertRegionPrices, createAuditEntry, createDeviceTier, createEnterpriseApp, createExternalTransaction, createGrant, createInAppProduct, createOffer, createOneTimeOffer, createOneTimeProduct, createRecoveryAction, createSpinner, createSubscription, createTrack, createTranslator, deactivateBasePlan, deactivateOffer, decodeNotification, defaultGitRunner, deferSubscriptionPurchase, deferSubscriptionV2, deleteBasePlan, deleteGrant, deleteImage, deleteInAppProduct, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffListings, diffListingsCommand, diffListingsEnhanced, diffOneTimeProduct, diffReleases, diffSubscription, discoverPlugins, downloadGeneratedApk, downloadReport, exportImages, exportReviews, fetchAggregateCost, fetchChangelog, fetchReleaseNotes, formatChangelogEntry, formatCustomPayload, formatDiscordPayload, formatJunit, formatNotification, formatOutput, formatPathLabel, formatSlackPayload, formatStatusDiff, formatStatusSummary, formatStatusTable, formatWordDiff, generateChangelog, generateMigrationPlan, generateNotesFromGit, getAllScannerNames, getApiSigningFingerprint, getAppInfo, getAppStatus, getCountryAvailability, getDeviceTier, getExternalTransaction, getInAppProduct, getKeystoreFingerprint, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getOrderDetails, getProductPurchase, getProductPurchaseV2, getQuotaUsage, getReleasesStatus, getReview, getRtdnStatus, getSubscription, getSubscriptionAnalytics, getSubscriptionPurchase, getTrainStatus, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsErrorCount, getVitalsLmk, getVitalsMemory, getVitalsOverview, getVitalsRendering, getVitalsStartup, importDataSafety, importTestersFromCsv, initAudit, initProject, inviteUser, isFinancialReportType, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, lintListing, lintListings, lintLocalListings, listAchievements, listAuditEvents, listDeviceTiers, listEvents, listGeneratedApks, listGrants, listImages, listInAppProducts, listLeaderboards, listOffers, listOneTimeOffers, listOneTimeProducts, listRecoveryActions, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadPreflightConfig, loadStatusCache, maybePaginate, migratePrices, normalizeFingerprint, parseAppfile, parseCommit, parseFastfile, parseGrantArg, parseKeytoolOutput, parseMonth, parseRemoteUrl, pauseTrain, promoteRelease, publish, publishEnterpriseApp, pullListings, pushListings, readListingsFromDir, readReleaseNotesFromDir, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, relativeTime, removeTesters, removeUser, renderChecklistMarkdown, renderJson, renderMarkdown, renderPlayStore, renderPlayStoreJson, renderPlayStoreMd, renderPlayStorePrompt, renderPrompt, replyToReview, resolveAiConfig, resolveLocales, revokeSubscriptionPurchase, runPreflight, runWatchLoop, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, searchAuditEvents, searchVitalsErrors, sendNotification, sendWebhook, sortResults, startTrain, statusHasBreach, syncInAppProducts, topFiles, trackBreachState, translateBundle, updateAppDetails, updateDataSafety, updateGrant, updateInAppProduct, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateBundleForApply, validateImage, validateLanguageCode, validatePackageName, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, waitForBundleProcessing, watchVitalsWithAutoHalt, wordDiff, writeAuditLog, writeListingsToDir, writeMigrationOutput };
package/dist/index.js CHANGED
@@ -5189,6 +5189,28 @@ var manifestScanner = {
5189
5189
  }
5190
5190
  }
5191
5191
  }
5192
+ const hasBackgroundLocation = manifest.permissions.includes(
5193
+ "android.permission.ACCESS_BACKGROUND_LOCATION"
5194
+ );
5195
+ if (hasBackgroundLocation) {
5196
+ for (const service of manifest.services) {
5197
+ const fst = service.foregroundServiceType;
5198
+ if (!fst) continue;
5199
+ const num = Number(fst);
5200
+ const hasLocation = fst.split("|").some((t) => t.trim() === "location") || !isNaN(num) && (num & 8) !== 0;
5201
+ if (hasLocation) {
5202
+ findings.push({
5203
+ scanner: "manifest",
5204
+ ruleId: "geofencing-foreground-service",
5205
+ severity: "warning",
5206
+ title: `Possible geofencing via foreground service "${service.name}"`,
5207
+ 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.`,
5208
+ 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"].',
5209
+ policyUrl: "https://support.google.com/googleplay/android-developer/answer/16926792"
5210
+ });
5211
+ }
5212
+ }
5213
+ }
5192
5214
  const allComponents = [
5193
5215
  ...manifest.activities,
5194
5216
  ...manifest.services,
@@ -5406,6 +5428,34 @@ var permissionsScanner = {
5406
5428
  });
5407
5429
  }
5408
5430
  }
5431
+ const contactsPerms = [
5432
+ "android.permission.READ_CONTACTS",
5433
+ "android.permission.WRITE_CONTACTS"
5434
+ ].filter((p) => manifest.permissions.includes(p) && !allowed.has(p));
5435
+ if (contactsPerms.length > 0) {
5436
+ const names = contactsPerms.map((p) => p.split(".").pop()).join(", ");
5437
+ findings.push({
5438
+ scanner: "permissions",
5439
+ ruleId: "contacts-permission-broad",
5440
+ severity: "warning",
5441
+ title: "Broad contacts access requires migration to Contact Picker",
5442
+ message: `Your app declares ${names}. Google Play now requires the Android Contact Picker instead of broad contacts access. Compliance deadline: May 15, 2026.`,
5443
+ 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.",
5444
+ policyUrl: "https://support.google.com/googleplay/android-developer/answer/16926792"
5445
+ });
5446
+ }
5447
+ const broadHealthPerm = "android.permission.health.READ_ALL_HEALTH_DATA";
5448
+ if (manifest.permissions.includes(broadHealthPerm) && !allowed.has(broadHealthPerm)) {
5449
+ findings.push({
5450
+ scanner: "permissions",
5451
+ ruleId: "health-connect-granular",
5452
+ severity: manifest.targetSdk >= 36 ? "warning" : "info",
5453
+ title: "Broad Health Connect permission \u2014 use granular permissions",
5454
+ message: `Your app declares READ_ALL_HEALTH_DATA. Android 16 requires granular Health Connect permissions for individual data types (e.g., steps, heart rate, sleep).${manifest.targetSdk >= 36 ? " Your targetSdk >= 36 makes this a policy requirement." : " This will become required when you target API 36+."}`,
5455
+ suggestion: "Replace READ_ALL_HEALTH_DATA with individual permissions like health.READ_STEPS, health.READ_HEART_RATE, etc. Only request the data types your app actually uses.",
5456
+ policyUrl: "https://developer.android.com/health-and-fitness/guides/health-connect/plan/data-types"
5457
+ });
5458
+ }
5409
5459
  const dataPermissions = [
5410
5460
  { perm: "android.permission.ACCESS_FINE_LOCATION", data: "precise location" },
5411
5461
  { perm: "android.permission.ACCESS_COARSE_LOCATION", data: "approximate location" },
@@ -8530,6 +8580,323 @@ function formatNotification(notification) {
8530
8580
  }
8531
8581
  return { ...base, type: "unknown" };
8532
8582
  }
8583
+
8584
+ // src/signing.ts
8585
+ import { execFile as execFile3 } from "child_process";
8586
+ import { access as access2, constants } from "fs/promises";
8587
+ var SHA256_RE = /SHA-?256\s*:\s*([A-Fa-f0-9]{2}(?::[A-Fa-f0-9]{2}){31})/;
8588
+ var ALIAS_RE = /Alias name:\s*(.+)/i;
8589
+ function normalizeFingerprint(fp) {
8590
+ return fp.replace(/[:\s]/g, "").toLowerCase();
8591
+ }
8592
+ function parseKeytoolOutput(stdout) {
8593
+ const sha256Match = SHA256_RE.exec(stdout);
8594
+ if (!sha256Match?.[1]) {
8595
+ throw new GpcError(
8596
+ "Could not find SHA-256 fingerprint in keytool output",
8597
+ "KEYTOOL_PARSE_ERROR",
8598
+ 1,
8599
+ "Ensure the keystore contains a valid certificate entry"
8600
+ );
8601
+ }
8602
+ const aliasMatch = ALIAS_RE.exec(stdout);
8603
+ return {
8604
+ sha256: sha256Match[1].toUpperCase(),
8605
+ alias: aliasMatch?.[1]?.trim() ?? "unknown"
8606
+ };
8607
+ }
8608
+ async function getKeystoreFingerprint(keystorePath, storePassword, keyAlias) {
8609
+ await access2(keystorePath, constants.R_OK).catch(() => {
8610
+ throw new GpcError(
8611
+ `Keystore not found or not readable: ${keystorePath}`,
8612
+ "KEYSTORE_READ_ERROR",
8613
+ 1,
8614
+ "Check the path and file permissions"
8615
+ );
8616
+ });
8617
+ const args = ["-list", "-v", "-keystore", keystorePath, "-storepass", storePassword];
8618
+ if (keyAlias) args.push("-alias", keyAlias);
8619
+ const stdout = await new Promise((resolve2, reject) => {
8620
+ execFile3("keytool", args, { timeout: 1e4 }, (err, stdout2, stderr) => {
8621
+ if (err) {
8622
+ const msg = stderr || err.message;
8623
+ if (msg.includes("not found") || err.code === "ENOENT") {
8624
+ reject(
8625
+ new GpcError(
8626
+ "keytool not found",
8627
+ "KEYTOOL_NOT_FOUND",
8628
+ 1,
8629
+ "Install a JDK (keytool ships with it) or add it to your PATH"
8630
+ )
8631
+ );
8632
+ return;
8633
+ }
8634
+ if (msg.includes("password was incorrect") || msg.includes("Keystore was tampered")) {
8635
+ reject(
8636
+ new GpcError(
8637
+ "Keystore password is incorrect or the keystore is corrupted",
8638
+ "KEYSTORE_PASSWORD_ERROR",
8639
+ 1,
8640
+ "Check --store-pass or GPC_STORE_PASSWORD"
8641
+ )
8642
+ );
8643
+ return;
8644
+ }
8645
+ reject(
8646
+ new GpcError(
8647
+ `keytool failed: ${msg}`,
8648
+ "KEYTOOL_ERROR",
8649
+ 1,
8650
+ "Check the keystore path and password"
8651
+ )
8652
+ );
8653
+ return;
8654
+ }
8655
+ resolve2(stdout2);
8656
+ });
8657
+ });
8658
+ const { sha256, alias } = parseKeytoolOutput(stdout);
8659
+ return { sha256, alias, keystorePath };
8660
+ }
8661
+ async function getApiSigningFingerprint(accessToken, packageName, apiHost = "androidpublisher.googleapis.com") {
8662
+ const baseUrl = `https://${apiHost}/androidpublisher/v3/applications/${encodeURIComponent(packageName)}`;
8663
+ const editResp = await fetch(`${baseUrl}/edits`, {
8664
+ method: "POST",
8665
+ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
8666
+ body: JSON.stringify({}),
8667
+ signal: AbortSignal.timeout(1e4)
8668
+ });
8669
+ if (!editResp.ok) return null;
8670
+ const edit = await editResp.json();
8671
+ try {
8672
+ const bundlesResp = await fetch(`${baseUrl}/edits/${edit.id}/bundles`, {
8673
+ headers: { Authorization: `Bearer ${accessToken}` },
8674
+ signal: AbortSignal.timeout(1e4)
8675
+ });
8676
+ if (!bundlesResp.ok) return null;
8677
+ const bundlesData = await bundlesResp.json();
8678
+ const bundles = bundlesData.bundles ?? [];
8679
+ if (bundles.length === 0) return null;
8680
+ const latest = bundles.reduce((max, b) => b.versionCode > max.versionCode ? b : max);
8681
+ const apksResp = await fetch(`${baseUrl}/generatedApks/${latest.versionCode}`, {
8682
+ headers: { Authorization: `Bearer ${accessToken}` },
8683
+ signal: AbortSignal.timeout(1e4)
8684
+ });
8685
+ if (!apksResp.ok) return null;
8686
+ const apksData = await apksResp.json();
8687
+ const fp = apksData.generatedApks?.[0]?.certificateSha256Fingerprint;
8688
+ if (!fp) return null;
8689
+ return { sha256: fp.toUpperCase(), versionCode: latest.versionCode };
8690
+ } finally {
8691
+ await fetch(`${baseUrl}/edits/${edit.id}`, {
8692
+ method: "DELETE",
8693
+ headers: { Authorization: `Bearer ${accessToken}` },
8694
+ signal: AbortSignal.timeout(5e3)
8695
+ }).catch(() => {
8696
+ });
8697
+ }
8698
+ }
8699
+ function compareFingerprints(a, b) {
8700
+ return normalizeFingerprint(a) === normalizeFingerprint(b);
8701
+ }
8702
+
8703
+ // src/signing-consistency.ts
8704
+ async function checkSigningConsistency(accessToken, packageName, apiHost = "androidpublisher.googleapis.com") {
8705
+ const baseUrl = `https://${apiHost}/androidpublisher/v3/applications/${encodeURIComponent(packageName)}`;
8706
+ const editResp = await fetch(`${baseUrl}/edits`, {
8707
+ method: "POST",
8708
+ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
8709
+ body: JSON.stringify({}),
8710
+ signal: AbortSignal.timeout(1e4)
8711
+ });
8712
+ if (!editResp.ok) {
8713
+ const body = await editResp.json().catch(() => ({}));
8714
+ throw new GpcError(
8715
+ `Failed to create edit: ${body.error?.message ?? `HTTP ${editResp.status}`}`,
8716
+ "EDIT_CREATE_FAILED",
8717
+ 4,
8718
+ "Check your credentials and app access permissions"
8719
+ );
8720
+ }
8721
+ const edit = await editResp.json();
8722
+ try {
8723
+ const bundlesResp = await fetch(`${baseUrl}/edits/${edit.id}/bundles`, {
8724
+ headers: { Authorization: `Bearer ${accessToken}` },
8725
+ signal: AbortSignal.timeout(1e4)
8726
+ });
8727
+ if (!bundlesResp.ok) {
8728
+ throw new GpcError(
8729
+ `Failed to list bundles: HTTP ${bundlesResp.status}`,
8730
+ "BUNDLES_LIST_FAILED",
8731
+ 4,
8732
+ "Check your API permissions for this app"
8733
+ );
8734
+ }
8735
+ const bundlesData = await bundlesResp.json();
8736
+ const bundles = (bundlesData.bundles ?? []).sort((a, b) => b.versionCode - a.versionCode);
8737
+ if (bundles.length === 0) {
8738
+ throw new GpcError(
8739
+ "No bundles found for this app",
8740
+ "NO_BUNDLES",
8741
+ 4,
8742
+ "Upload at least one AAB with 'gpc publish' before checking signing consistency"
8743
+ );
8744
+ }
8745
+ const currentVc = bundles[0].versionCode;
8746
+ const currentFp = await fetchFingerprint(baseUrl, accessToken, currentVc);
8747
+ if (bundles.length === 1) {
8748
+ return {
8749
+ currentVersionCode: currentVc,
8750
+ currentFingerprint: currentFp,
8751
+ consistent: true,
8752
+ firstRelease: true
8753
+ };
8754
+ }
8755
+ const previousVc = bundles[1].versionCode;
8756
+ const previousFp = await fetchFingerprint(baseUrl, accessToken, previousVc);
8757
+ const consistent = normalizeFingerprint(currentFp) === normalizeFingerprint(previousFp);
8758
+ return {
8759
+ currentVersionCode: currentVc,
8760
+ currentFingerprint: currentFp,
8761
+ previousVersionCode: previousVc,
8762
+ previousFingerprint: previousFp,
8763
+ consistent,
8764
+ firstRelease: false
8765
+ };
8766
+ } finally {
8767
+ await fetch(`${baseUrl}/edits/${edit.id}`, {
8768
+ method: "DELETE",
8769
+ headers: { Authorization: `Bearer ${accessToken}` },
8770
+ signal: AbortSignal.timeout(5e3)
8771
+ }).catch(() => {
8772
+ });
8773
+ }
8774
+ }
8775
+ async function fetchFingerprint(baseUrl, accessToken, versionCode) {
8776
+ const resp = await fetch(`${baseUrl}/generatedApks/${versionCode}`, {
8777
+ headers: { Authorization: `Bearer ${accessToken}` },
8778
+ signal: AbortSignal.timeout(1e4)
8779
+ });
8780
+ if (!resp.ok) {
8781
+ throw new GpcError(
8782
+ `Failed to get generated APKs for versionCode ${versionCode}: HTTP ${resp.status}`,
8783
+ "GENERATED_APKS_FAILED",
8784
+ 4,
8785
+ "Check your API permissions for this app"
8786
+ );
8787
+ }
8788
+ const data = await resp.json();
8789
+ const fp = data.generatedApks?.[0]?.certificateSha256Fingerprint;
8790
+ if (!fp) {
8791
+ throw new GpcError(
8792
+ `No signing certificate found for versionCode ${versionCode}`,
8793
+ "NO_SIGNING_CERT",
8794
+ 4,
8795
+ "The app may not be enrolled in Play App Signing"
8796
+ );
8797
+ }
8798
+ return fp.toUpperCase();
8799
+ }
8800
+
8801
+ // src/verify-checklist.ts
8802
+ var PLAY_CONSOLE_SETTINGS = "https://play.google.com/console/developers/settings";
8803
+ var VERIFICATION_PAGE = "https://developer.android.com/developer-verification";
8804
+ var PLAY_APP_SIGNING = "https://support.google.com/googleplay/android-developer/answer/9842756";
8805
+ function buildChecklist(input) {
8806
+ const items = [];
8807
+ const answers = input.interactiveAnswers ?? {};
8808
+ items.push({
8809
+ id: "account-active",
8810
+ title: "Play Console account active",
8811
+ status: input.authenticated ? "done" : "action-needed",
8812
+ detail: input.authenticated ? `Authenticated as ${input.accountEmail ?? "unknown"}` : "Could not authenticate with Google Play",
8813
+ actionUrl: input.authenticated ? void 0 : "https://play.google.com/console"
8814
+ });
8815
+ items.push(
8816
+ resolveManualStep(
8817
+ "identity-verified",
8818
+ "Identity verification complete",
8819
+ "Confirm your identity is verified under Developer Account in Play Console Settings",
8820
+ PLAY_CONSOLE_SETTINGS,
8821
+ answers
8822
+ )
8823
+ );
8824
+ items.push(
8825
+ resolveManualStep(
8826
+ "auto-registration-reviewed",
8827
+ "Auto-registration results reviewed",
8828
+ "Check the registration status banner above your app list in Play Console",
8829
+ PLAY_CONSOLE_SETTINGS,
8830
+ answers
8831
+ )
8832
+ );
8833
+ if (input.appAccessible !== void 0) {
8834
+ items.push({
8835
+ id: "app-accessible",
8836
+ title: "App accessible via API",
8837
+ status: input.appAccessible ? "done" : "action-needed",
8838
+ detail: input.appAccessible ? "App is reachable through the Play Developer API" : "Could not access app via API"
8839
+ });
8840
+ }
8841
+ if (input.bundleCount !== void 0) {
8842
+ items.push({
8843
+ id: "bundle-uploaded",
8844
+ title: "At least one bundle uploaded",
8845
+ status: input.bundleCount > 0 ? "done" : "action-needed",
8846
+ detail: input.bundleCount > 0 ? `${input.bundleCount} bundle${input.bundleCount !== 1 ? "s" : ""} on file` : "No bundles found. Upload an AAB with: gpc publish"
8847
+ });
8848
+ }
8849
+ if (input.hasGeneratedApks !== void 0) {
8850
+ items.push({
8851
+ id: "play-app-signing",
8852
+ title: "Play App Signing enrolled",
8853
+ status: input.hasGeneratedApks ? "done" : "action-needed",
8854
+ detail: input.hasGeneratedApks ? "Google manages your app signing key" : "App may not be enrolled in Play App Signing",
8855
+ actionUrl: input.hasGeneratedApks ? void 0 : PLAY_APP_SIGNING
8856
+ });
8857
+ }
8858
+ items.push(
8859
+ resolveManualStep(
8860
+ "additional-keys",
8861
+ "Additional signing keys registered",
8862
+ "If you distribute outside Play with a different key, register it in Play Console",
8863
+ VERIFICATION_PAGE,
8864
+ answers
8865
+ )
8866
+ );
8867
+ const completed = items.filter((i) => i.status === "done").length;
8868
+ return { items, completed, total: items.length };
8869
+ }
8870
+ function resolveManualStep(id, title, detail, actionUrl, answers) {
8871
+ if (id in answers) {
8872
+ return {
8873
+ id,
8874
+ title,
8875
+ status: answers[id] ? "done" : "action-needed",
8876
+ detail,
8877
+ actionUrl: answers[id] ? void 0 : actionUrl
8878
+ };
8879
+ }
8880
+ return { id, title, status: "cannot-detect", detail, actionUrl };
8881
+ }
8882
+ function renderChecklistMarkdown(result, accountEmail) {
8883
+ const lines = [
8884
+ "# Developer Verification Checklist",
8885
+ "",
8886
+ `Account: ${accountEmail}`,
8887
+ `Date: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
8888
+ `Progress: ${result.completed}/${result.total}`,
8889
+ ""
8890
+ ];
8891
+ for (const item of result.items) {
8892
+ const check = item.status === "done" ? "x" : " ";
8893
+ lines.push(`- [${check}] ${item.title}`);
8894
+ if (item.detail) lines.push(` ${item.detail}`);
8895
+ if (item.actionUrl && item.status !== "done") lines.push(` ${item.actionUrl}`);
8896
+ }
8897
+ lines.push("");
8898
+ return lines.join("\n");
8899
+ }
8533
8900
  export {
8534
8901
  ApiError,
8535
8902
  ConfigError,
@@ -8562,16 +8929,19 @@ export {
8562
8929
  applyReleaseNotes,
8563
8930
  batchGetOrders,
8564
8931
  batchSyncInAppProducts,
8932
+ buildChecklist,
8565
8933
  buildLocaleBundle,
8566
8934
  bundleToReleaseNotes,
8567
8935
  cancelRecoveryAction,
8568
8936
  cancelSubscriptionPurchase,
8569
8937
  cancelSubscriptionV2,
8570
8938
  checkBundleSize,
8939
+ checkSigningConsistency,
8571
8940
  checkThreshold,
8572
8941
  classifyError,
8573
8942
  clearAuditLog,
8574
8943
  compareBundles,
8944
+ compareFingerprints,
8575
8945
  compareVersionVitals,
8576
8946
  compareVitalsTrend,
8577
8947
  computeStatusDiff,
@@ -8639,12 +9009,14 @@ export {
8639
9009
  generateMigrationPlan,
8640
9010
  generateNotesFromGit,
8641
9011
  getAllScannerNames,
9012
+ getApiSigningFingerprint,
8642
9013
  getAppInfo,
8643
9014
  getAppStatus,
8644
9015
  getCountryAvailability,
8645
9016
  getDeviceTier,
8646
9017
  getExternalTransaction,
8647
9018
  getInAppProduct,
9019
+ getKeystoreFingerprint,
8648
9020
  getListings,
8649
9021
  getOffer,
8650
9022
  getOneTimeOffer,
@@ -8708,10 +9080,12 @@ export {
8708
9080
  loadStatusCache,
8709
9081
  maybePaginate,
8710
9082
  migratePrices,
9083
+ normalizeFingerprint,
8711
9084
  parseAppfile,
8712
9085
  parseCommit,
8713
9086
  parseFastfile,
8714
9087
  parseGrantArg,
9088
+ parseKeytoolOutput,
8715
9089
  parseMonth,
8716
9090
  parseRemoteUrl,
8717
9091
  pauseTrain,
@@ -8729,6 +9103,7 @@ export {
8729
9103
  relativeTime,
8730
9104
  removeTesters,
8731
9105
  removeUser,
9106
+ renderChecklistMarkdown,
8732
9107
  renderJson,
8733
9108
  renderMarkdown,
8734
9109
  renderPlayStore,