@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 +16 -16
- package/dist/index.d.ts +57 -1
- package/dist/index.js +375 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
-
|
|
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,
|