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
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"}
|