pabal-store-api-mcp 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/bin/pabal-mcp.js +6 -0
- package/dist/src/core/clients/app-store-factory.d.ts +29 -0
- package/dist/src/core/clients/app-store-factory.js +72 -0
- package/dist/src/core/clients/client-factory-helpers.d.ts +7 -0
- package/dist/src/core/clients/client-factory-helpers.js +10 -0
- package/dist/src/core/clients/google-play-factory.d.ts +29 -0
- package/dist/src/core/clients/google-play-factory.js +72 -0
- package/dist/src/core/clients/types.d.ts +8 -0
- package/dist/src/core/clients/types.js +1 -0
- package/dist/src/core/helpers/formatters.d.ts +3 -0
- package/dist/src/core/helpers/formatters.js +38 -0
- package/dist/src/core/helpers/registration.d.ts +21 -0
- package/dist/src/core/helpers/registration.js +21 -0
- package/dist/src/core/helpers/translate-release-notes.d.ts +46 -0
- package/dist/src/core/helpers/translate-release-notes.js +87 -0
- package/dist/src/core/services/app-resolution-service.d.ts +14 -0
- package/dist/src/core/services/app-resolution-service.js +35 -0
- package/dist/src/core/services/app-store-service.d.ts +41 -0
- package/dist/src/core/services/app-store-service.js +266 -0
- package/dist/src/core/services/google-play-service.d.ts +36 -0
- package/dist/src/core/services/google-play-service.js +203 -0
- package/dist/src/core/services/service-helpers.d.ts +15 -0
- package/dist/src/core/services/service-helpers.js +31 -0
- package/dist/src/core/services/types.d.ts +81 -0
- package/dist/src/core/services/types.js +1 -0
- package/dist/src/core/workflows/version-info.d.ts +29 -0
- package/dist/src/core/workflows/version-info.js +100 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +279 -0
- package/dist/src/packages/common/errors/app-error.d.ts +39 -0
- package/dist/src/packages/common/errors/app-error.js +134 -0
- package/dist/src/packages/common/errors/error-codes.d.ts +63 -0
- package/dist/src/packages/common/errors/error-codes.js +71 -0
- package/dist/src/packages/common/errors/status-codes.d.ts +10 -0
- package/dist/src/packages/common/errors/status-codes.js +9 -0
- package/dist/src/packages/configs/aso-config/constants.d.ts +14 -0
- package/dist/src/packages/configs/aso-config/constants.js +102 -0
- package/dist/src/packages/configs/aso-config/locale-guards.d.ts +3 -0
- package/dist/src/packages/configs/aso-config/locale-guards.js +7 -0
- package/dist/src/packages/configs/aso-config/store.d.ts +11 -0
- package/dist/src/packages/configs/aso-config/store.js +11 -0
- package/dist/src/packages/configs/aso-config/types.d.ts +98 -0
- package/dist/src/packages/configs/aso-config/types.js +2 -0
- package/dist/src/packages/configs/aso-config/utils.d.ts +43 -0
- package/dist/src/packages/configs/aso-config/utils.js +223 -0
- package/dist/src/packages/configs/secrets-config/config.d.ts +12 -0
- package/dist/src/packages/configs/secrets-config/config.js +187 -0
- package/dist/src/packages/configs/secrets-config/constants.d.ts +1 -0
- package/dist/src/packages/configs/secrets-config/constants.js +1 -0
- package/dist/src/packages/configs/secrets-config/errors.d.ts +9 -0
- package/dist/src/packages/configs/secrets-config/errors.js +15 -0
- package/dist/src/packages/configs/secrets-config/registered-apps.d.ts +52 -0
- package/dist/src/packages/configs/secrets-config/registered-apps.js +108 -0
- package/dist/src/packages/configs/secrets-config/schemas.d.ts +21 -0
- package/dist/src/packages/configs/secrets-config/schemas.js +9 -0
- package/dist/src/packages/configs/secrets-config/types.d.ts +8 -0
- package/dist/src/packages/configs/secrets-config/types.js +1 -0
- package/dist/src/packages/stores/app-store/api-converters.d.ts +26 -0
- package/dist/src/packages/stores/app-store/api-converters.js +131 -0
- package/dist/src/packages/stores/app-store/api-endpoints.d.ts +33 -0
- package/dist/src/packages/stores/app-store/api-endpoints.js +157 -0
- package/dist/src/packages/stores/app-store/auth.d.ts +12 -0
- package/dist/src/packages/stores/app-store/auth.js +36 -0
- package/dist/src/packages/stores/app-store/client.d.ts +78 -0
- package/dist/src/packages/stores/app-store/client.js +637 -0
- package/dist/src/packages/stores/app-store/constants.d.ts +11 -0
- package/dist/src/packages/stores/app-store/constants.js +38 -0
- package/dist/src/packages/stores/app-store/generated-types.d.ts +118537 -0
- package/dist/src/packages/stores/app-store/generated-types.js +5 -0
- package/dist/src/packages/stores/app-store/types.d.ts +39 -0
- package/dist/src/packages/stores/app-store/types.js +9 -0
- package/dist/src/packages/stores/app-store/verify-auth.d.ts +16 -0
- package/dist/src/packages/stores/app-store/verify-auth.js +34 -0
- package/dist/src/packages/stores/play-store/api-converters.d.ts +58 -0
- package/dist/src/packages/stores/play-store/api-converters.js +209 -0
- package/dist/src/packages/stores/play-store/api-endpoints.d.ts +68 -0
- package/dist/src/packages/stores/play-store/api-endpoints.js +145 -0
- package/dist/src/packages/stores/play-store/client.d.ts +55 -0
- package/dist/src/packages/stores/play-store/client.js +628 -0
- package/dist/src/packages/stores/play-store/constants.d.ts +10 -0
- package/dist/src/packages/stores/play-store/constants.js +17 -0
- package/dist/src/packages/stores/play-store/types.d.ts +146 -0
- package/dist/src/packages/stores/play-store/types.js +9 -0
- package/dist/src/packages/stores/play-store/verify-auth.d.ts +13 -0
- package/dist/src/packages/stores/play-store/verify-auth.js +31 -0
- package/dist/src/tools/apps/add.d.ts +28 -0
- package/dist/src/tools/apps/add.js +307 -0
- package/dist/src/tools/apps/init.d.ts +58 -0
- package/dist/src/tools/apps/init.js +390 -0
- package/dist/src/tools/apps/search.d.ts +33 -0
- package/dist/src/tools/apps/search.js +147 -0
- package/dist/src/tools/aso/pull.d.ts +22 -0
- package/dist/src/tools/aso/pull.js +264 -0
- package/dist/src/tools/aso/push.d.ts +23 -0
- package/dist/src/tools/aso/push.js +189 -0
- package/dist/src/tools/auth/app-store.d.ts +9 -0
- package/dist/src/tools/auth/app-store.js +34 -0
- package/dist/src/tools/auth/check.d.ts +14 -0
- package/dist/src/tools/auth/check.js +50 -0
- package/dist/src/tools/auth/play-store.d.ts +9 -0
- package/dist/src/tools/auth/play-store.js +30 -0
- package/dist/src/tools/release/check-versions.d.ts +14 -0
- package/dist/src/tools/release/check-versions.js +65 -0
- package/dist/src/tools/release/create.d.ts +23 -0
- package/dist/src/tools/release/create.js +128 -0
- package/dist/src/tools/release/pull-notes.d.ts +22 -0
- package/dist/src/tools/release/pull-notes.js +151 -0
- package/dist/src/tools/release/update-notes.d.ts +110 -0
- package/dist/src/tools/release/update-notes.js +537 -0
- package/package.json +71 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { ERROR_CODES } from "../../../packages/common/errors/error-codes.js";
|
|
5
|
+
import { HTTP_STATUS } from "../../../packages/common/errors/status-codes.js";
|
|
6
|
+
import { ConfigError } from "./errors.js";
|
|
7
|
+
import { DATA_DIR_ENV_KEY } from "./constants.js";
|
|
8
|
+
import { appStoreSchema, playStoreSchema } from "./schemas.js";
|
|
9
|
+
// Config paths
|
|
10
|
+
const CONFIG_DIR = join(homedir(), ".config", "pabal-store-api-mcp");
|
|
11
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
12
|
+
// Security: recommended permissions
|
|
13
|
+
const RECOMMENDED_DIR_MODE = 0o700; // drwx------
|
|
14
|
+
const RECOMMENDED_FILE_MODE = 0o600; // -rw-------
|
|
15
|
+
/**
|
|
16
|
+
* Check file/directory permissions and warn if too permissive
|
|
17
|
+
*/
|
|
18
|
+
function checkPermissions(path, isDirectory) {
|
|
19
|
+
try {
|
|
20
|
+
const stats = statSync(path);
|
|
21
|
+
const mode = stats.mode & 0o777;
|
|
22
|
+
const recommended = isDirectory
|
|
23
|
+
? RECOMMENDED_DIR_MODE
|
|
24
|
+
: RECOMMENDED_FILE_MODE;
|
|
25
|
+
// Check if "group" or "others" have any permissions
|
|
26
|
+
const groupOthersPerms = mode & 0o077;
|
|
27
|
+
if (groupOthersPerms !== 0) {
|
|
28
|
+
const typeLabel = isDirectory ? "directory" : "file";
|
|
29
|
+
const recommendedStr = recommended.toString(8);
|
|
30
|
+
console.error(`[Security] ⚠️ Warning: ${typeLabel} "${path}" has permissive permissions (${mode.toString(8)})`);
|
|
31
|
+
console.error(`[Security] Run: chmod ${recommendedStr} "${path}"`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Ignore permission check errors
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check all config files for secure permissions
|
|
40
|
+
*/
|
|
41
|
+
function checkConfigSecurity(configDir) {
|
|
42
|
+
// Check directory permissions
|
|
43
|
+
checkPermissions(configDir, true);
|
|
44
|
+
// Check all files in config directory
|
|
45
|
+
const sensitiveFiles = [
|
|
46
|
+
"config.json",
|
|
47
|
+
"app-store-key.p8",
|
|
48
|
+
"google-play-service-account.json",
|
|
49
|
+
"registered-apps.json",
|
|
50
|
+
];
|
|
51
|
+
for (const file of sensitiveFiles) {
|
|
52
|
+
const filePath = join(configDir, file);
|
|
53
|
+
if (existsSync(filePath)) {
|
|
54
|
+
checkPermissions(filePath, false);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get config directory path: ~/.config/pabal-mcp/
|
|
60
|
+
*/
|
|
61
|
+
export function getConfigDir() {
|
|
62
|
+
return CONFIG_DIR;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get config file path: ~/.config/pabal-mcp/config.json
|
|
66
|
+
*/
|
|
67
|
+
export function getConfigPath() {
|
|
68
|
+
return CONFIG_PATH;
|
|
69
|
+
}
|
|
70
|
+
export function readConfigFile() {
|
|
71
|
+
const configPath = getConfigPath();
|
|
72
|
+
if (!existsSync(configPath)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const configContent = readFileSync(configPath, "utf-8");
|
|
77
|
+
return JSON.parse(configContent);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error(`[Config] ⚠️ Failed to read config file: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function getDataDir() {
|
|
85
|
+
const configDir = getConfigDir();
|
|
86
|
+
console.error(`[Config] 🔍 Checking data directory config...`);
|
|
87
|
+
// 1. Check config file first
|
|
88
|
+
const config = readConfigFile();
|
|
89
|
+
if (config?.dataDir && typeof config.dataDir === "string") {
|
|
90
|
+
const normalized = config.dataDir.trim();
|
|
91
|
+
if (normalized) {
|
|
92
|
+
const resultDir = isAbsolute(normalized)
|
|
93
|
+
? normalized
|
|
94
|
+
: resolve(configDir, normalized);
|
|
95
|
+
console.error(`[Config] ✅ Using config file dataDir: ${resultDir}`);
|
|
96
|
+
return resultDir;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// 2. Check environment variable
|
|
100
|
+
const override = process.env[DATA_DIR_ENV_KEY];
|
|
101
|
+
if (override && override.trim()) {
|
|
102
|
+
const normalized = override.trim();
|
|
103
|
+
const resultDir = isAbsolute(normalized)
|
|
104
|
+
? normalized
|
|
105
|
+
: resolve(configDir, normalized);
|
|
106
|
+
console.error(`[Config] ✅ Using environment variable: ${resultDir}`);
|
|
107
|
+
return resultDir;
|
|
108
|
+
}
|
|
109
|
+
// 3. Default to config directory
|
|
110
|
+
console.error(`[Config] ✅ Using default (config dir): ${configDir}`);
|
|
111
|
+
return configDir;
|
|
112
|
+
}
|
|
113
|
+
export function loadConfig() {
|
|
114
|
+
const configDir = getConfigDir();
|
|
115
|
+
// Security check: warn if permissions are too permissive
|
|
116
|
+
checkConfigSecurity(configDir);
|
|
117
|
+
const config = readConfigFile();
|
|
118
|
+
if (!config) {
|
|
119
|
+
const configPath = getConfigPath();
|
|
120
|
+
throw new ConfigError(`Config file not found: ${configPath}\n` +
|
|
121
|
+
`Please create ~/.config/pabal-store-api-mcp/config.json`, { status: HTTP_STATUS.NOT_FOUND, code: ERROR_CODES.CONFIG_NOT_FOUND });
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const result = {};
|
|
125
|
+
if (config.appStore) {
|
|
126
|
+
const { issuerId, keyId, privateKeyPath } = config.appStore;
|
|
127
|
+
if (issuerId && keyId && privateKeyPath) {
|
|
128
|
+
const keyPath = isAbsolute(privateKeyPath)
|
|
129
|
+
? privateKeyPath
|
|
130
|
+
: resolve(configDir, privateKeyPath);
|
|
131
|
+
const privateKey = readFileSafe(keyPath);
|
|
132
|
+
if (privateKey) {
|
|
133
|
+
result.appStore = appStoreSchema.parse({
|
|
134
|
+
keyId,
|
|
135
|
+
issuerId,
|
|
136
|
+
privateKey: normalizePrivateKey(privateKey),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (config.googlePlay?.serviceAccountKeyPath) {
|
|
142
|
+
const serviceAccountPath = config.googlePlay.serviceAccountKeyPath;
|
|
143
|
+
const jsonPath = isAbsolute(serviceAccountPath)
|
|
144
|
+
? serviceAccountPath
|
|
145
|
+
: resolve(configDir, serviceAccountPath);
|
|
146
|
+
const json = readFileSafe(jsonPath);
|
|
147
|
+
if (json) {
|
|
148
|
+
result.playStore = playStoreSchema.parse({
|
|
149
|
+
serviceAccountJson: json,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!result.appStore && !result.playStore) {
|
|
154
|
+
throw new ConfigError("Config file does not contain App Store or Play Store authentication information.", {
|
|
155
|
+
status: HTTP_STATUS.UNAUTHORIZED,
|
|
156
|
+
code: ERROR_CODES.CONFIG_AUTH_NOT_FOUND,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error instanceof ConfigError) {
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
throw new ConfigError(`Error reading config file: ${error instanceof Error ? error.message : String(error)}`, {
|
|
166
|
+
status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
167
|
+
code: ERROR_CODES.CONFIG_READ_FAILED,
|
|
168
|
+
cause: error,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function readFileSafe(path) {
|
|
173
|
+
try {
|
|
174
|
+
return readFileSync(path, "utf-8");
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Restore line breaks in PEM key
|
|
181
|
+
function normalizePrivateKey(raw) {
|
|
182
|
+
if (raw.includes("-----BEGIN")) {
|
|
183
|
+
return raw;
|
|
184
|
+
}
|
|
185
|
+
const restored = raw.replace(/\\n/g, "\n");
|
|
186
|
+
return restored;
|
|
187
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DATA_DIR_ENV_KEY = "PABAL_MCP_DATA_DIR";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DATA_DIR_ENV_KEY = "PABAL_MCP_DATA_DIR";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AppError } from "../../../packages/common/errors/app-error.js";
|
|
2
|
+
import { ERROR_CODES } from "../../../packages/common/errors/error-codes.js";
|
|
3
|
+
import { HTTP_STATUS } from "../../../packages/common/errors/status-codes.js";
|
|
4
|
+
export class ConfigError extends AppError {
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super({
|
|
7
|
+
status: options?.status ?? HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
8
|
+
code: options?.code ?? ERROR_CODES.CONFIG_ERROR,
|
|
9
|
+
message,
|
|
10
|
+
details: options?.details,
|
|
11
|
+
cause: options?.cause,
|
|
12
|
+
});
|
|
13
|
+
this.name = "ConfigError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registered app management
|
|
3
|
+
* Manage app list through ~/.config/pabal-mcp/registered-apps.json file
|
|
4
|
+
*/
|
|
5
|
+
export interface RegisteredAppStoreInfo {
|
|
6
|
+
bundleId: string;
|
|
7
|
+
appId?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
supportedLocales?: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface RegisteredGooglePlayInfo {
|
|
12
|
+
packageName: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
supportedLocales?: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface RegisteredApp {
|
|
17
|
+
/** App identifier (user-defined, must be unique) */
|
|
18
|
+
slug: string;
|
|
19
|
+
/** Display name */
|
|
20
|
+
name: string;
|
|
21
|
+
/** App Store information */
|
|
22
|
+
appStore?: RegisteredAppStoreInfo;
|
|
23
|
+
/** Google Play information */
|
|
24
|
+
googlePlay?: RegisteredGooglePlayInfo;
|
|
25
|
+
}
|
|
26
|
+
export interface RegisteredAppsConfig {
|
|
27
|
+
apps: RegisteredApp[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load registered app list
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadRegisteredApps(): RegisteredAppsConfig;
|
|
33
|
+
/**
|
|
34
|
+
* Save registered app list
|
|
35
|
+
*/
|
|
36
|
+
export declare function saveRegisteredApps(config: RegisteredAppsConfig): void;
|
|
37
|
+
/**
|
|
38
|
+
* Register app
|
|
39
|
+
*/
|
|
40
|
+
export declare function registerApp(app: RegisteredApp): RegisteredApp;
|
|
41
|
+
/**
|
|
42
|
+
* Find app (search by slug, bundleId, packageName)
|
|
43
|
+
*/
|
|
44
|
+
export declare function findApp(identifier: string): RegisteredApp | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Update supported locales for an app
|
|
47
|
+
*/
|
|
48
|
+
export declare function updateAppSupportedLocales({ identifier, store, locales, }: {
|
|
49
|
+
identifier: string;
|
|
50
|
+
store: "appStore" | "googlePlay";
|
|
51
|
+
locales: string[];
|
|
52
|
+
}): boolean;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registered app management
|
|
3
|
+
* Manage app list through ~/.config/pabal-mcp/registered-apps.json file
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { AppError } from "../../../packages/common/errors/app-error.js";
|
|
8
|
+
import { ERROR_CODES } from "../../../packages/common/errors/error-codes.js";
|
|
9
|
+
import { getConfigDir } from "../../../packages/configs/secrets-config/config.js";
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// File Paths
|
|
12
|
+
// ============================================================================
|
|
13
|
+
function getRegisteredAppsPath() {
|
|
14
|
+
return join(getConfigDir(), "registered-apps.json");
|
|
15
|
+
}
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// CRUD Functions
|
|
18
|
+
// ============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Load registered app list
|
|
21
|
+
*/
|
|
22
|
+
export function loadRegisteredApps() {
|
|
23
|
+
const filePath = getRegisteredAppsPath();
|
|
24
|
+
console.error(`[RegisteredApps] 🔍 Loading registered apps...`);
|
|
25
|
+
console.error(`[RegisteredApps] File path: ${filePath}`);
|
|
26
|
+
console.error(`[RegisteredApps] File exists: ${existsSync(filePath)}`);
|
|
27
|
+
if (!existsSync(filePath)) {
|
|
28
|
+
console.error(`[RegisteredApps] ⚠️ File not found, returning empty list`);
|
|
29
|
+
return { apps: [] };
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(filePath, "utf-8");
|
|
33
|
+
const config = JSON.parse(content);
|
|
34
|
+
console.error(`[RegisteredApps] ✅ Loaded ${config.apps.length} apps: ${config.apps.map((a) => a.slug).join(", ")}`);
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw AppError.validation(ERROR_CODES.REGISTERED_APPS_READ_FAILED, `Failed to load registered apps: ${error instanceof Error ? error.message : String(error)}`, { filePath });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Save registered app list
|
|
43
|
+
*/
|
|
44
|
+
export function saveRegisteredApps(config) {
|
|
45
|
+
const filePath = getRegisteredAppsPath();
|
|
46
|
+
try {
|
|
47
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2));
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw AppError.io(ERROR_CODES.REGISTERED_APPS_WRITE_FAILED, `Failed to save registered apps: ${error instanceof Error ? error.message : String(error)}`, { filePath });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Register app
|
|
55
|
+
*/
|
|
56
|
+
export function registerApp(app) {
|
|
57
|
+
const config = loadRegisteredApps();
|
|
58
|
+
// Check for duplicates
|
|
59
|
+
const existing = config.apps.find((a) => a.slug === app.slug);
|
|
60
|
+
if (existing) {
|
|
61
|
+
throw AppError.conflict(ERROR_CODES.REGISTERED_APP_DUPLICATE, `App with slug "${app.slug}" already exists`);
|
|
62
|
+
}
|
|
63
|
+
config.apps.push(app);
|
|
64
|
+
saveRegisteredApps(config);
|
|
65
|
+
return app;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Find app (search by slug, bundleId, packageName)
|
|
69
|
+
*/
|
|
70
|
+
export function findApp(identifier) {
|
|
71
|
+
const config = loadRegisteredApps();
|
|
72
|
+
return config.apps.find((app) => app.slug === identifier ||
|
|
73
|
+
app.appStore?.bundleId === identifier ||
|
|
74
|
+
app.googlePlay?.packageName === identifier);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Update supported locales for an app
|
|
78
|
+
*/
|
|
79
|
+
export function updateAppSupportedLocales({ identifier, store, locales, }) {
|
|
80
|
+
const config = loadRegisteredApps();
|
|
81
|
+
const appIndex = config.apps.findIndex((app) => app.slug === identifier ||
|
|
82
|
+
app.appStore?.bundleId === identifier ||
|
|
83
|
+
app.googlePlay?.packageName === identifier);
|
|
84
|
+
if (appIndex === -1) {
|
|
85
|
+
throw AppError.notFound(ERROR_CODES.REGISTERED_APP_NOT_FOUND, `App "${identifier}" not found in registered apps`);
|
|
86
|
+
}
|
|
87
|
+
const app = config.apps[appIndex];
|
|
88
|
+
// Merge and deduplicate locales
|
|
89
|
+
const existingLocales = store === "appStore"
|
|
90
|
+
? app.appStore?.supportedLocales || []
|
|
91
|
+
: app.googlePlay?.supportedLocales || [];
|
|
92
|
+
const mergedLocales = Array.from(new Set([...existingLocales, ...locales])).sort();
|
|
93
|
+
// Update the app
|
|
94
|
+
if (store === "appStore") {
|
|
95
|
+
if (!app.appStore) {
|
|
96
|
+
throw AppError.validation(ERROR_CODES.REGISTERED_APP_STORE_INFO_MISSING, `App "${identifier}" is missing App Store info`);
|
|
97
|
+
}
|
|
98
|
+
app.appStore.supportedLocales = mergedLocales;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
if (!app.googlePlay) {
|
|
102
|
+
throw AppError.validation(ERROR_CODES.REGISTERED_APP_STORE_INFO_MISSING, `App "${identifier}" is missing Google Play info`);
|
|
103
|
+
}
|
|
104
|
+
app.googlePlay.supportedLocales = mergedLocales;
|
|
105
|
+
}
|
|
106
|
+
saveRegisteredApps(config);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const appStoreSchema: z.ZodObject<{
|
|
3
|
+
keyId: z.ZodString;
|
|
4
|
+
issuerId: z.ZodString;
|
|
5
|
+
privateKey: z.ZodString;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
keyId: string;
|
|
8
|
+
issuerId: string;
|
|
9
|
+
privateKey: string;
|
|
10
|
+
}, {
|
|
11
|
+
keyId: string;
|
|
12
|
+
issuerId: string;
|
|
13
|
+
privateKey: string;
|
|
14
|
+
}>;
|
|
15
|
+
export declare const playStoreSchema: z.ZodObject<{
|
|
16
|
+
serviceAccountJson: z.ZodString;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
serviceAccountJson: string;
|
|
19
|
+
}, {
|
|
20
|
+
serviceAccountJson: string;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const appStoreSchema = z.object({
|
|
3
|
+
keyId: z.string().min(1),
|
|
4
|
+
issuerId: z.string().min(1),
|
|
5
|
+
privateKey: z.string().min(1), // PEM format string
|
|
6
|
+
});
|
|
7
|
+
export const playStoreSchema = z.object({
|
|
8
|
+
serviceAccountJson: z.string().min(1), // JSON string
|
|
9
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { appStoreSchema, playStoreSchema } from "./schemas.js";
|
|
3
|
+
export type AppStoreConfig = z.infer<typeof appStoreSchema>;
|
|
4
|
+
export type PlayStoreConfig = z.infer<typeof playStoreSchema>;
|
|
5
|
+
export type EnvConfig = {
|
|
6
|
+
appStore?: AppStoreConfig;
|
|
7
|
+
playStore?: PlayStoreConfig;
|
|
8
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store Connect API Data Converters
|
|
3
|
+
*
|
|
4
|
+
* Data transformation logic between API responses and internal types
|
|
5
|
+
*/
|
|
6
|
+
import type { AppStoreAsoData, AppStoreMultilingualAsoData, AppStoreReleaseNote, AppStoreScreenshots } from "../../../packages/configs/aso-config/types.js";
|
|
7
|
+
import type { ApiResponse, AppInfoLocalization, AppStoreApp, AppStoreLocalization, AppStoreScreenshot, AppStoreScreenshotSet, AppStoreVersion } from "./types.js";
|
|
8
|
+
export declare function sortVersions(versions: AppStoreVersion[]): AppStoreVersion[];
|
|
9
|
+
export declare function selectEnglishAppName(localizations: AppInfoLocalization[]): string | null;
|
|
10
|
+
export declare function mapLocalizationsByLocale<T extends {
|
|
11
|
+
attributes?: {
|
|
12
|
+
locale?: string;
|
|
13
|
+
};
|
|
14
|
+
}>(localizations: T[]): Record<string, T>;
|
|
15
|
+
export declare function fetchScreenshotsForLocalization(localizationId: string | undefined, listScreenshotSets: (localizationId: string) => Promise<ApiResponse<AppStoreScreenshotSet[]>>, listScreenshots: (screenshotSetId: string) => Promise<ApiResponse<AppStoreScreenshot[]>>): Promise<AppStoreScreenshots>;
|
|
16
|
+
export declare function convertToAsoData(params: {
|
|
17
|
+
app: AppStoreApp;
|
|
18
|
+
appInfoLocalization?: AppInfoLocalization | null;
|
|
19
|
+
localization?: AppStoreLocalization | null;
|
|
20
|
+
screenshots: AppStoreScreenshots;
|
|
21
|
+
locale: string;
|
|
22
|
+
bundleId: string;
|
|
23
|
+
}): AppStoreAsoData;
|
|
24
|
+
export declare function convertToMultilingualAsoData(locales: Record<string, AppStoreAsoData>, defaultLocale?: string): AppStoreMultilingualAsoData;
|
|
25
|
+
export declare function convertToReleaseNote(version: AppStoreVersion, localizations: AppStoreLocalization[]): AppStoreReleaseNote | null;
|
|
26
|
+
export declare function sortReleaseNotes(releaseNotes: AppStoreReleaseNote[]): AppStoreReleaseNote[];
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store Connect API Data Converters
|
|
3
|
+
*
|
|
4
|
+
* Data transformation logic between API responses and internal types
|
|
5
|
+
*/
|
|
6
|
+
import { DEFAULT_LOCALE } from "../../../packages/configs/aso-config/constants.js";
|
|
7
|
+
import { SCREENSHOT_TYPE_MAP } from "./constants.js";
|
|
8
|
+
export function sortVersions(versions) {
|
|
9
|
+
return versions.sort((a, b) => {
|
|
10
|
+
const vA = (a.attributes?.versionString ?? "0").split(".").map(Number);
|
|
11
|
+
const vB = (b.attributes?.versionString ?? "0").split(".").map(Number);
|
|
12
|
+
for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
|
|
13
|
+
const diff = (vB[i] || 0) - (vA[i] || 0);
|
|
14
|
+
if (diff !== 0)
|
|
15
|
+
return diff;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function selectEnglishAppName(localizations) {
|
|
21
|
+
const enUS = localizations.find((l) => l.attributes?.locale === "en-US");
|
|
22
|
+
if (enUS?.attributes?.name)
|
|
23
|
+
return enUS.attributes.name;
|
|
24
|
+
const enGB = localizations.find((l) => l.attributes?.locale === "en-GB");
|
|
25
|
+
if (enGB?.attributes?.name)
|
|
26
|
+
return enGB.attributes.name;
|
|
27
|
+
const enAny = localizations.find((l) => l.attributes?.locale?.startsWith("en"));
|
|
28
|
+
if (enAny?.attributes?.name)
|
|
29
|
+
return enAny.attributes.name;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
export function mapLocalizationsByLocale(localizations) {
|
|
33
|
+
return localizations.reduce((acc, loc) => {
|
|
34
|
+
const locale = loc.attributes?.locale;
|
|
35
|
+
if (locale)
|
|
36
|
+
acc[locale] = loc;
|
|
37
|
+
return acc;
|
|
38
|
+
}, {});
|
|
39
|
+
}
|
|
40
|
+
export async function fetchScreenshotsForLocalization(localizationId, listScreenshotSets, listScreenshots) {
|
|
41
|
+
const screenshots = {};
|
|
42
|
+
if (!localizationId)
|
|
43
|
+
return screenshots;
|
|
44
|
+
const setsResponse = await listScreenshotSets(localizationId);
|
|
45
|
+
for (const set of setsResponse.data || []) {
|
|
46
|
+
const screenshotsResponse = await listScreenshots(set.id);
|
|
47
|
+
const urls = (screenshotsResponse.data || [])
|
|
48
|
+
.map((s) => s.attributes?.imageAsset?.templateUrl)
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
if (urls.length > 0) {
|
|
51
|
+
const displayType = set.attributes?.screenshotDisplayType;
|
|
52
|
+
if (displayType) {
|
|
53
|
+
const mappedType = SCREENSHOT_TYPE_MAP[displayType];
|
|
54
|
+
if (mappedType)
|
|
55
|
+
screenshots[mappedType] = urls;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return screenshots;
|
|
60
|
+
}
|
|
61
|
+
export function convertToAsoData(params) {
|
|
62
|
+
const { app, appInfoLocalization, localization, screenshots, locale, bundleId, } = params;
|
|
63
|
+
return {
|
|
64
|
+
name: appInfoLocalization?.attributes?.name ||
|
|
65
|
+
app.attributes?.name ||
|
|
66
|
+
"Unknown",
|
|
67
|
+
subtitle: appInfoLocalization?.attributes?.subtitle,
|
|
68
|
+
description: localization?.attributes?.description || "",
|
|
69
|
+
keywords: localization?.attributes?.keywords,
|
|
70
|
+
promotionalText: localization?.attributes?.promotionalText,
|
|
71
|
+
screenshots,
|
|
72
|
+
bundleId,
|
|
73
|
+
locale,
|
|
74
|
+
supportUrl: localization?.attributes?.supportUrl,
|
|
75
|
+
marketingUrl: localization?.attributes?.marketingUrl,
|
|
76
|
+
whatsNew: localization?.attributes?.whatsNew,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function convertToMultilingualAsoData(locales, defaultLocale) {
|
|
80
|
+
let finalDefaultLocale = defaultLocale;
|
|
81
|
+
if (!finalDefaultLocale && Object.keys(locales).length > 0) {
|
|
82
|
+
if (locales[DEFAULT_LOCALE]) {
|
|
83
|
+
finalDefaultLocale = DEFAULT_LOCALE;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
finalDefaultLocale = Object.keys(locales)[0];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
locales,
|
|
91
|
+
defaultLocale: finalDefaultLocale || DEFAULT_LOCALE,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function convertToReleaseNote(version, localizations) {
|
|
95
|
+
const releaseNotesMap = {};
|
|
96
|
+
for (const localization of localizations) {
|
|
97
|
+
const locale = localization.attributes?.locale;
|
|
98
|
+
const whatsNew = localization.attributes?.whatsNew;
|
|
99
|
+
if (locale && whatsNew) {
|
|
100
|
+
releaseNotesMap[locale] = whatsNew;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (Object.keys(releaseNotesMap).length === 0) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const versionString = version.attributes?.versionString;
|
|
107
|
+
const platform = version.attributes?.platform;
|
|
108
|
+
if (!versionString || !platform) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
versionString,
|
|
113
|
+
releaseNotes: releaseNotesMap,
|
|
114
|
+
platform,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function sortReleaseNotes(releaseNotes) {
|
|
118
|
+
const versionMap = new Map(releaseNotes.map((note) => [note.versionString, note]));
|
|
119
|
+
const sortedVersions = sortVersions(releaseNotes.map((note) => ({
|
|
120
|
+
type: "appStoreVersions",
|
|
121
|
+
id: note.versionString,
|
|
122
|
+
attributes: {
|
|
123
|
+
versionString: note.versionString,
|
|
124
|
+
platform: note.platform,
|
|
125
|
+
},
|
|
126
|
+
links: { self: note.versionString },
|
|
127
|
+
})));
|
|
128
|
+
return sortedVersions
|
|
129
|
+
.map((version) => versionMap.get(version.attributes?.versionString ?? ""))
|
|
130
|
+
.filter((note) => !!note && !!note.versionString);
|
|
131
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store Connect API Endpoints
|
|
3
|
+
*
|
|
4
|
+
* Centralized API endpoint management for App Store Connect
|
|
5
|
+
*/
|
|
6
|
+
import type { ApiResponse, AppInfo, AppInfoLocalization, AppInfoLocalizationUpdateAttributes, AppStoreApp, AppStoreLocalization, AppStoreScreenshot, AppStoreScreenshotSet, AppStoreVersion, AppStoreVersionLocalizationUpdateAttributes } from "./types.js";
|
|
7
|
+
export declare class AppStoreApiEndpoints {
|
|
8
|
+
private generateToken;
|
|
9
|
+
private issuerId;
|
|
10
|
+
private keyId;
|
|
11
|
+
constructor(generateToken: () => Promise<string>, issuerId: string, keyId: string);
|
|
12
|
+
normalizeNextLink(nextLink?: string | null): string | null;
|
|
13
|
+
private request;
|
|
14
|
+
listApps(nextUrl?: string): Promise<ApiResponse<AppStoreApp[]>>;
|
|
15
|
+
findAppByBundleId(bundleId: string): Promise<ApiResponse<AppStoreApp[]>>;
|
|
16
|
+
getApp(appId: string): Promise<ApiResponse<AppStoreApp>>;
|
|
17
|
+
listAppInfos(appId: string): Promise<ApiResponse<AppInfo[]>>;
|
|
18
|
+
listAppInfoLocalizations(appInfoId: string, locale?: string): Promise<ApiResponse<AppInfoLocalization[]>>;
|
|
19
|
+
updateAppInfoLocalization(localizationId: string, attributes: AppInfoLocalizationUpdateAttributes): Promise<void>;
|
|
20
|
+
createAppInfoLocalization(appInfoId: string, locale: string, attributes: AppInfoLocalizationUpdateAttributes): Promise<ApiResponse<AppInfoLocalization>>;
|
|
21
|
+
listAppStoreVersions(appId: string, options?: {
|
|
22
|
+
platform?: string;
|
|
23
|
+
state?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
}): Promise<ApiResponse<AppStoreVersion[]>>;
|
|
26
|
+
createAppStoreVersion(appId: string, versionString: string, platform?: string): Promise<ApiResponse<AppStoreVersion>>;
|
|
27
|
+
listAppStoreVersionLocalizations(versionId: string, locale?: string): Promise<ApiResponse<AppStoreLocalization[]>>;
|
|
28
|
+
getAppStoreVersionLocalization(localizationId: string): Promise<ApiResponse<AppStoreLocalization>>;
|
|
29
|
+
updateAppStoreVersionLocalization(localizationId: string, attributes: AppStoreVersionLocalizationUpdateAttributes): Promise<void>;
|
|
30
|
+
createAppStoreVersionLocalization(versionId: string, locale: string, attributes: AppStoreVersionLocalizationUpdateAttributes): Promise<ApiResponse<AppStoreLocalization>>;
|
|
31
|
+
listScreenshotSets(localizationId: string): Promise<ApiResponse<AppStoreScreenshotSet[]>>;
|
|
32
|
+
listScreenshots(screenshotSetId: string): Promise<ApiResponse<AppStoreScreenshot[]>>;
|
|
33
|
+
}
|