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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alejandro Sanabria
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # appstore-tools
2
+
3
+ TypeScript CLI and library for App Store Connect. Authenticate with JWT, list apps, generate IPAs, and upload builds — all from your terminal.
4
+
5
+ ## Installation
6
+
7
+ ### From npm
8
+
9
+ ```bash
10
+ npx appstore-tools --help
11
+ ```
12
+
13
+ Or install globally:
14
+
15
+ ```bash
16
+ npm install -g appstore-tools
17
+ appstore-tools --help
18
+ ```
19
+
20
+ ### From source
21
+
22
+ ```bash
23
+ git clone https://github.com/alesanabriav7/appstore-tools.git
24
+ cd appstore-tools
25
+ pnpm install
26
+ npm link
27
+ appstore-tools --help
28
+ ```
29
+
30
+ ## Setup
31
+
32
+ Requires Node.js 20+ and an [App Store Connect API key](https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api).
33
+
34
+ Set these environment variables (via `.env`, shell, or CI secrets):
35
+
36
+ ```env
37
+ ASC_ISSUER_ID=your-issuer-id
38
+ ASC_KEY_ID=your-key-id
39
+ ```
40
+
41
+ For the private key, point to your `.p8` file (recommended):
42
+
43
+ ```env
44
+ ASC_PRIVATE_KEY_PATH=./AuthKey_XXXXXX.p8
45
+ ```
46
+
47
+ Or pass the key inline:
48
+
49
+ ```env
50
+ ASC_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
51
+ ```
52
+
53
+ `ASC_PRIVATE_KEY_PATH` takes priority when both are set. `ASC_BASE_URL` is optional and defaults to `https://api.appstoreconnect.apple.com/`.
54
+
55
+ ## Usage
56
+
57
+ ### List apps
58
+
59
+ ```bash
60
+ npx appstore-tools apps list
61
+ ```
62
+
63
+ JSON output:
64
+
65
+ ```bash
66
+ npx appstore-tools apps list --json
67
+ ```
68
+
69
+ ### Generate IPA
70
+
71
+ No credentials required.
72
+
73
+ From xcodebuild:
74
+
75
+ ```bash
76
+ npx appstore-tools ipa generate \
77
+ --output-ipa ./dist/MyApp.ipa \
78
+ --scheme MyApp \
79
+ --workspace-path ./MyApp.xcworkspace \
80
+ --export-options-plist ./ExportOptions.plist
81
+ ```
82
+
83
+ From a custom command:
84
+
85
+ ```bash
86
+ npx appstore-tools ipa generate \
87
+ --output-ipa ./dist/MyApp.ipa \
88
+ --build-command "make build-ipa" \
89
+ --generated-ipa-path ./build/MyApp.ipa
90
+ ```
91
+
92
+ ### Upload build
93
+
94
+ Dry-run by default — verifies the IPA locally without uploading:
95
+
96
+ ```bash
97
+ npx appstore-tools builds upload \
98
+ --app com.example.myapp \
99
+ --version 1.2.3 \
100
+ --build-number 45 \
101
+ --ipa ./dist/MyApp.ipa
102
+ ```
103
+
104
+ Add `--apply` to upload, `--wait-processing` to poll until done:
105
+
106
+ ```bash
107
+ npx appstore-tools builds upload \
108
+ --app com.example.myapp \
109
+ --version 1.2.3 \
110
+ --build-number 45 \
111
+ --ipa ./dist/MyApp.ipa \
112
+ --apply --wait-processing
113
+ ```
114
+
115
+ Build and upload in one step (xcodebuild mode):
116
+
117
+ ```bash
118
+ npx appstore-tools builds upload \
119
+ --app com.example.myapp \
120
+ --version 1.2.3 \
121
+ --build-number 45 \
122
+ --scheme MyApp \
123
+ --workspace-path ./MyApp.xcworkspace \
124
+ --export-options-plist ./ExportOptions.plist \
125
+ --apply
126
+ ```
127
+
128
+ #### Preflight checks
129
+
130
+ Every upload runs these checks before touching App Store Connect:
131
+
132
+ - File exists, is readable, has `.ipa` extension
133
+ - Archive contains `Payload/*.app/Info.plist`
134
+ - Bundle ID, version, and build number match expectations
135
+ - Code signing is valid (`codesign --verify --strict --deep`)
136
+ - SHA-256 and MD5 checksums computed
137
+
138
+ ### Help
139
+
140
+ ```bash
141
+ npx appstore-tools --help
142
+ ```
143
+
144
+ ## Library usage
145
+
146
+ ```typescript
147
+ import { AppStoreConnectClient, listApps } from "appstore-tools";
148
+
149
+ const client = new AppStoreConnectClient({
150
+ issuerId: process.env.ASC_ISSUER_ID!,
151
+ keyId: process.env.ASC_KEY_ID!,
152
+ privateKey: process.env.ASC_PRIVATE_KEY!
153
+ });
154
+
155
+ const apps = await listApps(client);
156
+ console.log(apps);
157
+ ```
158
+
159
+ ## Development
160
+
161
+ ```bash
162
+ pnpm install
163
+ pnpm verify # typecheck + test + build + help
164
+ ```
165
+
166
+ Individual commands:
167
+
168
+ ```bash
169
+ pnpm typecheck # type check
170
+ pnpm test # run tests
171
+ pnpm build # compile to dist/
172
+ pnpm cli -- --help # run built CLI
173
+ pnpm cli:dev -- --help # run from source (no build needed)
174
+ ```
175
+
176
+ ## Project structure
177
+
178
+ ```
179
+ src/
180
+ api/
181
+ client.ts # HTTP client with JWT auth
182
+ types.ts # Shared upload operation types
183
+ commands/
184
+ apps-list.ts # apps list command
185
+ builds-upload.ts # builds upload command
186
+ ipa-generate.ts # ipa generate command
187
+ ipa/
188
+ artifact.ts # IPA resolution (prebuilt/xcodebuild/custom)
189
+ preflight.ts # IPA verification
190
+ cli.ts # CLI entry point
191
+ index.ts # Public API exports
192
+ ```
@@ -0,0 +1,54 @@
1
+ export declare class DomainError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class InfrastructureError extends Error {
5
+ readonly cause?: unknown;
6
+ constructor(message: string, cause?: unknown);
7
+ }
8
+ export type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE";
9
+ export type HttpQueryValue = string | number | boolean | undefined;
10
+ export interface HttpRequest {
11
+ readonly method: HttpMethod;
12
+ readonly path: string;
13
+ readonly query?: Readonly<Record<string, HttpQueryValue>>;
14
+ readonly headers?: Readonly<Record<string, string>>;
15
+ readonly body?: unknown;
16
+ }
17
+ export interface HttpResponse<T> {
18
+ readonly status: number;
19
+ readonly headers: Headers;
20
+ readonly data: T;
21
+ }
22
+ export interface AppStoreConnectAuthConfig {
23
+ readonly issuerId: string;
24
+ readonly keyId: string;
25
+ readonly privateKey: string;
26
+ readonly audience?: string;
27
+ readonly scope?: readonly string[];
28
+ readonly tokenTtlSeconds?: number;
29
+ }
30
+ export interface Clock {
31
+ now(): Date;
32
+ }
33
+ export type FetchLike = (input: URL | string, init?: RequestInit) => Promise<Response>;
34
+ export declare class AppStoreConnectClient {
35
+ private cachedToken;
36
+ private readonly baseUrl;
37
+ private readonly clock;
38
+ private readonly fetchLike;
39
+ private readonly config;
40
+ constructor(config: AppStoreConnectAuthConfig, options?: {
41
+ readonly baseUrl?: string;
42
+ readonly clock?: Clock;
43
+ readonly fetchLike?: FetchLike;
44
+ });
45
+ request<T>(request: HttpRequest): Promise<HttpResponse<T>>;
46
+ getToken(): Promise<string>;
47
+ private buildPayload;
48
+ private sign;
49
+ private currentEpochSeconds;
50
+ private buildUrl;
51
+ private assertValidConfig;
52
+ }
53
+ export declare function safeReadText(response: Response): Promise<string>;
54
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAMA,qBAAa,WAAY,SAAQ,KAAK;gBACjB,OAAO,EAAE,MAAM;CAInC;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,SAAyB,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEtB,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAKpD;AAMD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE7D,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAEnE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IAC1D,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;CAClB;AAMD,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAuCD,MAAM,WAAW,KAAK;IACpB,GAAG,IAAI,IAAI,CAAC;CACb;AAYD,MAAM,MAAM,SAAS,GAAG,CACtB,KAAK,EAAE,GAAG,GAAG,MAAM,EACnB,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB,qBAAa,qBAAqB;IAChC,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAM;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAGjD,MAAM,EAAE,yBAAyB,EACjC,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;QACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;KAChC;IASU,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IA8D1D,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IA+BxC,OAAO,CAAC,YAAY;IAoBpB,OAAO,CAAC,IAAI;IAiBZ,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,iBAAiB;CAqB1B;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAMtE"}
@@ -0,0 +1,183 @@
1
+ import { createSign } from "node:crypto";
2
+ // ---------------------------------------------------------------------------
3
+ // Errors
4
+ // ---------------------------------------------------------------------------
5
+ export class DomainError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "DomainError";
9
+ }
10
+ }
11
+ export class InfrastructureError extends Error {
12
+ cause;
13
+ constructor(message, cause) {
14
+ super(message);
15
+ this.name = "InfrastructureError";
16
+ this.cause = cause;
17
+ }
18
+ }
19
+ function encodeJsonAsBase64Url(value) {
20
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
21
+ }
22
+ const MAX_TOKEN_TTL_SECONDS = 1200;
23
+ const DEFAULT_TOKEN_TTL_SECONDS = 1200;
24
+ const REFRESH_WINDOW_SECONDS = 30;
25
+ const DEFAULT_AUDIENCE = "appstoreconnect-v1";
26
+ const DEFAULT_BASE_URL = "https://api.appstoreconnect.apple.com/";
27
+ class SystemClock {
28
+ now() {
29
+ return new Date();
30
+ }
31
+ }
32
+ export class AppStoreConnectClient {
33
+ cachedToken = null;
34
+ baseUrl;
35
+ clock;
36
+ fetchLike;
37
+ config;
38
+ constructor(config, options) {
39
+ this.assertValidConfig(config);
40
+ this.config = config;
41
+ this.baseUrl = new URL(options?.baseUrl ?? DEFAULT_BASE_URL);
42
+ this.clock = options?.clock ?? new SystemClock();
43
+ this.fetchLike = options?.fetchLike ?? fetch;
44
+ }
45
+ async request(request) {
46
+ const token = await this.getToken();
47
+ const url = this.buildUrl(request.path, request.query);
48
+ const headers = new Headers(request.headers);
49
+ headers.set("Authorization", `Bearer ${token}`);
50
+ const hasBody = request.body !== undefined;
51
+ if (hasBody && !headers.has("Content-Type")) {
52
+ headers.set("Content-Type", "application/json");
53
+ }
54
+ const requestInit = {
55
+ method: request.method,
56
+ headers
57
+ };
58
+ if (hasBody) {
59
+ requestInit.body = JSON.stringify(request.body);
60
+ }
61
+ const response = await this.fetchLike(url, requestInit);
62
+ if (!response.ok) {
63
+ const errorBody = await safeReadText(response);
64
+ throw new InfrastructureError(`App Store Connect request failed (${response.status}): ${errorBody || response.statusText}`);
65
+ }
66
+ if (response.status === 204) {
67
+ return {
68
+ status: response.status,
69
+ headers: response.headers,
70
+ data: undefined
71
+ };
72
+ }
73
+ const textBody = await response.text();
74
+ if (!textBody) {
75
+ return {
76
+ status: response.status,
77
+ headers: response.headers,
78
+ data: undefined
79
+ };
80
+ }
81
+ try {
82
+ return {
83
+ status: response.status,
84
+ headers: response.headers,
85
+ data: JSON.parse(textBody)
86
+ };
87
+ }
88
+ catch (error) {
89
+ throw new InfrastructureError("Received invalid JSON from App Store Connect.", error);
90
+ }
91
+ }
92
+ async getToken() {
93
+ const nowEpochSeconds = this.currentEpochSeconds();
94
+ if (this.cachedToken &&
95
+ nowEpochSeconds < this.cachedToken.expiresAtEpochSeconds - REFRESH_WINDOW_SECONDS) {
96
+ return this.cachedToken.token;
97
+ }
98
+ const payload = this.buildPayload(nowEpochSeconds);
99
+ const header = {
100
+ alg: "ES256",
101
+ kid: this.config.keyId,
102
+ typ: "JWT"
103
+ };
104
+ const encodedHeader = encodeJsonAsBase64Url(header);
105
+ const encodedPayload = encodeJsonAsBase64Url(payload);
106
+ const signaturePayload = `${encodedHeader}.${encodedPayload}`;
107
+ const signature = this.sign(signaturePayload);
108
+ const token = `${signaturePayload}.${signature}`;
109
+ this.cachedToken = {
110
+ token,
111
+ expiresAtEpochSeconds: payload.exp
112
+ };
113
+ return token;
114
+ }
115
+ buildPayload(nowEpochSeconds) {
116
+ const ttlSeconds = this.config.tokenTtlSeconds ?? DEFAULT_TOKEN_TTL_SECONDS;
117
+ const payload = {
118
+ iss: this.config.issuerId,
119
+ iat: nowEpochSeconds,
120
+ exp: nowEpochSeconds + ttlSeconds,
121
+ aud: this.config.audience ?? DEFAULT_AUDIENCE
122
+ };
123
+ if (this.config.scope && this.config.scope.length > 0) {
124
+ return {
125
+ ...payload,
126
+ scope: this.config.scope
127
+ };
128
+ }
129
+ return payload;
130
+ }
131
+ sign(payload) {
132
+ try {
133
+ const signer = createSign("SHA256");
134
+ signer.update(payload);
135
+ signer.end();
136
+ return signer
137
+ .sign({ key: this.config.privateKey, dsaEncoding: "ieee-p1363" })
138
+ .toString("base64url");
139
+ }
140
+ catch (error) {
141
+ throw new InfrastructureError("Failed to sign App Store Connect JWT token.", error);
142
+ }
143
+ }
144
+ currentEpochSeconds() {
145
+ return Math.floor(this.clock.now().getTime() / 1000);
146
+ }
147
+ buildUrl(path, query) {
148
+ const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
149
+ const url = new URL(normalizedPath, this.baseUrl);
150
+ if (!query) {
151
+ return url;
152
+ }
153
+ for (const [key, value] of Object.entries(query)) {
154
+ if (value !== undefined) {
155
+ url.searchParams.set(key, String(value));
156
+ }
157
+ }
158
+ return url;
159
+ }
160
+ assertValidConfig(config) {
161
+ if (!config.issuerId.trim()) {
162
+ throw new InfrastructureError("issuerId is required.");
163
+ }
164
+ if (!config.keyId.trim()) {
165
+ throw new InfrastructureError("keyId is required.");
166
+ }
167
+ if (!config.privateKey.trim()) {
168
+ throw new InfrastructureError("privateKey is required.");
169
+ }
170
+ const ttl = config.tokenTtlSeconds ?? DEFAULT_TOKEN_TTL_SECONDS;
171
+ if (ttl <= 0 || ttl > MAX_TOKEN_TTL_SECONDS) {
172
+ throw new InfrastructureError(`tokenTtlSeconds must be between 1 and ${MAX_TOKEN_TTL_SECONDS}.`);
173
+ }
174
+ }
175
+ }
176
+ export async function safeReadText(response) {
177
+ try {
178
+ return await response.text();
179
+ }
180
+ catch {
181
+ return "";
182
+ }
183
+ }
@@ -0,0 +1,24 @@
1
+ export interface UploadHttpHeader {
2
+ readonly name: string;
3
+ readonly value: string;
4
+ }
5
+ export interface UploadOperation {
6
+ readonly method: string;
7
+ readonly url: string;
8
+ readonly length: number;
9
+ readonly offset: number;
10
+ readonly requestHeaders: readonly UploadHttpHeader[];
11
+ }
12
+ export declare function parseUploadOperations(operationsPayload: readonly {
13
+ readonly method?: string;
14
+ readonly url?: string;
15
+ readonly offset?: number;
16
+ readonly length?: number;
17
+ readonly requestHeaders?: readonly {
18
+ readonly name?: string;
19
+ readonly value?: string;
20
+ }[];
21
+ }[], context: string): UploadOperation[];
22
+ export type UploadFetchLike = (input: URL | string, init?: RequestInit) => Promise<Response>;
23
+ export declare function executeUploadOperations(filePath: string, operations: readonly UploadOperation[], fetchLike?: UploadFetchLike): Promise<void>;
24
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACtD;AAED,wBAAgB,qBAAqB,CACnC,iBAAiB,EAAE,SAAS;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS;QACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;KACzB,EAAE,CAAC;CACL,EAAE,EACH,OAAO,EAAE,MAAM,GACd,eAAe,EAAE,CAkCnB;AAMD,MAAM,MAAM,eAAe,GAAG,CAC5B,KAAK,EAAE,GAAG,GAAG,MAAM,EACnB,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,SAAS,eAAe,EAAE,EACtC,SAAS,GAAE,eAAuB,GACjC,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -0,0 +1,77 @@
1
+ import { open } from "node:fs/promises";
2
+ import { InfrastructureError, safeReadText } from "./client.js";
3
+ export function parseUploadOperations(operationsPayload, context) {
4
+ return operationsPayload.map((item) => {
5
+ const method = item.method;
6
+ const url = item.url;
7
+ const length = item.length;
8
+ const offset = item.offset;
9
+ if (!method || !url || length === undefined || offset === undefined) {
10
+ throw new InfrastructureError(`Malformed ${context} payload: invalid upload operation.`);
11
+ }
12
+ const requestHeaders = (item.requestHeaders ?? []).map((header) => {
13
+ if (!header.name || header.value === undefined) {
14
+ throw new InfrastructureError(`Malformed ${context} payload: invalid upload operation header.`);
15
+ }
16
+ return {
17
+ name: header.name,
18
+ value: header.value
19
+ };
20
+ });
21
+ return {
22
+ method,
23
+ url,
24
+ length,
25
+ offset,
26
+ requestHeaders
27
+ };
28
+ });
29
+ }
30
+ export async function executeUploadOperations(filePath, operations, fetchLike = fetch) {
31
+ const fileHandle = await open(filePath, "r");
32
+ try {
33
+ for (const operation of operations) {
34
+ await executeOperation(fileHandle, operation, fetchLike);
35
+ }
36
+ }
37
+ finally {
38
+ await fileHandle.close();
39
+ }
40
+ }
41
+ async function executeOperation(fileHandle, operation, fetchLike) {
42
+ if (operation.length < 0 || operation.offset < 0) {
43
+ throw new InfrastructureError("Upload operation has invalid offset/length.");
44
+ }
45
+ const headers = new Headers();
46
+ for (const header of operation.requestHeaders) {
47
+ headers.set(header.name, header.value);
48
+ }
49
+ const body = operation.length === 0
50
+ ? undefined
51
+ : await readFileChunk(fileHandle, operation.offset, operation.length);
52
+ const requestInit = {
53
+ method: operation.method,
54
+ headers
55
+ };
56
+ if (body !== undefined) {
57
+ requestInit.body = body;
58
+ }
59
+ const response = await fetchLike(operation.url, requestInit);
60
+ if (!response.ok) {
61
+ const errorBody = await safeReadText(response);
62
+ throw new InfrastructureError(`Upload operation failed (${response.status}): ${errorBody || response.statusText}`);
63
+ }
64
+ }
65
+ async function readFileChunk(fileHandle, offset, length) {
66
+ const buffer = Buffer.alloc(length);
67
+ const { bytesRead } = await fileHandle.read({
68
+ buffer,
69
+ offset: 0,
70
+ length,
71
+ position: offset
72
+ });
73
+ if (bytesRead !== length) {
74
+ throw new InfrastructureError(`Upload operation expected ${length} bytes but read ${bytesRead} bytes.`);
75
+ }
76
+ return buffer;
77
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import type { IpaSource } from "./ipa/artifact.js";
3
+ interface CliEnvironment {
4
+ readonly issuerId: string;
5
+ readonly keyId: string;
6
+ readonly privateKey: string;
7
+ readonly baseUrl: string;
8
+ }
9
+ export declare function resolveCliEnvironment(env: NodeJS.ProcessEnv): Promise<CliEnvironment>;
10
+ export interface AppsListCliCommand {
11
+ readonly kind: "apps-list";
12
+ readonly json: boolean;
13
+ }
14
+ export interface IpaGenerateCliCommand {
15
+ readonly kind: "ipa-generate";
16
+ readonly json: boolean;
17
+ readonly outputIpaPath: string;
18
+ readonly ipaSource: Exclude<IpaSource, {
19
+ kind: "prebuilt";
20
+ }>;
21
+ }
22
+ export interface BuildsUploadCliCommand {
23
+ readonly kind: "builds-upload";
24
+ readonly json: boolean;
25
+ readonly apply: boolean;
26
+ readonly waitProcessing: boolean;
27
+ readonly appReference: string;
28
+ readonly version: string;
29
+ readonly buildNumber: string;
30
+ readonly ipaSource: IpaSource;
31
+ }
32
+ export interface HelpCliCommand {
33
+ readonly kind: "help";
34
+ }
35
+ export type CliCommand = AppsListCliCommand | IpaGenerateCliCommand | BuildsUploadCliCommand | HelpCliCommand;
36
+ export declare function parseCliCommand(argv: readonly string[]): CliCommand;
37
+ export declare function runCli(argv: readonly string[], env: NodeJS.ProcessEnv): Promise<number>;
38
+ export {};
39
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AASnD,UAAU,cAAc;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAID,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAqC3F;AA2BD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE;QAAE,IAAI,EAAE,UAAU,CAAA;KAAE,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,UAAU,GAClB,kBAAkB,GAClB,qBAAqB,GACrB,sBAAsB,GACtB,cAAc,CAAC;AAEnB,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA2BnE;AAyND,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,MAAM,CAAC,CASjB"}