appstore-tools 1.0.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.
@@ -0,0 +1,9 @@
1
+ import { type IpaSource } from "../ipa/artifact.js";
2
+ export declare function ipaGenerateCommand(command: {
3
+ readonly outputIpaPath: string;
4
+ readonly ipaSource: Exclude<IpaSource, {
5
+ kind: "prebuilt";
6
+ }>;
7
+ readonly json: boolean;
8
+ }): Promise<number>;
9
+ //# sourceMappingURL=ipa-generate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipa-generate.d.ts","sourceRoot":"","sources":["../../src/commands/ipa-generate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGxE,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE;QAAE,IAAI,EAAE,UAAU,CAAA;KAAE,CAAC,CAAC;IAC7D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoClB"}
@@ -0,0 +1,31 @@
1
+ import { resolveIpaArtifact } from "../ipa/artifact.js";
2
+ import { verifyIpa } from "../ipa/preflight.js";
3
+ export async function ipaGenerateCommand(command) {
4
+ const artifact = await resolveIpaArtifact(command.ipaSource);
5
+ try {
6
+ const report = await verifyIpa({ ipaPath: artifact.ipaPath });
7
+ if (command.json) {
8
+ console.log(JSON.stringify({
9
+ outputIpaPath: command.outputIpaPath,
10
+ report
11
+ }, null, 2));
12
+ }
13
+ else {
14
+ console.log(`Generated IPA: ${command.outputIpaPath}`);
15
+ console.log(`Bundle ID: ${report.bundleId ?? "unknown"}`);
16
+ console.log(`Version: ${report.version ?? "unknown"} (${report.buildNumber ?? "unknown"})`);
17
+ console.log(`SHA-256: ${report.sha256 ?? "unavailable"}`);
18
+ console.log(`Signing validated: ${report.signingValidated ? "yes" : "no"}`);
19
+ }
20
+ if (report.errors.length > 0) {
21
+ report.errors.forEach((line) => console.error(`- ${line}`));
22
+ return 1;
23
+ }
24
+ return 0;
25
+ }
26
+ finally {
27
+ if (artifact.dispose) {
28
+ await artifact.dispose();
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,9 @@
1
+ export { AppStoreConnectClient, DomainError, InfrastructureError, type AppStoreConnectAuthConfig, type Clock, type FetchLike, type HttpMethod, type HttpQueryValue, type HttpRequest, type HttpResponse } from "./api/client.js";
2
+ export { executeUploadOperations, parseUploadOperations, type UploadFetchLike, type UploadHttpHeader, type UploadOperation } from "./api/types.js";
3
+ export { appsListCommand, listApps, type AppSummary } from "./commands/apps-list.js";
4
+ export { buildsUploadCommand, uploadBuild, type BuildsUploadInput, type BuildsUploadResult } from "./commands/builds-upload.js";
5
+ export { ipaGenerateCommand } from "./commands/ipa-generate.js";
6
+ export { resolveIpaArtifact, type CustomCommandIpaSource, type IpaArtifact, type IpaSource, type PrebuiltIpaSource, type ProcessRunner, type XcodebuildIpaSource } from "./ipa/artifact.js";
7
+ export { verifyIpa, type IpaPreflightReport, type VerifyStrictIpaInput } from "./ipa/preflight.js";
8
+ export { parseCliCommand, resolveCliEnvironment, runCli, type AppsListCliCommand, type BuildsUploadCliCommand, type CliCommand, type HelpCliCommand, type IpaGenerateCliCommand } from "./cli.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,qBAAqB,EACrB,WAAW,EACX,mBAAmB,EACnB,KAAK,yBAAyB,EAC9B,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGhE,OAAO,EACL,kBAAkB,EAClB,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,SAAS,EACT,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EAC1B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,MAAM,EACN,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC3B,MAAM,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // API client
2
+ export { AppStoreConnectClient, DomainError, InfrastructureError } from "./api/client.js";
3
+ // Upload operations
4
+ export { executeUploadOperations, parseUploadOperations } from "./api/types.js";
5
+ // Commands
6
+ export { appsListCommand, listApps } from "./commands/apps-list.js";
7
+ export { buildsUploadCommand, uploadBuild } from "./commands/builds-upload.js";
8
+ export { ipaGenerateCommand } from "./commands/ipa-generate.js";
9
+ // IPA utilities
10
+ export { resolveIpaArtifact } from "./ipa/artifact.js";
11
+ export { verifyIpa } from "./ipa/preflight.js";
12
+ // CLI
13
+ export { parseCliCommand, resolveCliEnvironment, runCli } from "./cli.js";
@@ -0,0 +1,40 @@
1
+ export interface PrebuiltIpaSource {
2
+ readonly kind: "prebuilt";
3
+ readonly ipaPath: string;
4
+ }
5
+ export interface XcodebuildIpaSource {
6
+ readonly kind: "xcodebuild";
7
+ readonly scheme: string;
8
+ readonly exportOptionsPlist: string;
9
+ readonly workspacePath?: string;
10
+ readonly projectPath?: string;
11
+ readonly configuration?: string;
12
+ readonly archivePath?: string;
13
+ readonly derivedDataPath?: string;
14
+ readonly outputIpaPath?: string;
15
+ }
16
+ export interface CustomCommandIpaSource {
17
+ readonly kind: "customCommand";
18
+ readonly buildCommand: string;
19
+ readonly generatedIpaPath: string;
20
+ readonly outputIpaPath?: string;
21
+ }
22
+ export type IpaSource = PrebuiltIpaSource | XcodebuildIpaSource | CustomCommandIpaSource;
23
+ export interface IpaArtifact {
24
+ readonly ipaPath: string;
25
+ readonly dispose?: () => Promise<void>;
26
+ }
27
+ interface ProcessRunResult {
28
+ readonly stdout: string;
29
+ readonly stderr: string;
30
+ }
31
+ export interface ProcessRunner {
32
+ run(command: string, args: readonly string[], options?: {
33
+ readonly cwd?: string;
34
+ readonly env?: NodeJS.ProcessEnv;
35
+ }): Promise<ProcessRunResult>;
36
+ }
37
+ export declare const defaultProcessRunner: ProcessRunner;
38
+ export declare function resolveIpaArtifact(source: IpaSource, processRunner?: ProcessRunner): Promise<IpaArtifact>;
39
+ export {};
40
+ //# sourceMappingURL=artifact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../../src/ipa/artifact.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,MAAM,SAAS,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,sBAAsB,CAAC;AAEzF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAMD,UAAU,gBAAgB;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CACD,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;KAAE,GACpE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC9B;AA+DD,eAAO,MAAM,oBAAoB,EAAE,aAAyC,CAAC;AAE7E,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,SAAS,EACjB,aAAa,GAAE,aAAoC,GAClD,OAAO,CAAC,WAAW,CAAC,CAWtB"}
@@ -0,0 +1,180 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access, constants, copyFile, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { InfrastructureError } from "../api/client.js";
6
+ function createNodeProcessRunner() {
7
+ return {
8
+ run(command, args, options = {}) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(command, args, {
11
+ cwd: options.cwd,
12
+ env: options.env,
13
+ stdio: ["ignore", "pipe", "pipe"]
14
+ });
15
+ const stdoutChunks = [];
16
+ const stderrChunks = [];
17
+ child.stdout.on("data", (chunk) => {
18
+ stdoutChunks.push(chunk);
19
+ });
20
+ child.stderr.on("data", (chunk) => {
21
+ stderrChunks.push(chunk);
22
+ });
23
+ child.on("error", (error) => {
24
+ reject(new InfrastructureError(`Failed to run command: ${[command, ...args].join(" ")}`, error));
25
+ });
26
+ child.on("close", (exitCode) => {
27
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
28
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
29
+ if (exitCode !== 0) {
30
+ reject(new InfrastructureError([
31
+ `Command exited with status ${String(exitCode)}.`,
32
+ `Command: ${[command, ...args].join(" ")}`,
33
+ stderr.trim() ? `stderr: ${stderr.trim()}` : "",
34
+ stdout.trim() ? `stdout: ${stdout.trim()}` : ""
35
+ ]
36
+ .filter((line) => line.length > 0)
37
+ .join("\n")));
38
+ return;
39
+ }
40
+ resolve({ stdout, stderr });
41
+ });
42
+ });
43
+ }
44
+ };
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Public API
48
+ // ---------------------------------------------------------------------------
49
+ export const defaultProcessRunner = createNodeProcessRunner();
50
+ export async function resolveIpaArtifact(source, processRunner = defaultProcessRunner) {
51
+ switch (source.kind) {
52
+ case "prebuilt":
53
+ return resolvePrebuilt(source);
54
+ case "xcodebuild":
55
+ return resolveXcodebuild(source, processRunner);
56
+ case "customCommand":
57
+ return resolveCustomCommand(source, processRunner);
58
+ default:
59
+ throw new InfrastructureError(`Unsupported IPA source kind: ${String(source)}`);
60
+ }
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // Prebuilt
64
+ // ---------------------------------------------------------------------------
65
+ async function resolvePrebuilt(source) {
66
+ const ipaPath = path.resolve(source.ipaPath);
67
+ await access(ipaPath, constants.R_OK).catch((error) => {
68
+ throw new InfrastructureError(`IPA file is not readable: ${ipaPath}`, error);
69
+ });
70
+ return { ipaPath };
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // Xcodebuild
74
+ // ---------------------------------------------------------------------------
75
+ const DEFAULT_CONFIGURATION = "Release";
76
+ async function resolveXcodebuild(source, processRunner) {
77
+ if (!source.scheme.trim()) {
78
+ throw new InfrastructureError("scheme is required for xcodebuild IPA source.");
79
+ }
80
+ if (!source.exportOptionsPlist.trim()) {
81
+ throw new InfrastructureError("exportOptionsPlist is required for xcodebuild IPA source.");
82
+ }
83
+ const hasWorkspace = Boolean(source.workspacePath);
84
+ const hasProject = Boolean(source.projectPath);
85
+ if (hasWorkspace === hasProject) {
86
+ throw new InfrastructureError("Exactly one of workspacePath or projectPath must be provided.");
87
+ }
88
+ const createdTemporaryDirectories = [];
89
+ const rootTemporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "asc-build-"));
90
+ createdTemporaryDirectories.push(rootTemporaryDirectory);
91
+ const archivePath = source.archivePath
92
+ ? path.resolve(source.archivePath)
93
+ : path.join(rootTemporaryDirectory, "archive.xcarchive");
94
+ const exportDirectory = path.join(rootTemporaryDirectory, "export");
95
+ const exportOptionsPlist = path.resolve(source.exportOptionsPlist);
96
+ await access(exportOptionsPlist, constants.R_OK).catch((error) => {
97
+ throw new InfrastructureError(`Export options plist is not readable: ${exportOptionsPlist}`, error);
98
+ });
99
+ const archiveArgs = createArchiveArgs(source, archivePath);
100
+ await processRunner.run("xcodebuild", archiveArgs);
101
+ await mkdir(exportDirectory, { recursive: true });
102
+ const exportArgs = [
103
+ "-exportArchive",
104
+ "-archivePath",
105
+ archivePath,
106
+ "-exportOptionsPlist",
107
+ exportOptionsPlist,
108
+ "-exportPath",
109
+ exportDirectory
110
+ ];
111
+ await processRunner.run("xcodebuild", exportArgs);
112
+ const exportedIpaPath = await findIpaInDirectory(exportDirectory);
113
+ if (!source.outputIpaPath) {
114
+ return {
115
+ ipaPath: exportedIpaPath,
116
+ dispose: async () => {
117
+ await cleanup(createdTemporaryDirectories);
118
+ }
119
+ };
120
+ }
121
+ const outputIpaPath = path.resolve(source.outputIpaPath);
122
+ await mkdir(path.dirname(outputIpaPath), { recursive: true });
123
+ await copyFile(exportedIpaPath, outputIpaPath);
124
+ await cleanup(createdTemporaryDirectories);
125
+ return { ipaPath: outputIpaPath };
126
+ }
127
+ function createArchiveArgs(source, archivePath) {
128
+ const args = [
129
+ "archive",
130
+ "-scheme",
131
+ source.scheme,
132
+ "-configuration",
133
+ source.configuration ?? DEFAULT_CONFIGURATION,
134
+ "-archivePath",
135
+ archivePath
136
+ ];
137
+ if (source.workspacePath) {
138
+ args.push("-workspace", path.resolve(source.workspacePath));
139
+ }
140
+ if (source.projectPath) {
141
+ args.push("-project", path.resolve(source.projectPath));
142
+ }
143
+ if (source.derivedDataPath) {
144
+ args.push("-derivedDataPath", path.resolve(source.derivedDataPath));
145
+ }
146
+ return args;
147
+ }
148
+ async function findIpaInDirectory(directoryPath) {
149
+ const entries = await readdir(directoryPath, { withFileTypes: true });
150
+ const ipaEntry = entries.find((entry) => entry.isFile() && entry.name.endsWith(".ipa"));
151
+ if (!ipaEntry) {
152
+ throw new InfrastructureError(`xcodebuild export did not produce an .ipa file in: ${directoryPath}`);
153
+ }
154
+ return path.join(directoryPath, ipaEntry.name);
155
+ }
156
+ async function cleanup(paths) {
157
+ for (const pathToRemove of paths) {
158
+ await rm(pathToRemove, { recursive: true, force: true });
159
+ }
160
+ }
161
+ // ---------------------------------------------------------------------------
162
+ // Custom command
163
+ // ---------------------------------------------------------------------------
164
+ async function resolveCustomCommand(source, processRunner) {
165
+ if (!source.buildCommand.trim()) {
166
+ throw new InfrastructureError("buildCommand is required for custom command IPA source.");
167
+ }
168
+ await processRunner.run("zsh", ["-lc", source.buildCommand]);
169
+ const generatedIpaPath = path.resolve(source.generatedIpaPath);
170
+ await access(generatedIpaPath, constants.R_OK).catch((error) => {
171
+ throw new InfrastructureError(`Generated IPA file is not readable: ${generatedIpaPath}`, error);
172
+ });
173
+ if (!source.outputIpaPath) {
174
+ return { ipaPath: generatedIpaPath };
175
+ }
176
+ const outputIpaPath = path.resolve(source.outputIpaPath);
177
+ await mkdir(path.dirname(outputIpaPath), { recursive: true });
178
+ await copyFile(generatedIpaPath, outputIpaPath);
179
+ return { ipaPath: outputIpaPath };
180
+ }
@@ -0,0 +1,21 @@
1
+ import { type ProcessRunner } from "./artifact.js";
2
+ export interface VerifyStrictIpaInput {
3
+ readonly ipaPath: string;
4
+ readonly expectedBundleId?: string;
5
+ readonly expectedVersion?: string;
6
+ readonly expectedBuildNumber?: string;
7
+ }
8
+ export interface IpaPreflightReport {
9
+ readonly ipaPath: string;
10
+ readonly bundleId: string | null;
11
+ readonly version: string | null;
12
+ readonly buildNumber: string | null;
13
+ readonly sizeBytes: number;
14
+ readonly sha256: string | null;
15
+ readonly md5: string | null;
16
+ readonly signingValidated: boolean;
17
+ readonly errors: readonly string[];
18
+ readonly warnings: readonly string[];
19
+ }
20
+ export declare function verifyIpa(input: VerifyStrictIpaInput, processRunner?: ProcessRunner): Promise<IpaPreflightReport>;
21
+ //# sourceMappingURL=preflight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preflight.d.ts","sourceRoot":"","sources":["../../src/ipa/preflight.ts"],"names":[],"mappings":"AAOA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAMzE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAMD,wBAAsB,SAAS,CAC7B,KAAK,EAAE,oBAAoB,EAC3B,aAAa,GAAE,aAAoC,GAClD,OAAO,CAAC,kBAAkB,CAAC,CA2H7B"}
@@ -0,0 +1,203 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { access, constants, mkdtemp, rm, stat } from "node:fs/promises";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { InfrastructureError } from "../api/client.js";
7
+ import { defaultProcessRunner } from "./artifact.js";
8
+ // ---------------------------------------------------------------------------
9
+ // Public API
10
+ // ---------------------------------------------------------------------------
11
+ export async function verifyIpa(input, processRunner = defaultProcessRunner) {
12
+ const ipaPath = path.resolve(input.ipaPath);
13
+ const errors = [];
14
+ const warnings = [];
15
+ let sizeBytes = 0;
16
+ let sha256 = null;
17
+ let md5 = null;
18
+ let bundleId = null;
19
+ let version = null;
20
+ let buildNumber = null;
21
+ let signingValidated = false;
22
+ const statResult = await stat(ipaPath).catch(() => null);
23
+ if (!statResult) {
24
+ errors.push(`IPA file does not exist: ${ipaPath}`);
25
+ }
26
+ else if (!statResult.isFile()) {
27
+ errors.push(`IPA path is not a file: ${ipaPath}`);
28
+ }
29
+ else {
30
+ sizeBytes = statResult.size;
31
+ }
32
+ await access(ipaPath, constants.R_OK).catch(() => {
33
+ errors.push(`IPA file is not readable: ${ipaPath}`);
34
+ });
35
+ if (!ipaPath.endsWith(".ipa")) {
36
+ errors.push("IPA file must have .ipa extension.");
37
+ }
38
+ if (sizeBytes <= 0) {
39
+ errors.push("IPA file is empty.");
40
+ }
41
+ if (errors.length === 0) {
42
+ try {
43
+ const digests = await computeDigests(ipaPath);
44
+ sha256 = digests.sha256;
45
+ md5 = digests.md5;
46
+ }
47
+ catch (error) {
48
+ errors.push(toMessage(error, "Failed to compute IPA checksums."));
49
+ }
50
+ }
51
+ let infoPlistEntryPath = null;
52
+ if (errors.length === 0) {
53
+ try {
54
+ const zipEntries = await listZipEntries(ipaPath, processRunner);
55
+ infoPlistEntryPath =
56
+ zipEntries.find((entry) => /^Payload\/[^/]+\.app\/Info\.plist$/.test(entry)) ?? null;
57
+ if (!infoPlistEntryPath) {
58
+ errors.push("IPA is missing Payload/*.app/Info.plist.");
59
+ }
60
+ }
61
+ catch (error) {
62
+ errors.push(toMessage(error, "Failed to inspect IPA archive contents."));
63
+ }
64
+ }
65
+ if (infoPlistEntryPath) {
66
+ try {
67
+ const bundleInfo = await extractBundleInfo(ipaPath, infoPlistEntryPath, processRunner);
68
+ bundleId = bundleInfo.bundleId;
69
+ version = bundleInfo.version;
70
+ buildNumber = bundleInfo.buildNumber;
71
+ }
72
+ catch (error) {
73
+ errors.push(toMessage(error, "Failed to read Info.plist from IPA."));
74
+ }
75
+ }
76
+ if (input.expectedBundleId) {
77
+ if (bundleId !== input.expectedBundleId) {
78
+ errors.push(`CFBundleIdentifier mismatch. Expected "${input.expectedBundleId}", got "${bundleId ?? "null"}".`);
79
+ }
80
+ }
81
+ else if (!bundleId) {
82
+ errors.push("CFBundleIdentifier is missing in Info.plist.");
83
+ }
84
+ if (input.expectedVersion) {
85
+ if (version !== input.expectedVersion) {
86
+ errors.push(`CFBundleShortVersionString mismatch. Expected "${input.expectedVersion}", got "${version ?? "null"}".`);
87
+ }
88
+ }
89
+ else if (!version) {
90
+ errors.push("CFBundleShortVersionString is missing in Info.plist.");
91
+ }
92
+ if (input.expectedBuildNumber) {
93
+ if (buildNumber !== input.expectedBuildNumber) {
94
+ errors.push(`CFBundleVersion mismatch. Expected "${input.expectedBuildNumber}", got "${buildNumber ?? "null"}".`);
95
+ }
96
+ }
97
+ else if (!buildNumber) {
98
+ errors.push("CFBundleVersion is missing in Info.plist.");
99
+ }
100
+ if (infoPlistEntryPath) {
101
+ try {
102
+ await verifyCodeSigning(ipaPath, infoPlistEntryPath, processRunner);
103
+ signingValidated = true;
104
+ }
105
+ catch (error) {
106
+ errors.push(toMessage(error, "Code signing verification failed."));
107
+ }
108
+ }
109
+ return {
110
+ ipaPath,
111
+ bundleId,
112
+ version,
113
+ buildNumber,
114
+ sizeBytes,
115
+ sha256,
116
+ md5,
117
+ signingValidated,
118
+ errors,
119
+ warnings
120
+ };
121
+ }
122
+ function computeDigests(filePath) {
123
+ return new Promise((resolve, reject) => {
124
+ const sha256Hash = createHash("sha256");
125
+ const md5Hash = createHash("md5");
126
+ const stream = createReadStream(filePath);
127
+ stream.on("data", (chunk) => {
128
+ sha256Hash.update(chunk);
129
+ md5Hash.update(chunk);
130
+ });
131
+ stream.on("error", (error) => {
132
+ reject(error);
133
+ });
134
+ stream.on("end", () => {
135
+ resolve({
136
+ sha256: sha256Hash.digest("hex"),
137
+ md5: md5Hash.digest("hex")
138
+ });
139
+ });
140
+ });
141
+ }
142
+ async function listZipEntries(ipaPath, processRunner) {
143
+ const output = await processRunner.run("unzip", ["-Z1", ipaPath]);
144
+ return output.stdout
145
+ .split("\n")
146
+ .map((line) => line.trim())
147
+ .filter((line) => line.length > 0);
148
+ }
149
+ async function extractBundleInfo(ipaPath, infoPlistEntryPath, processRunner) {
150
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), "asc-ipa-info-"));
151
+ try {
152
+ await processRunner.run("unzip", [
153
+ "-q",
154
+ "-o",
155
+ ipaPath,
156
+ infoPlistEntryPath,
157
+ "-d",
158
+ tempDirectory
159
+ ]);
160
+ const infoPlistPath = path.join(tempDirectory, ...infoPlistEntryPath.split("/"));
161
+ const plistJson = await processRunner.run("plutil", [
162
+ "-convert",
163
+ "json",
164
+ "-o",
165
+ "-",
166
+ infoPlistPath
167
+ ]);
168
+ const payload = JSON.parse(plistJson.stdout);
169
+ return {
170
+ bundleId: toNullableString(payload.CFBundleIdentifier),
171
+ version: toNullableString(payload.CFBundleShortVersionString),
172
+ buildNumber: toNullableString(payload.CFBundleVersion)
173
+ };
174
+ }
175
+ finally {
176
+ await rm(tempDirectory, { recursive: true, force: true });
177
+ }
178
+ }
179
+ async function verifyCodeSigning(ipaPath, infoPlistEntryPath, processRunner) {
180
+ const appDirectoryEntryPath = infoPlistEntryPath.replace(/\/Info\.plist$/, "");
181
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), "asc-ipa-signing-"));
182
+ try {
183
+ await processRunner.run("unzip", ["-q", "-o", ipaPath, "Payload/*", "-d", tempDirectory]);
184
+ const appDirectoryPath = path.join(tempDirectory, ...appDirectoryEntryPath.split("/"));
185
+ await processRunner.run("codesign", ["--verify", "--strict", "--deep", appDirectoryPath]);
186
+ await processRunner.run("codesign", ["-dv", appDirectoryPath]);
187
+ }
188
+ finally {
189
+ await rm(tempDirectory, { recursive: true, force: true });
190
+ }
191
+ }
192
+ function toNullableString(value) {
193
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
194
+ }
195
+ function toMessage(error, fallback) {
196
+ if (error instanceof InfrastructureError) {
197
+ return error.message;
198
+ }
199
+ if (error instanceof Error && error.message.trim()) {
200
+ return error.message;
201
+ }
202
+ return fallback;
203
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "appstore-tools",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript App Store Connect API client and CLI.",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "test": "vitest run",
8
+ "build": "tsc -p tsconfig.build.json",
9
+ "typecheck": "tsc --noEmit",
10
+ "test:watch": "vitest",
11
+ "verify": "pnpm typecheck && pnpm test && pnpm build && pnpm cli -- --help",
12
+ "prepare": "tsc -p tsconfig.build.json",
13
+ "cli": "node dist/cli.js",
14
+ "cli:dev": "npx tsx src/cli.ts"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "keywords": [
20
+ "app-store-connect",
21
+ "apple",
22
+ "ios",
23
+ "ipa",
24
+ "cli"
25
+ ],
26
+ "author": "Alejandro Sanabria <alesanabriav@gmail.com>",
27
+ "license": "MIT",
28
+ "type": "module",
29
+ "types": "dist/index.d.ts",
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^25.3.0",
35
+ "tsx": "^4.20.6",
36
+ "typescript": "^5.9.3",
37
+ "vitest": "^4.0.18"
38
+ },
39
+ "packageManager": "pnpm@10.29.3",
40
+ "bin": {
41
+ "appstore-tools": "dist/cli.js"
42
+ }
43
+ }