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.
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/api/client.d.ts +54 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +183 -0
- package/dist/api/types.d.ts +24 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +77 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +292 -0
- package/dist/commands/apps-list.d.ts +12 -0
- package/dist/commands/apps-list.d.ts.map +1 -0
- package/dist/commands/apps-list.js +63 -0
- package/dist/commands/builds-upload.d.ts +45 -0
- package/dist/commands/builds-upload.d.ts.map +1 -0
- package/dist/commands/builds-upload.js +262 -0
- package/dist/commands/ipa-generate.d.ts +9 -0
- package/dist/commands/ipa-generate.d.ts.map +1 -0
- package/dist/commands/ipa-generate.js +31 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/ipa/artifact.d.ts +40 -0
- package/dist/ipa/artifact.d.ts.map +1 -0
- package/dist/ipa/artifact.js +180 -0
- package/dist/ipa/preflight.d.ts +21 -0
- package/dist/ipa/preflight.d.ts.map +1 -0
- package/dist/ipa/preflight.js +203 -0
- package/package.json +43 -0
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|