@sitepong/fastnode 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -0
- package/dist/actions/index.cjs +758 -110
- package/dist/actions/index.cjs.map +1 -1
- package/dist/actions/index.mjs +758 -110
- package/dist/actions/index.mjs.map +1 -1
- package/dist/cert/index.cjs +40 -0
- package/dist/cert/index.cjs.map +1 -1
- package/dist/cert/index.d.cts +3 -3
- package/dist/cert/index.d.ts +3 -3
- package/dist/cert/index.mjs +37 -1
- package/dist/cert/index.mjs.map +1 -1
- package/dist/{cert-owkOFNW8.d.ts → cert-e5GAgAPb.d.ts} +1 -1
- package/dist/{cert-BmJGtYtb.d.cts → cert-uLLdb5a7.d.cts} +1 -1
- package/dist/cli/index.cjs +758 -110
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.mjs +758 -110
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.cjs +751 -109
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.mjs +747 -110
- package/dist/client/index.mjs.map +1 -1
- package/dist/deliver/index.d.cts +2 -2
- package/dist/deliver/index.d.ts +2 -2
- package/dist/{index-BFdNOmPJ.d.cts → index-BN0iWhuw.d.cts} +1 -1
- package/dist/index-BNP7Bpu5.d.ts +61 -0
- package/dist/{index-1Vu2cRQ2.d.ts → index-BbY_H_f6.d.ts} +1 -1
- package/dist/{index-D6zjGlPL.d.cts → index-BgHagzj1.d.cts} +1 -1
- package/dist/{index-D5eihzwM.d.cts → index-BjkOUQcQ.d.cts} +1 -1
- package/dist/{index-D1Izm7qt.d.ts → index-BwwpAlk2.d.ts} +363 -2
- package/dist/{index-A5MimeQk.d.ts → index-C2YLoFVl.d.ts} +1 -1
- package/dist/{index-TfXqwuac.d.ts → index-CEldrbyz.d.ts} +1 -1
- package/dist/{index-BLubSlgK.d.cts → index-CdfgEZwh.d.cts} +363 -2
- package/dist/{index-DfCUVJX1.d.cts → index-Clyf4-lu.d.cts} +1 -1
- package/dist/index-CtydPNxA.d.cts +61 -0
- package/dist/{index-BQJIJlK7.d.cts → index-DHxi-QPC.d.cts} +3 -3
- package/dist/{index-BN0ubxjZ.d.ts → index-Di4Ic0EE.d.ts} +3 -3
- package/dist/{index-EcVvViW8.d.cts → index-DrCOh0Vh.d.cts} +1 -1
- package/dist/{index-B0yhu_LZ.d.ts → index-EKU_G1G5.d.ts} +1 -1
- package/dist/{index-CWNLnSOL.d.cts → index-XsMsVc3O.d.cts} +1 -1
- package/dist/{index-B9yXicrQ.d.ts → index-jeI44Q5G.d.ts} +1 -1
- package/dist/{index-DHw08dUc.d.ts → index-mddpvrRX.d.ts} +1 -1
- package/dist/index.cjs +1233 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +226 -10
- package/dist/index.d.ts +226 -10
- package/dist/index.mjs +1230 -111
- package/dist/index.mjs.map +1 -1
- package/dist/lane/index.cjs +758 -110
- package/dist/lane/index.cjs.map +1 -1
- package/dist/lane/index.mjs +758 -110
- package/dist/lane/index.mjs.map +1 -1
- package/dist/match/index.d.cts +4 -4
- package/dist/match/index.d.ts +4 -4
- package/dist/pem/index.d.cts +2 -2
- package/dist/pem/index.d.ts +2 -2
- package/dist/pilot/index.d.cts +2 -2
- package/dist/pilot/index.d.ts +2 -2
- package/dist/precheck/index.d.cts +2 -2
- package/dist/precheck/index.d.ts +2 -2
- package/dist/produce/index.d.cts +2 -2
- package/dist/produce/index.d.ts +2 -2
- package/dist/sigh/index.d.cts +2 -2
- package/dist/sigh/index.d.ts +2 -2
- package/package.json +3 -1
- package/dist/index-DfirmFbC.d.ts +0 -32
- package/dist/index-lt819Jgj.d.cts +0 -32
package/dist/index.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { mkdir, writeFile, readFile, readdir, mkdtemp, unlink, stat, rm, copyFil
|
|
|
11
11
|
import emojiRegex from 'emoji-regex';
|
|
12
12
|
import * as plist5 from 'plist';
|
|
13
13
|
import plist5__default from 'plist';
|
|
14
|
+
import forge from 'node-forge';
|
|
14
15
|
import { Command } from 'commander';
|
|
15
16
|
|
|
16
17
|
var __defProp = Object.defineProperty;
|
|
@@ -219,9 +220,9 @@ Please accept the agreement at https://developer.apple.com/account before contin
|
|
|
219
220
|
throw new Error(msg);
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
|
-
async function
|
|
223
|
-
const log = config.onLog ??
|
|
224
|
-
|
|
223
|
+
async function beginLogin(config) {
|
|
224
|
+
const log = config.onLog ?? (() => {
|
|
225
|
+
});
|
|
225
226
|
log("Fetching Apple service key...");
|
|
226
227
|
const serviceKey = await fetchServiceKey();
|
|
227
228
|
const initRes = await fetch(`${AUTH_BASE}/signin?widgetKey=${serviceKey}`, {
|
|
@@ -238,27 +239,94 @@ async function loginWithAppleId(config) {
|
|
|
238
239
|
const cookieStr = Object.entries(initCookies).map(([k, v]) => k.startsWith("DES") ? `${k}="${v}"` : `${k}=${v}`).join("; ");
|
|
239
240
|
log("Starting SIRP Apple ID login...");
|
|
240
241
|
const response = await doSirp(config.email, config.password, serviceKey, cookieStr, hashcash);
|
|
241
|
-
const
|
|
242
|
+
const baseState = {
|
|
242
243
|
cookies: { ...initCookies, ...response.cookies },
|
|
243
244
|
sessionId: response.sessionId,
|
|
244
245
|
scnt: response.scnt,
|
|
245
246
|
serviceKey,
|
|
246
|
-
userEmail: config.email
|
|
247
|
-
pendingAgreements: []
|
|
247
|
+
userEmail: config.email
|
|
248
248
|
};
|
|
249
249
|
if (response.status === 403) {
|
|
250
250
|
throw new Error(`Invalid username and password combination. Used '${config.email}' as the username.`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
await fetchOlympusSession(session);
|
|
254
|
-
} else if (response.status === 409) {
|
|
255
|
-
await handleTwoStepOrFactor(session, log, promptFn);
|
|
256
|
-
await fetchOlympusSession(session);
|
|
257
|
-
} else if (response.status === 412) {
|
|
251
|
+
}
|
|
252
|
+
if (response.status === 412) {
|
|
258
253
|
throw new Error("Need to acknowledge Apple's Apple ID and Privacy statement. Please manually log into https://appleid.apple.com.");
|
|
254
|
+
}
|
|
255
|
+
if (response.status === 200) {
|
|
256
|
+
log("Login successful (no 2FA).");
|
|
257
|
+
const session = { ...baseState, pendingAgreements: [] };
|
|
258
|
+
await finalizeSession(session, log);
|
|
259
|
+
return { kind: "logged_in", session };
|
|
260
|
+
}
|
|
261
|
+
if (response.status === 409) {
|
|
262
|
+
log("2FA required, fetching challenge...");
|
|
263
|
+
const challenge = await fetchTwoFactorChallenge(baseState);
|
|
264
|
+
return { kind: "needs_2fa", pending: baseState, challenge };
|
|
265
|
+
}
|
|
266
|
+
throw new Error(`Unexpected login response (${response.status}): ${JSON.stringify(response.body).slice(0, 500)}`);
|
|
267
|
+
}
|
|
268
|
+
async function fetchTwoFactorChallenge(pending) {
|
|
269
|
+
const r = await fetch(`${AUTH_BASE}`, { headers: buildAuthHeadersFromPending(pending) });
|
|
270
|
+
const body = await r.json();
|
|
271
|
+
const trustedPhoneNumbers = (body.trustedPhoneNumbers ?? []).map((p) => ({
|
|
272
|
+
id: p.id,
|
|
273
|
+
numberWithDialCode: p.numberWithDialCode,
|
|
274
|
+
pushMode: p.pushMode
|
|
275
|
+
}));
|
|
276
|
+
const phoneOnly = body.noTrustedDevices === true;
|
|
277
|
+
const codeLength = body.securityCode?.length ?? 6;
|
|
278
|
+
return {
|
|
279
|
+
kind: phoneOnly ? "phone_only" : "device",
|
|
280
|
+
codeLength,
|
|
281
|
+
trustedPhoneNumbers
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function requestSmsCode(input) {
|
|
285
|
+
await fetch(`${AUTH_BASE}/verify/phone`, {
|
|
286
|
+
method: "PUT",
|
|
287
|
+
headers: { ...buildAuthHeadersFromPending(input.pending), "Content-Type": "application/json" },
|
|
288
|
+
body: JSON.stringify({ phoneNumber: { id: input.phoneId }, mode: input.mode ?? "sms" })
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async function submitTwoFactorCode(input) {
|
|
292
|
+
const log = input.onLog ?? (() => {
|
|
293
|
+
});
|
|
294
|
+
let path;
|
|
295
|
+
let body;
|
|
296
|
+
if (input.source === "device") {
|
|
297
|
+
path = `${AUTH_BASE}/verify/trusteddevice/securitycode`;
|
|
298
|
+
body = JSON.stringify({ securityCode: { code: input.code } });
|
|
259
299
|
} else {
|
|
260
|
-
throw new Error(
|
|
300
|
+
if (input.phoneId == null) throw new Error("phoneId is required when source='phone'");
|
|
301
|
+
path = `${AUTH_BASE}/verify/phone/securitycode`;
|
|
302
|
+
body = JSON.stringify({
|
|
303
|
+
securityCode: { code: input.code },
|
|
304
|
+
phoneNumber: { id: input.phoneId },
|
|
305
|
+
mode: input.mode ?? "sms"
|
|
306
|
+
});
|
|
261
307
|
}
|
|
308
|
+
log("Submitting 2FA code...");
|
|
309
|
+
const verifyRes = await fetch(path, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: { ...buildAuthHeadersFromPending(input.pending), "Content-Type": "application/json" },
|
|
312
|
+
body
|
|
313
|
+
});
|
|
314
|
+
if (!verifyRes.ok) {
|
|
315
|
+
const err = await verifyRes.json().catch(() => ({}));
|
|
316
|
+
const msgs = (err?.service_errors ?? err?.serviceErrors ?? []).map((e) => e.message).filter(Boolean);
|
|
317
|
+
throw new Error(msgs.join("; ") || `Verification failed (${verifyRes.status})`);
|
|
318
|
+
}
|
|
319
|
+
const session = {
|
|
320
|
+
...input.pending,
|
|
321
|
+
cookies: { ...input.pending.cookies, ...parseCookies(verifyRes.headers) },
|
|
322
|
+
pendingAgreements: []
|
|
323
|
+
};
|
|
324
|
+
await trustSession(session);
|
|
325
|
+
await finalizeSession(session, log);
|
|
326
|
+
return session;
|
|
327
|
+
}
|
|
328
|
+
async function finalizeSession(session, log) {
|
|
329
|
+
await fetchOlympusSession(session);
|
|
262
330
|
const agreements = await fetchContractMessages(session);
|
|
263
331
|
if (agreements.length > 0) {
|
|
264
332
|
for (const msg of agreements) {
|
|
@@ -267,7 +335,53 @@ async function loginWithAppleId(config) {
|
|
|
267
335
|
}
|
|
268
336
|
session.pendingAgreements = agreements;
|
|
269
337
|
log(`Logged in as ${session.userEmail}`);
|
|
270
|
-
|
|
338
|
+
}
|
|
339
|
+
async function loginWithAppleId(config) {
|
|
340
|
+
const log = config.onLog ?? console.log;
|
|
341
|
+
const promptFn = config.onPrompt ?? defaultPrompt;
|
|
342
|
+
const result = await beginLogin({ email: config.email, password: config.password, onLog: log });
|
|
343
|
+
if (result.kind === "logged_in") return result.session;
|
|
344
|
+
const { pending, challenge } = result;
|
|
345
|
+
const phones = challenge.trustedPhoneNumbers;
|
|
346
|
+
if (challenge.kind === "device") {
|
|
347
|
+
log("Two-factor Authentication (6 digit code) is enabled for this account");
|
|
348
|
+
log("(Input 'sms' to escape this prompt and select a trusted phone number to send the code as a text message)");
|
|
349
|
+
const input = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
|
|
350
|
+
if (input.toLowerCase() === "sms") {
|
|
351
|
+
log("Please select a trusted phone number:");
|
|
352
|
+
for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
|
|
353
|
+
const choice2 = await promptFn("Enter number: ");
|
|
354
|
+
const phone2 = phones[Math.max(0, parseInt(choice2, 10) - 1)] ?? phones[0];
|
|
355
|
+
await requestSmsCode({ pending, phoneId: phone2.id, mode: phone2.pushMode });
|
|
356
|
+
log(`Code sent to ${phone2.numberWithDialCode}`);
|
|
357
|
+
const code2 = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
|
|
358
|
+
return submitTwoFactorCode({ pending, code: code2, source: "phone", phoneId: phone2.id, mode: phone2.pushMode ?? "sms", onLog: log });
|
|
359
|
+
}
|
|
360
|
+
return submitTwoFactorCode({ pending, code: input, source: "device", onLog: log });
|
|
361
|
+
}
|
|
362
|
+
log("Two-factor Authentication (6 digit code) is enabled for this account");
|
|
363
|
+
if (phones.length === 1) {
|
|
364
|
+
const phone2 = phones[0];
|
|
365
|
+
log(`Code automatically sent to ${phone2.numberWithDialCode}`);
|
|
366
|
+
const code2 = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
|
|
367
|
+
return submitTwoFactorCode({ pending, code: code2, source: "phone", phoneId: phone2.id, mode: phone2.pushMode ?? "sms", onLog: log });
|
|
368
|
+
}
|
|
369
|
+
log("Please select a trusted phone number:");
|
|
370
|
+
for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
|
|
371
|
+
const choice = await promptFn("Enter number: ");
|
|
372
|
+
const phone = phones[Math.max(0, parseInt(choice, 10) - 1)] ?? phones[0];
|
|
373
|
+
await requestSmsCode({ pending, phoneId: phone.id, mode: phone.pushMode });
|
|
374
|
+
log(`Code sent to ${phone.numberWithDialCode}`);
|
|
375
|
+
const code = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
|
|
376
|
+
return submitTwoFactorCode({ pending, code, source: "phone", phoneId: phone.id, mode: phone.pushMode ?? "sms", onLog: log });
|
|
377
|
+
}
|
|
378
|
+
function buildAuthHeadersFromPending(pending) {
|
|
379
|
+
const h = { "X-Apple-Widget-Key": pending.serviceKey, Accept: "application/json" };
|
|
380
|
+
if (pending.sessionId) h["X-Apple-Id-Session-Id"] = pending.sessionId;
|
|
381
|
+
if (pending.scnt) h["scnt"] = pending.scnt;
|
|
382
|
+
const c = Object.entries(pending.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
383
|
+
if (c) h["Cookie"] = c;
|
|
384
|
+
return h;
|
|
271
385
|
}
|
|
272
386
|
async function doSirp(user, password, serviceKey, cookieStr, hashcash) {
|
|
273
387
|
const client = new SRPClient(2048);
|
|
@@ -323,99 +437,6 @@ async function doSirp(user, password, serviceKey, cookieStr, hashcash) {
|
|
|
323
437
|
scnt: completeRes.headers.get("scnt") ?? ""
|
|
324
438
|
};
|
|
325
439
|
}
|
|
326
|
-
async function handleTwoStepOrFactor(session, log, promptFn) {
|
|
327
|
-
const r = await fetch(`${AUTH_BASE}`, { headers: buildAuthHeaders(session) });
|
|
328
|
-
const body = await r.json();
|
|
329
|
-
if (body.trustedDevices?.length > 0) {
|
|
330
|
-
await handleTwoStep(session, body, log, promptFn);
|
|
331
|
-
} else if (body.trustedPhoneNumbers?.length > 0) {
|
|
332
|
-
await handleTwoFactor(session, body, log, promptFn);
|
|
333
|
-
} else {
|
|
334
|
-
throw new Error("2FA required but no trusted devices or phone numbers found.");
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
async function handleTwoFactor(session, authBody, log, promptFn) {
|
|
338
|
-
log("Two-factor Authentication (6 digit code) is enabled for this account");
|
|
339
|
-
const codeLength = authBody.securityCode?.length ?? 6;
|
|
340
|
-
const phones = authBody.trustedPhoneNumbers ?? [];
|
|
341
|
-
const noTrusted = authBody.noTrustedDevices === true;
|
|
342
|
-
let codeType;
|
|
343
|
-
let reqBody;
|
|
344
|
-
if (noTrusted && phones.length === 1) {
|
|
345
|
-
const phone = phones[0];
|
|
346
|
-
log(`Code automatically sent to ${phone.numberWithDialCode}`);
|
|
347
|
-
const code = await promptFn(`Please enter the ${codeLength} digit code you received at ${phone.numberWithDialCode}: `);
|
|
348
|
-
codeType = "phone";
|
|
349
|
-
reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
|
|
350
|
-
} else if (noTrusted) {
|
|
351
|
-
const phone = await choosePhone(phones, log, promptFn);
|
|
352
|
-
await requestPhoneCode(session, phone);
|
|
353
|
-
log(`Successfully requested text message to ${phone.numberWithDialCode}`);
|
|
354
|
-
const code = await promptFn(`Please enter the ${codeLength} digit code: `);
|
|
355
|
-
codeType = "phone";
|
|
356
|
-
reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
|
|
357
|
-
} else {
|
|
358
|
-
log("(Input 'sms' to escape this prompt and select a trusted phone number to send the code as a text message)");
|
|
359
|
-
const input = await promptFn(`Please enter the ${codeLength} digit code: `);
|
|
360
|
-
if (input.toLowerCase() === "sms") {
|
|
361
|
-
const phone = await choosePhone(phones, log, promptFn);
|
|
362
|
-
await requestPhoneCode(session, phone);
|
|
363
|
-
const code = await promptFn(`Please enter the ${codeLength} digit code sent to ${phone.numberWithDialCode}: `);
|
|
364
|
-
codeType = "phone";
|
|
365
|
-
reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
|
|
366
|
-
} else {
|
|
367
|
-
codeType = "trusteddevice";
|
|
368
|
-
reqBody = JSON.stringify({ securityCode: { code: input } });
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
log("Requesting session...");
|
|
372
|
-
const verifyRes = await fetch(`${AUTH_BASE}/verify/${codeType}/securitycode`, {
|
|
373
|
-
method: "POST",
|
|
374
|
-
headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
|
|
375
|
-
body: reqBody
|
|
376
|
-
});
|
|
377
|
-
if (!verifyRes.ok) {
|
|
378
|
-
const err = await verifyRes.json().catch(() => ({}));
|
|
379
|
-
const msgs = (err?.service_errors ?? err?.serviceErrors ?? []).map((e) => e.message).filter(Boolean);
|
|
380
|
-
throw new Error(msgs.join("; ") || `Verification failed (${verifyRes.status})`);
|
|
381
|
-
}
|
|
382
|
-
Object.assign(session.cookies, parseCookies(verifyRes.headers));
|
|
383
|
-
await trustSession(session);
|
|
384
|
-
}
|
|
385
|
-
async function handleTwoStep(session, authBody, log, promptFn) {
|
|
386
|
-
log("Two-step Verification (4 digit code) is enabled for this account");
|
|
387
|
-
const devices = authBody.trustedDevices ?? [];
|
|
388
|
-
log("Please select a trusted device:");
|
|
389
|
-
for (let i = 0; i < devices.length; i++) {
|
|
390
|
-
log(` ${i + 1}. ${devices[i].name} ${devices[i].modelName ?? "SMS"} (${devices[i].id})`);
|
|
391
|
-
}
|
|
392
|
-
const choice = await promptFn("Enter device number: ");
|
|
393
|
-
const device = devices[Math.max(0, parseInt(choice, 10) - 1)] ?? devices[0];
|
|
394
|
-
await fetch(`${AUTH_BASE}/verify/device/${device.id}/securitycode`, { method: "PUT", headers: buildAuthHeaders(session) });
|
|
395
|
-
log("Successfully requested notification");
|
|
396
|
-
const code = await promptFn("Please enter the 4 digit code: ");
|
|
397
|
-
const verifyRes = await fetch(`${AUTH_BASE}/verify/phone/securitycode`, {
|
|
398
|
-
method: "POST",
|
|
399
|
-
headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
|
|
400
|
-
body: JSON.stringify({ phoneNumber: { id: device.id }, securityCode: { code }, mode: "sms" })
|
|
401
|
-
});
|
|
402
|
-
if (!verifyRes.ok) throw new Error("Incorrect verification code.");
|
|
403
|
-
Object.assign(session.cookies, parseCookies(verifyRes.headers));
|
|
404
|
-
await trustSession(session);
|
|
405
|
-
}
|
|
406
|
-
async function choosePhone(phones, log, promptFn) {
|
|
407
|
-
log("Please select a trusted phone number:");
|
|
408
|
-
for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
|
|
409
|
-
const choice = await promptFn("Enter number: ");
|
|
410
|
-
return phones[Math.max(0, parseInt(choice, 10) - 1)] ?? phones[0];
|
|
411
|
-
}
|
|
412
|
-
async function requestPhoneCode(session, phone) {
|
|
413
|
-
await fetch(`${AUTH_BASE}/verify/phone`, {
|
|
414
|
-
method: "PUT",
|
|
415
|
-
headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
|
|
416
|
-
body: JSON.stringify({ phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" })
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
440
|
async function trustSession(session) {
|
|
420
441
|
const r = await fetch(`${AUTH_BASE}/2sv/trust`, { headers: buildAuthHeaders(session) });
|
|
421
442
|
Object.assign(session.cookies, parseCookies(r.headers));
|
|
@@ -2455,6 +2476,12 @@ var init_xcode_metrics = __esm({
|
|
|
2455
2476
|
});
|
|
2456
2477
|
|
|
2457
2478
|
// src/client/portal.ts
|
|
2479
|
+
function pickUniqueKeyName(base, taken) {
|
|
2480
|
+
if (!taken.has(base)) return base;
|
|
2481
|
+
let n = 1;
|
|
2482
|
+
while (taken.has(`${base} - ${n}`)) n++;
|
|
2483
|
+
return `${base} - ${n}`;
|
|
2484
|
+
}
|
|
2458
2485
|
var PORTAL_BASE, PortalClient;
|
|
2459
2486
|
var init_portal = __esm({
|
|
2460
2487
|
"src/client/portal.ts"() {
|
|
@@ -2475,6 +2502,279 @@ var init_portal = __esm({
|
|
|
2475
2502
|
async init() {
|
|
2476
2503
|
await this.post("account/listTeams.action", {});
|
|
2477
2504
|
}
|
|
2505
|
+
/**
|
|
2506
|
+
* JSON POST — different content type than the standard form-encoded
|
|
2507
|
+
* `post()`. Used by the App Store Connect API key endpoints
|
|
2508
|
+
* (`account/auth/key/v2/create`), which Spaceship discovered take a
|
|
2509
|
+
* JSON body even though every other portal mutation is form-encoded.
|
|
2510
|
+
*
|
|
2511
|
+
* Like the form-encoded `post()`, this also runs the response through
|
|
2512
|
+
* `checkPortalResponse` — Apple returns HTTP 200 even for errors and
|
|
2513
|
+
* encodes the error in `resultCode`/`userString` fields, so a raw
|
|
2514
|
+
* status check would silently miss real failures.
|
|
2515
|
+
*/
|
|
2516
|
+
async postJson(path, body) {
|
|
2517
|
+
const headers = {
|
|
2518
|
+
Cookie: this.cookieStr,
|
|
2519
|
+
"Content-Type": "application/json",
|
|
2520
|
+
Accept: "application/json"
|
|
2521
|
+
};
|
|
2522
|
+
if (this.csrf) {
|
|
2523
|
+
headers["csrf"] = this.csrf;
|
|
2524
|
+
headers["csrf_ts"] = this.csrfTs;
|
|
2525
|
+
}
|
|
2526
|
+
console.log(`[portal] POST(json) ${path}`);
|
|
2527
|
+
const res = await fetch(`${PORTAL_BASE}/${path}`, {
|
|
2528
|
+
method: "POST",
|
|
2529
|
+
headers,
|
|
2530
|
+
body: JSON.stringify(body)
|
|
2531
|
+
});
|
|
2532
|
+
if (res.headers.get("csrf")) {
|
|
2533
|
+
this.csrf = res.headers.get("csrf");
|
|
2534
|
+
this.csrfTs = res.headers.get("csrf_ts") ?? "";
|
|
2535
|
+
}
|
|
2536
|
+
for (const raw of res.headers.getSetCookie?.() ?? []) {
|
|
2537
|
+
const m = raw.match(/^([^=]+)=([^;]*)/);
|
|
2538
|
+
if (m && !this.cookieStr.includes(m[1] + "=")) this.cookieStr += "; " + m[1] + "=" + m[2];
|
|
2539
|
+
}
|
|
2540
|
+
const text = await res.text();
|
|
2541
|
+
let data = null;
|
|
2542
|
+
try {
|
|
2543
|
+
data = text ? JSON.parse(text) : null;
|
|
2544
|
+
} catch {
|
|
2545
|
+
}
|
|
2546
|
+
console.log(`[portal] POST(json) ${path} \u2192 ${res.status} ${text.slice(0, 400)}`);
|
|
2547
|
+
checkPortalResponse(data, this.session);
|
|
2548
|
+
return { status: res.status, body: data, rawText: text };
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* GET helper for the portal endpoints that take query parameters
|
|
2552
|
+
* (currently just `account/auth/key/download`). Returns the raw text
|
|
2553
|
+
* since download endpoints can return PEMs or JSON depending on Apple's
|
|
2554
|
+
* mood — caller decides how to parse.
|
|
2555
|
+
*/
|
|
2556
|
+
async getRaw(path, params = {}) {
|
|
2557
|
+
const url = new URL(`${PORTAL_BASE}/${path}`);
|
|
2558
|
+
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
2559
|
+
const headers = {
|
|
2560
|
+
Cookie: this.cookieStr,
|
|
2561
|
+
Accept: "application/json, text/plain, application/x-pem-file, */*"
|
|
2562
|
+
};
|
|
2563
|
+
if (this.csrf) {
|
|
2564
|
+
headers["csrf"] = this.csrf;
|
|
2565
|
+
headers["csrf_ts"] = this.csrfTs;
|
|
2566
|
+
}
|
|
2567
|
+
console.log(`[portal] GET ${url.pathname}${url.search}`);
|
|
2568
|
+
const res = await fetch(url.toString(), { headers });
|
|
2569
|
+
for (const raw of res.headers.getSetCookie?.() ?? []) {
|
|
2570
|
+
const m = raw.match(/^([^=]+)=([^;]*)/);
|
|
2571
|
+
if (m && !this.cookieStr.includes(m[1] + "=")) this.cookieStr += "; " + m[1] + "=" + m[2];
|
|
2572
|
+
}
|
|
2573
|
+
const text = await res.text();
|
|
2574
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
2575
|
+
console.log(`[portal] GET ${url.pathname} \u2192 ${res.status} ${contentType} ${text.length}b`);
|
|
2576
|
+
return { status: res.status, rawText: text, contentType };
|
|
2577
|
+
}
|
|
2578
|
+
// ── App Store Connect API Keys (.p8) ────────────────────────────────────
|
|
2579
|
+
/**
|
|
2580
|
+
* Create an App Store Connect API key via the developer portal's
|
|
2581
|
+
* `account/auth/key/v2/create` endpoint. Reverse-engineered from the
|
|
2582
|
+
* _current_ @expo/apple-utils npm package (v2.1.21), which is what
|
|
2583
|
+
* EAS CLI uses to manage Apple credentials in production.
|
|
2584
|
+
*
|
|
2585
|
+
* This differs from older published reverse-engineering guides:
|
|
2586
|
+
* • URL uses the `/v2/create` suffix (Apple kept it)
|
|
2587
|
+
* • Field is `serviceConfigurations`, shape is an object keyed by
|
|
2588
|
+
* service id (NOT `serviceConfigurationsRequests: [...]` array)
|
|
2589
|
+
* • Body REQUIRES `scope: "team"`
|
|
2590
|
+
* • Needs a scope-specific CSRF warmup via the `keys` endpoint
|
|
2591
|
+
* before the create call (EAS's `ensureCSRFAsync("keys", …)`)
|
|
2592
|
+
*
|
|
2593
|
+
* For an App Store Connect API key (PUBLIC_API, used by altool for
|
|
2594
|
+
* TestFlight uploads), `serviceConfigurations` is an empty object `{}`.
|
|
2595
|
+
* For an APNs auth key, it's `{ "U27F4V844T": [] }`. For MusicKit:
|
|
2596
|
+
* `{ "6A7HVUVQ3M": [musicId] }`, etc.
|
|
2597
|
+
*
|
|
2598
|
+
* Apple's name validator rejects parentheses, brackets, and most
|
|
2599
|
+
* punctuation — sanitize to alphanumeric + space + dash + underscore
|
|
2600
|
+
* + period and collapse whitespace.
|
|
2601
|
+
*/
|
|
2602
|
+
async createApiKey(rawName, serviceConfigurations = {}) {
|
|
2603
|
+
const baseName = rawName.replace(/[^A-Za-z0-9 _\-.]/g, " ").replace(/\s+/g, " ").trim().slice(0, 45);
|
|
2604
|
+
if (!baseName) throw new Error("createApiKey: name is empty after sanitization");
|
|
2605
|
+
let existingNames = /* @__PURE__ */ new Set();
|
|
2606
|
+
try {
|
|
2607
|
+
const list = await this.listApiKeys();
|
|
2608
|
+
existingNames = new Set(
|
|
2609
|
+
(list.keys ?? []).map((k) => String(k.keyName ?? "").trim())
|
|
2610
|
+
);
|
|
2611
|
+
} catch {
|
|
2612
|
+
}
|
|
2613
|
+
const name = pickUniqueKeyName(baseName, existingNames);
|
|
2614
|
+
const body = {
|
|
2615
|
+
teamId: this.teamId,
|
|
2616
|
+
name,
|
|
2617
|
+
serviceConfigurations,
|
|
2618
|
+
scope: "team"
|
|
2619
|
+
};
|
|
2620
|
+
const result = await this.postJson("account/auth/key/v2/create", body);
|
|
2621
|
+
const created = result.body?.keys?.[0] ?? result.body?.key ?? result.body?.data ?? result.body;
|
|
2622
|
+
const keyId = created?.keyId ?? created?.id ?? created?.kid ?? created?.attributes?.kid;
|
|
2623
|
+
if (!keyId) {
|
|
2624
|
+
throw new Error(`createApiKey: no key id in response. Body: ${result.rawText.slice(0, 400)}`);
|
|
2625
|
+
}
|
|
2626
|
+
return { keyId, raw: created };
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* Fetch a single API key by id via `account/auth/key/get`. Reverse-
|
|
2630
|
+
* engineered from Spaceship's `get_key`. Returns the raw key record so
|
|
2631
|
+
* the caller can extract whatever fields it needs (e.g. issuerId).
|
|
2632
|
+
*
|
|
2633
|
+
* Form-encoded POST — same shape as every other PortalClient mutation
|
|
2634
|
+
* except the JSON `create` endpoint.
|
|
2635
|
+
*/
|
|
2636
|
+
async getApiKey(keyId) {
|
|
2637
|
+
const data = await this.post("account/auth/key/get", {
|
|
2638
|
+
teamId: this.teamId,
|
|
2639
|
+
keyId
|
|
2640
|
+
});
|
|
2641
|
+
return data?.keys?.[0] ?? data?.key ?? data?.data ?? data;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Revoke an existing key. Form-encoded POST per Spaceship's
|
|
2645
|
+
* `revoke_key!`. Used for cleaning up orphan keys created during
|
|
2646
|
+
* failed wizard runs.
|
|
2647
|
+
*/
|
|
2648
|
+
async revokeApiKey(keyId) {
|
|
2649
|
+
await this.post("account/auth/key/revoke", {
|
|
2650
|
+
teamId: this.teamId,
|
|
2651
|
+
keyId
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
// ── APNs Authentication Keys (.p8) ──────────────────────────────────────
|
|
2655
|
+
/** Apple's hardcoded service id for APNs auth keys (from Spaceship). */
|
|
2656
|
+
static APNS_SERVICE_ID = "U27F4V844T";
|
|
2657
|
+
/**
|
|
2658
|
+
* Create an APNs Authentication Key (.p8) via the developer portal.
|
|
2659
|
+
*
|
|
2660
|
+
* Apple requires BOTH `serviceConfigurations` (the map) AND
|
|
2661
|
+
* `serviceConfigurationsRequests` (the array of full config objects)
|
|
2662
|
+
* in the v2/create body. Just sending the map alone results in
|
|
2663
|
+
* "No value was provided for the parameter 'serviceConfigurations'".
|
|
2664
|
+
*
|
|
2665
|
+
* The exact body comes from @expo/apple-utils v2.1.21 — both fields
|
|
2666
|
+
* are produced by separate functions (`d` for the map, `h` for the
|
|
2667
|
+
* array) and merged into the create payload.
|
|
2668
|
+
*
|
|
2669
|
+
* Apple also rejects duplicate key names ("Another key with same
|
|
2670
|
+
* name exists") — we dedupe by listing existing keys first and
|
|
2671
|
+
* appending " (N)" until we find a free slot, mirroring the
|
|
2672
|
+
* provisioning profile dedup.
|
|
2673
|
+
*/
|
|
2674
|
+
async createApnsAuthKey(rawName) {
|
|
2675
|
+
const baseName = rawName.replace(/[^A-Za-z0-9 _\-.]/g, " ").replace(/\s+/g, " ").trim().slice(0, 45);
|
|
2676
|
+
if (!baseName) throw new Error("createApnsAuthKey: name is empty after sanitization");
|
|
2677
|
+
let existingNames = /* @__PURE__ */ new Set();
|
|
2678
|
+
try {
|
|
2679
|
+
const list = await this.listApiKeys();
|
|
2680
|
+
existingNames = new Set(
|
|
2681
|
+
(list.keys ?? []).map((k) => String(k.keyName ?? "").trim())
|
|
2682
|
+
);
|
|
2683
|
+
} catch {
|
|
2684
|
+
}
|
|
2685
|
+
const name = pickUniqueKeyName(baseName, existingNames);
|
|
2686
|
+
const body = {
|
|
2687
|
+
teamId: this.teamId,
|
|
2688
|
+
name,
|
|
2689
|
+
serviceConfigurations: {
|
|
2690
|
+
[_PortalClient.APNS_SERVICE_ID]: []
|
|
2691
|
+
},
|
|
2692
|
+
serviceConfigurationsRequests: [
|
|
2693
|
+
{
|
|
2694
|
+
isNew: true,
|
|
2695
|
+
serviceId: _PortalClient.APNS_SERVICE_ID,
|
|
2696
|
+
identifiers: {},
|
|
2697
|
+
environment: "all",
|
|
2698
|
+
scope: "team"
|
|
2699
|
+
}
|
|
2700
|
+
],
|
|
2701
|
+
scope: "team"
|
|
2702
|
+
};
|
|
2703
|
+
const result = await this.postJson("account/auth/key/v2/create", body);
|
|
2704
|
+
const created = result.body?.keys?.[0];
|
|
2705
|
+
if (!created?.keyId) {
|
|
2706
|
+
throw new Error(`createApnsAuthKey: no key id in response. Body: ${result.rawText.slice(0, 400)}`);
|
|
2707
|
+
}
|
|
2708
|
+
const pem = await this.downloadApiKey(created.keyId);
|
|
2709
|
+
return { keyId: created.keyId, pem, raw: created };
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Find existing APNs auth keys in the team. Used by the wizard to
|
|
2713
|
+
* detect "you already have one" cases and offer to reuse rather than
|
|
2714
|
+
* proliferate keys.
|
|
2715
|
+
*/
|
|
2716
|
+
async listApnsAuthKeys() {
|
|
2717
|
+
const list = await this.listApiKeys();
|
|
2718
|
+
return (list.keys ?? []).filter(
|
|
2719
|
+
(k) => (k.services ?? []).some((s) => s.id === _PortalClient.APNS_SERVICE_ID || /apns/i.test(s.name ?? ""))
|
|
2720
|
+
).map((k) => ({
|
|
2721
|
+
keyId: k.keyId,
|
|
2722
|
+
keyName: k.keyName,
|
|
2723
|
+
canDownload: Boolean(k.canDownload)
|
|
2724
|
+
}));
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Download an existing App Store Connect API key as PEM. Spaceship's
|
|
2728
|
+
* `download_key` endpoint — GET with teamId + keyId in query string.
|
|
2729
|
+
* Returns the raw .p8 PEM text. Apple shows the .p8 exactly once
|
|
2730
|
+
* (after creation), so call this immediately after createApiKey.
|
|
2731
|
+
*/
|
|
2732
|
+
async downloadApiKey(keyId) {
|
|
2733
|
+
const { status, rawText, contentType } = await this.getRaw("account/auth/key/download", {
|
|
2734
|
+
teamId: this.teamId,
|
|
2735
|
+
keyId
|
|
2736
|
+
});
|
|
2737
|
+
if (status !== 200) {
|
|
2738
|
+
throw new Error(`downloadApiKey failed (${status}): ${rawText.slice(0, 200)}`);
|
|
2739
|
+
}
|
|
2740
|
+
if (rawText.includes("BEGIN PRIVATE KEY")) return rawText;
|
|
2741
|
+
if (contentType.includes("json")) {
|
|
2742
|
+
try {
|
|
2743
|
+
const parsed = JSON.parse(rawText);
|
|
2744
|
+
const candidates = [
|
|
2745
|
+
parsed?.privateKey,
|
|
2746
|
+
parsed?.key?.privateKey,
|
|
2747
|
+
parsed?.keys?.[0]?.privateKey,
|
|
2748
|
+
parsed?.data?.attributes?.privateKey,
|
|
2749
|
+
parsed?.value
|
|
2750
|
+
];
|
|
2751
|
+
for (const c of candidates) {
|
|
2752
|
+
if (typeof c === "string" && c.includes("BEGIN PRIVATE KEY")) return c;
|
|
2753
|
+
}
|
|
2754
|
+
} catch {
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
throw new Error(`downloadApiKey returned no PEM. Content-Type=${contentType} body=${rawText.slice(0, 300)}`);
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* List existing API keys via the portal. Used to discover the team's
|
|
2761
|
+
* issuer id — every key in the team shares the same issuer id, and
|
|
2762
|
+
* altool needs it alongside the key id and .p8 file.
|
|
2763
|
+
*
|
|
2764
|
+
* Form-encoded POST (NOT JSON), with pagination params, per Spaceship's
|
|
2765
|
+
* `list_keys` method. JSON returned 415 from this endpoint.
|
|
2766
|
+
*/
|
|
2767
|
+
async listApiKeys() {
|
|
2768
|
+
const data = await this.post("account/auth/key/list", {
|
|
2769
|
+
teamId: this.teamId,
|
|
2770
|
+
pageNumber: "1",
|
|
2771
|
+
pageSize: "500",
|
|
2772
|
+
sort: "name=asc"
|
|
2773
|
+
});
|
|
2774
|
+
const keys = data?.keys ?? [];
|
|
2775
|
+
const issuerId = data?.issuerId ?? data?.issuer ?? data?.team?.issuerId ?? keys[0]?.issuerId ?? keys[0]?.issuer;
|
|
2776
|
+
return { keys, issuerId };
|
|
2777
|
+
}
|
|
2478
2778
|
async post(path, params) {
|
|
2479
2779
|
const body = new URLSearchParams();
|
|
2480
2780
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -2542,6 +2842,31 @@ var init_portal = __esm({
|
|
|
2542
2842
|
async deleteAppId(appIdId) {
|
|
2543
2843
|
await this.post("account/ios/identifiers/deleteAppId.action", { teamId: this.teamId, appIdId });
|
|
2544
2844
|
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Toggle a capability (a.k.a. "service") on a bundle ID. Mirrors
|
|
2847
|
+
* Spaceship::Portal's `update_service` — the legacy portal endpoint
|
|
2848
|
+
* still works and is the only session-cookie-authenticated way to flip
|
|
2849
|
+
* capabilities like Push Notifications and Sign in with Apple from
|
|
2850
|
+
* outside the Apple Developer web UI.
|
|
2851
|
+
*
|
|
2852
|
+
* Common service codes (from Spaceship portal/portal_client.rb):
|
|
2853
|
+
* push → push notifications
|
|
2854
|
+
* APG3427HIY → Sign in with Apple
|
|
2855
|
+
* IAD53UNK2F → in-app purchase
|
|
2856
|
+
* homeKit → HomeKit
|
|
2857
|
+
* healthKit → HealthKit
|
|
2858
|
+
*
|
|
2859
|
+
* @param featureType Apple's internal capability key (string code or short name)
|
|
2860
|
+
* @param enabled true → turn on, false → turn off
|
|
2861
|
+
*/
|
|
2862
|
+
async setBundleCapability(params) {
|
|
2863
|
+
await this.post("account/ios/identifiers/updateAppService.action", {
|
|
2864
|
+
teamId: this.teamId,
|
|
2865
|
+
displayId: params.appIdId,
|
|
2866
|
+
featureType: params.featureType,
|
|
2867
|
+
featureValue: params.enabled ? "on" : "off"
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2545
2870
|
// ── Certificates ───────────────────────────────────────────────────────
|
|
2546
2871
|
async listCertificates(types) {
|
|
2547
2872
|
const iosTypes = types ?? [
|
|
@@ -2559,12 +2884,21 @@ var init_portal = __esm({
|
|
|
2559
2884
|
return data.certRequests ?? [];
|
|
2560
2885
|
}
|
|
2561
2886
|
async listPushCertificates() {
|
|
2887
|
+
const types = Array.from(
|
|
2888
|
+
/* @__PURE__ */ new Set([
|
|
2889
|
+
_PortalClient.CERT_TYPES.IOS_PRODUCTION_PUSH,
|
|
2890
|
+
_PortalClient.CERT_TYPES.IOS_DEVELOPMENT_PUSH,
|
|
2891
|
+
// Legacy push codes from OLDER_IOS_CERTIFICATE_TYPES
|
|
2892
|
+
"3BQKVH9I2X",
|
|
2893
|
+
"BKLRAVXMGM"
|
|
2894
|
+
])
|
|
2895
|
+
);
|
|
2562
2896
|
const data = await this.post("account/ios/certificate/listCertRequests.action", {
|
|
2563
2897
|
teamId: this.teamId,
|
|
2564
2898
|
pageSize: "500",
|
|
2565
2899
|
pageNumber: "1",
|
|
2566
2900
|
sort: "certRequestStatusCode=asc",
|
|
2567
|
-
types:
|
|
2901
|
+
types: types.join(",")
|
|
2568
2902
|
});
|
|
2569
2903
|
return data.certRequests ?? [];
|
|
2570
2904
|
}
|
|
@@ -2706,18 +3040,332 @@ var init_portal = __esm({
|
|
|
2706
3040
|
}
|
|
2707
3041
|
});
|
|
2708
3042
|
|
|
3043
|
+
// src/client/iris.ts
|
|
3044
|
+
var IRIS_BASE, IrisClient, IrisAuthError;
|
|
3045
|
+
var init_iris = __esm({
|
|
3046
|
+
"src/client/iris.ts"() {
|
|
3047
|
+
IRIS_BASE = "https://appstoreconnect.apple.com/iris/v1";
|
|
3048
|
+
IrisClient = class {
|
|
3049
|
+
constructor(session) {
|
|
3050
|
+
this.session = session;
|
|
3051
|
+
this.cookieStr = Object.entries(session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
3052
|
+
}
|
|
3053
|
+
session;
|
|
3054
|
+
cookieStr;
|
|
3055
|
+
buildHeaders() {
|
|
3056
|
+
return {
|
|
3057
|
+
Cookie: this.cookieStr,
|
|
3058
|
+
Accept: "application/vnd.api+json, application/json, text/plain, */*",
|
|
3059
|
+
"Content-Type": "application/vnd.api+json",
|
|
3060
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
3061
|
+
// Mimic the App Store Connect web UI's user agent
|
|
3062
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
/**
|
|
3066
|
+
* Issue a request and absorb any new cookies (Apple rotates session
|
|
3067
|
+
* cookies on a lot of iris calls).
|
|
3068
|
+
*/
|
|
3069
|
+
async request(method, path, body) {
|
|
3070
|
+
const headers = this.buildHeaders();
|
|
3071
|
+
if (process.env.FASTNODE_DEBUG) {
|
|
3072
|
+
console.log(`[iris] ${method} ${path}${body ? ` body=${JSON.stringify(body).slice(0, 200)}` : ""}`);
|
|
3073
|
+
}
|
|
3074
|
+
const res = await fetch(`${IRIS_BASE}${path}`, {
|
|
3075
|
+
method,
|
|
3076
|
+
headers,
|
|
3077
|
+
body: body ? JSON.stringify(body) : void 0
|
|
3078
|
+
});
|
|
3079
|
+
for (const raw of res.headers.getSetCookie?.() ?? []) {
|
|
3080
|
+
const m = raw.match(/^([^=]+)=([^;]*)/);
|
|
3081
|
+
if (m) {
|
|
3082
|
+
this.session.cookies[m[1]] = m[2];
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
this.cookieStr = Object.entries(this.session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
3086
|
+
const text = await res.text();
|
|
3087
|
+
let parsed = null;
|
|
3088
|
+
try {
|
|
3089
|
+
parsed = text ? JSON.parse(text) : null;
|
|
3090
|
+
} catch {
|
|
3091
|
+
}
|
|
3092
|
+
if (process.env.FASTNODE_DEBUG) {
|
|
3093
|
+
console.log(`[iris] \u2192 ${res.status} ${text.slice(0, 300)}`);
|
|
3094
|
+
}
|
|
3095
|
+
return { status: res.status, body: parsed, rawText: text };
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* List existing API keys. Used both as a sanity check (the user has
|
|
3099
|
+
* permission) and to discover the team's issuer id (which is the same
|
|
3100
|
+
* for every key in the team and is needed by altool).
|
|
3101
|
+
*
|
|
3102
|
+
* Important: Apple's iris endpoint rejects most query parameters with
|
|
3103
|
+
* `PARAMETER_ERROR.INVALID`. Call with no params and let the API return
|
|
3104
|
+
* its default page.
|
|
3105
|
+
*/
|
|
3106
|
+
async listApiKeys() {
|
|
3107
|
+
const { status, body, rawText } = await this.request("GET", "/apiKeys");
|
|
3108
|
+
if (status === 403) {
|
|
3109
|
+
throw new IrisAuthError(
|
|
3110
|
+
"You don't have permission to manage App Store Connect API keys (this requires the Account Holder or Admin role)."
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
if (status >= 400 || !body) {
|
|
3114
|
+
throw new Error(`iris listApiKeys failed (${status}): ${rawText.slice(0, 200)}`);
|
|
3115
|
+
}
|
|
3116
|
+
const issuerId = body?.meta?.issuerId ?? body?.data?.[0]?.attributes?.issuerId;
|
|
3117
|
+
const keys = (body.data ?? []).map((d) => ({
|
|
3118
|
+
id: d.id,
|
|
3119
|
+
kid: d.attributes?.kid ?? d.id,
|
|
3120
|
+
nickname: d.attributes?.nickname ?? "",
|
|
3121
|
+
issuerId: d.attributes?.issuerId ?? issuerId,
|
|
3122
|
+
roles: d.attributes?.roles ?? [],
|
|
3123
|
+
allAppsVisible: d.attributes?.allAppsVisible ?? true
|
|
3124
|
+
}));
|
|
3125
|
+
return { keys, issuerId };
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* Create a new API key. Apple returns the .p8 PEM exactly once in the
|
|
3129
|
+
* create response — capture it immediately or the key is dead weight.
|
|
3130
|
+
*
|
|
3131
|
+
* `keyType` is required by Apple's iris API:
|
|
3132
|
+
* • PUBLIC_API — the standard team-wide API key (what TestFlight uploads need)
|
|
3133
|
+
* • MARKETING_API — for App Analytics access only
|
|
3134
|
+
*/
|
|
3135
|
+
async createApiKey(input) {
|
|
3136
|
+
const requestBody = {
|
|
3137
|
+
data: {
|
|
3138
|
+
type: "apiKeys",
|
|
3139
|
+
attributes: {
|
|
3140
|
+
nickname: input.nickname,
|
|
3141
|
+
roles: input.roles ?? ["APP_MANAGER"],
|
|
3142
|
+
allAppsVisible: input.allAppsVisible ?? true,
|
|
3143
|
+
keyType: input.keyType ?? "PUBLIC_API"
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
console.log("[iris] createApiKey request:", JSON.stringify(requestBody));
|
|
3148
|
+
const { status, body, rawText } = await this.request("POST", "/apiKeys", requestBody);
|
|
3149
|
+
console.log(`[iris] createApiKey response: ${status} ${rawText.slice(0, 800)}`);
|
|
3150
|
+
if (status === 403) {
|
|
3151
|
+
throw new IrisAuthError(
|
|
3152
|
+
"You don't have permission to create App Store Connect API keys (this requires the Account Holder or Admin role)."
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
if (status >= 400 || !body) {
|
|
3156
|
+
const msg = body?.errors?.[0]?.detail ?? body?.errors?.[0]?.title ?? rawText.slice(0, 300) ?? `iris createApiKey failed (${status})`;
|
|
3157
|
+
throw new Error(msg);
|
|
3158
|
+
}
|
|
3159
|
+
const data = body.data ?? body;
|
|
3160
|
+
if (!data?.id) {
|
|
3161
|
+
throw new Error(`iris createApiKey returned no key id. Raw response: ${rawText.slice(0, 400)}`);
|
|
3162
|
+
}
|
|
3163
|
+
const key = {
|
|
3164
|
+
id: data.id,
|
|
3165
|
+
kid: data.attributes?.kid ?? data.id,
|
|
3166
|
+
nickname: data.attributes?.nickname ?? input.nickname,
|
|
3167
|
+
privateKey: data.attributes?.privateKey,
|
|
3168
|
+
issuerId: data.attributes?.issuerId,
|
|
3169
|
+
roles: data.attributes?.roles ?? input.roles ?? ["APP_MANAGER"],
|
|
3170
|
+
allAppsVisible: data.attributes?.allAppsVisible ?? true
|
|
3171
|
+
};
|
|
3172
|
+
return key;
|
|
3173
|
+
}
|
|
3174
|
+
/**
|
|
3175
|
+
* Fetch the .p8 PEM AND the team's issuer id for a previously-created
|
|
3176
|
+
* ASC API key. Both come back in a single request via:
|
|
3177
|
+
*
|
|
3178
|
+
* GET /iris/v1/apiKeys/{id}?fields[apiKeys]=privateKey&include=provider
|
|
3179
|
+
*
|
|
3180
|
+
* Sparse fieldset requesting ONLY `privateKey` is the magic that makes
|
|
3181
|
+
* Apple actually include the (base64-encoded) PEM in the response —
|
|
3182
|
+
* the default view returns `privateKey: null`. The `include=provider`
|
|
3183
|
+
* pulls the team's contentProvider record into `included[]`, and its
|
|
3184
|
+
* `id` field IS the issuer id altool needs (a UUID like
|
|
3185
|
+
* `cfadaf94-44ca-4c2c-8fa6-ecc28d3c7d0d`).
|
|
3186
|
+
*
|
|
3187
|
+
* Reverse-engineered from @expo/apple-utils v2.1.21 (the package that
|
|
3188
|
+
* powers EAS CLI), specifically the `ApiKey.downloadAsync` method
|
|
3189
|
+
* combined with `ApiKey.infoAsync` reading `provider?.id`.
|
|
3190
|
+
*
|
|
3191
|
+
* Returns `{ pem, issuerId }`, or `null` if Apple won't return the
|
|
3192
|
+
* private key (typically because the key was already downloaded once).
|
|
3193
|
+
*/
|
|
3194
|
+
async fetchApiKeyPrivateKey(keyId) {
|
|
3195
|
+
try {
|
|
3196
|
+
const url = `${IRIS_BASE}/apiKeys/${keyId}?fields%5BapiKeys%5D=privateKey&include=provider`;
|
|
3197
|
+
const res = await fetch(url, {
|
|
3198
|
+
headers: {
|
|
3199
|
+
...this.buildHeaders(),
|
|
3200
|
+
Accept: "application/vnd.api+json, application/json, text/plain, */*"
|
|
3201
|
+
}
|
|
3202
|
+
});
|
|
3203
|
+
const text = await res.text();
|
|
3204
|
+
console.log(`[iris] fetchApiKeyPrivateKey \u2192 ${res.status} (${text.length} bytes)`);
|
|
3205
|
+
if (res.status !== 200) {
|
|
3206
|
+
console.log(`[iris] err: ${text.slice(0, 300)}`);
|
|
3207
|
+
return null;
|
|
3208
|
+
}
|
|
3209
|
+
const body = JSON.parse(text);
|
|
3210
|
+
const encoded = body?.data?.attributes?.privateKey;
|
|
3211
|
+
if (typeof encoded !== "string" || encoded.length === 0) {
|
|
3212
|
+
console.log(`[iris] no privateKey in response`);
|
|
3213
|
+
return null;
|
|
3214
|
+
}
|
|
3215
|
+
const pem = Buffer.from(encoded, "base64").toString("utf-8");
|
|
3216
|
+
if (!pem.includes("BEGIN PRIVATE KEY")) {
|
|
3217
|
+
console.log(`[iris] decoded body didn't contain PEM marker`);
|
|
3218
|
+
return null;
|
|
3219
|
+
}
|
|
3220
|
+
const provider = (body?.included ?? []).find((i) => i.type === "contentProviders");
|
|
3221
|
+
const issuerId = provider?.id;
|
|
3222
|
+
console.log(`[iris] \u2713 PEM ${pem.length} bytes, issuerId=${issuerId ?? "(missing)"}`);
|
|
3223
|
+
return { pem, issuerId };
|
|
3224
|
+
} catch (err) {
|
|
3225
|
+
console.log(`[iris] fetchApiKeyPrivateKey error: ${err.message}`);
|
|
3226
|
+
return null;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* @deprecated Kept as a fallback probe for debugging. The real path
|
|
3231
|
+
* is the sparse fieldset above — everything in this brute-force list
|
|
3232
|
+
* returned 404 during testing. Left here so we can re-verify if Apple
|
|
3233
|
+
* changes anything.
|
|
3234
|
+
*/
|
|
3235
|
+
async fetchApiKeyPrivateKeyBruteForce(keyId) {
|
|
3236
|
+
const attempts = [
|
|
3237
|
+
// Sparse fieldsets — JSON-API spec lets us request specific attributes.
|
|
3238
|
+
// Apple may exclude privateKey by default and return it only when asked.
|
|
3239
|
+
{ kind: "GET", path: `/apiKeys/${keyId}?fields[apiKeys]=privateKey,kid,issuerId,nickname,roles` },
|
|
3240
|
+
{ kind: "GET", path: `/apiKeys/${keyId}?include=privateKey` },
|
|
3241
|
+
{ kind: "GET", path: `/apiKeys/${keyId}?fields[apiKeys]=privateKey` },
|
|
3242
|
+
// Custom Accept headers — maybe Apple negotiates content type
|
|
3243
|
+
{ kind: "GET", path: `/apiKeys/${keyId}`, accept: "application/x-pem-file" },
|
|
3244
|
+
{ kind: "GET", path: `/apiKeys/${keyId}`, accept: "application/octet-stream" },
|
|
3245
|
+
{ kind: "GET", path: `/apiKeys/${keyId}/p8`, accept: "application/x-pem-file" },
|
|
3246
|
+
// POST variants — Apple sometimes uses POST to mint a download token
|
|
3247
|
+
{ kind: "POST", path: `/apiKeys/${keyId}/secrets`, body: {} },
|
|
3248
|
+
{ kind: "POST", path: `/apiKeys/${keyId}/regenerate`, body: {} },
|
|
3249
|
+
{ kind: "POST", path: `/apiKeys/${keyId}/p8`, body: {} },
|
|
3250
|
+
// Bare GET (last) — confirms canonical fields, in case my parser missed any
|
|
3251
|
+
{ kind: "GET", path: `/apiKeys/${keyId}` },
|
|
3252
|
+
// Different host — the legacy developer.apple.com user-and-access namespace
|
|
3253
|
+
{ kind: "ABSOLUTE", url: `https://appstoreconnect.apple.com/api/v1/apiKeys/${keyId}/p8` },
|
|
3254
|
+
{ kind: "ABSOLUTE", url: `https://appstoreconnect.apple.com/access/api/keys/${keyId}/download` }
|
|
3255
|
+
];
|
|
3256
|
+
for (const attempt of attempts) {
|
|
3257
|
+
try {
|
|
3258
|
+
let result;
|
|
3259
|
+
if (attempt.kind === "GET") {
|
|
3260
|
+
result = await this.requestWithHeaders("GET", attempt.path, void 0, {
|
|
3261
|
+
...attempt.accept ? { Accept: attempt.accept } : {}
|
|
3262
|
+
});
|
|
3263
|
+
console.log(`[iris] fetchApiKeyPrivateKey GET ${attempt.path}${attempt.accept ? ` accept=${attempt.accept}` : ""} \u2192 ${result.status} (${result.rawText.length} bytes)`);
|
|
3264
|
+
} else if (attempt.kind === "POST") {
|
|
3265
|
+
result = await this.request("POST", attempt.path, attempt.body);
|
|
3266
|
+
console.log(`[iris] fetchApiKeyPrivateKey POST ${attempt.path} \u2192 ${result.status} (${result.rawText.length} bytes)`);
|
|
3267
|
+
} else {
|
|
3268
|
+
result = await this.requestAbsolute("GET", attempt.url);
|
|
3269
|
+
console.log(`[iris] fetchApiKeyPrivateKey GET ${attempt.url} \u2192 ${result.status} (${result.rawText.length} bytes)`);
|
|
3270
|
+
}
|
|
3271
|
+
if (result.status === 200) {
|
|
3272
|
+
console.log(`[iris] body: ${result.rawText.slice(0, 1500)}`);
|
|
3273
|
+
}
|
|
3274
|
+
if (result.status !== 200) continue;
|
|
3275
|
+
if (result.rawText && result.rawText.includes("BEGIN PRIVATE KEY")) {
|
|
3276
|
+
console.log(`[iris] \u2713 found PEM (plain text response)`);
|
|
3277
|
+
return result.rawText;
|
|
3278
|
+
}
|
|
3279
|
+
if (result.body) {
|
|
3280
|
+
const candidates = [
|
|
3281
|
+
result.body?.data?.attributes?.privateKey,
|
|
3282
|
+
result.body?.data?.attributes?.p8,
|
|
3283
|
+
result.body?.data?.attributes?.privateKeyContent,
|
|
3284
|
+
result.body?.data?.attributes?.kpem,
|
|
3285
|
+
result.body?.data?.attributes?.secret,
|
|
3286
|
+
result.body?.data?.attributes?.keyContent,
|
|
3287
|
+
result.body?.privateKey,
|
|
3288
|
+
result.body?.p8,
|
|
3289
|
+
result.body?.value,
|
|
3290
|
+
result.body?.secret
|
|
3291
|
+
];
|
|
3292
|
+
for (const c of candidates) {
|
|
3293
|
+
if (typeof c === "string" && c.includes("BEGIN PRIVATE KEY")) {
|
|
3294
|
+
console.log(`[iris] \u2713 found PEM in JSON response`);
|
|
3295
|
+
return c;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
} catch (err) {
|
|
3300
|
+
console.log(`[iris] fetchApiKeyPrivateKey attempt failed: ${err.message}`);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
console.log(`[iris] fetchApiKeyPrivateKey: no working download path for key ${keyId}`);
|
|
3304
|
+
return null;
|
|
3305
|
+
}
|
|
3306
|
+
/** Internal: like request() but lets us override headers. */
|
|
3307
|
+
async requestWithHeaders(method, path, body, extraHeaders = {}) {
|
|
3308
|
+
const headers = { ...this.buildHeaders(), ...extraHeaders };
|
|
3309
|
+
const res = await fetch(`${IRIS_BASE}${path}`, {
|
|
3310
|
+
method,
|
|
3311
|
+
headers,
|
|
3312
|
+
body: body ? JSON.stringify(body) : void 0
|
|
3313
|
+
});
|
|
3314
|
+
for (const raw of res.headers.getSetCookie?.() ?? []) {
|
|
3315
|
+
const m = raw.match(/^([^=]+)=([^;]*)/);
|
|
3316
|
+
if (m) this.session.cookies[m[1]] = m[2];
|
|
3317
|
+
}
|
|
3318
|
+
this.cookieStr = Object.entries(this.session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
3319
|
+
const text = await res.text();
|
|
3320
|
+
let parsed = null;
|
|
3321
|
+
try {
|
|
3322
|
+
parsed = text ? JSON.parse(text) : null;
|
|
3323
|
+
} catch {
|
|
3324
|
+
}
|
|
3325
|
+
return { status: res.status, body: parsed, rawText: text };
|
|
3326
|
+
}
|
|
3327
|
+
/** Internal: fetch an absolute URL with the session cookies. */
|
|
3328
|
+
async requestAbsolute(method, url) {
|
|
3329
|
+
const headers = this.buildHeaders();
|
|
3330
|
+
const res = await fetch(url, { method, headers });
|
|
3331
|
+
const text = await res.text();
|
|
3332
|
+
let parsed = null;
|
|
3333
|
+
try {
|
|
3334
|
+
parsed = text ? JSON.parse(text) : null;
|
|
3335
|
+
} catch {
|
|
3336
|
+
}
|
|
3337
|
+
return { status: res.status, body: parsed, rawText: text };
|
|
3338
|
+
}
|
|
3339
|
+
async deleteApiKey(keyId) {
|
|
3340
|
+
await this.request("DELETE", `/apiKeys/${keyId}`);
|
|
3341
|
+
}
|
|
3342
|
+
};
|
|
3343
|
+
IrisAuthError = class extends Error {
|
|
3344
|
+
constructor(message) {
|
|
3345
|
+
super(message);
|
|
3346
|
+
this.name = "IrisAuthError";
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
});
|
|
3351
|
+
|
|
2709
3352
|
// src/client/index.ts
|
|
2710
3353
|
var client_exports = {};
|
|
2711
3354
|
__export(client_exports, {
|
|
2712
3355
|
AppStoreConnectClient: () => AppStoreConnectClient,
|
|
2713
3356
|
AppStoreConnectError: () => AppStoreConnectError,
|
|
3357
|
+
IrisAuthError: () => IrisAuthError,
|
|
3358
|
+
IrisClient: () => IrisClient,
|
|
2714
3359
|
PortalClient: () => PortalClient,
|
|
2715
3360
|
ProgramLicenseAgreementError: () => ProgramLicenseAgreementError,
|
|
2716
3361
|
RateLimitError: () => RateLimitError,
|
|
2717
3362
|
RequestClient: () => RequestClient,
|
|
2718
3363
|
TokenManager: () => TokenManager,
|
|
3364
|
+
beginLogin: () => beginLogin,
|
|
2719
3365
|
checkPortalResponse: () => checkPortalResponse,
|
|
2720
|
-
loginWithAppleId: () => loginWithAppleId
|
|
3366
|
+
loginWithAppleId: () => loginWithAppleId,
|
|
3367
|
+
requestSmsCode: () => requestSmsCode,
|
|
3368
|
+
submitTwoFactorCode: () => submitTwoFactorCode
|
|
2721
3369
|
});
|
|
2722
3370
|
var AppStoreConnectClient;
|
|
2723
3371
|
var init_client = __esm({
|
|
@@ -2762,6 +3410,7 @@ var init_client = __esm({
|
|
|
2762
3410
|
init_request();
|
|
2763
3411
|
init_apple_session();
|
|
2764
3412
|
init_portal();
|
|
3413
|
+
init_iris();
|
|
2765
3414
|
AppStoreConnectClient = class {
|
|
2766
3415
|
http;
|
|
2767
3416
|
certificates;
|
|
@@ -7949,6 +8598,7 @@ init_review_submissions();
|
|
|
7949
8598
|
// src/cert/index.ts
|
|
7950
8599
|
var cert_exports2 = {};
|
|
7951
8600
|
__export(cert_exports2, {
|
|
8601
|
+
bundleP12: () => bundleP12,
|
|
7952
8602
|
createCertificate: () => createCertificate,
|
|
7953
8603
|
downloadCertificate: () => downloadCertificate,
|
|
7954
8604
|
findValidCertificates: () => findValidCertificates,
|
|
@@ -7959,6 +8609,41 @@ __export(cert_exports2, {
|
|
|
7959
8609
|
});
|
|
7960
8610
|
init_cert();
|
|
7961
8611
|
init_csr();
|
|
8612
|
+
function derBufferToPem(der) {
|
|
8613
|
+
const b64 = der.toString("base64");
|
|
8614
|
+
const lines = [];
|
|
8615
|
+
for (let i = 0; i < b64.length; i += 64) lines.push(b64.slice(i, i + 64));
|
|
8616
|
+
return `-----BEGIN CERTIFICATE-----
|
|
8617
|
+
${lines.join("\n")}
|
|
8618
|
+
-----END CERTIFICATE-----
|
|
8619
|
+
`;
|
|
8620
|
+
}
|
|
8621
|
+
function normalizeCertPem(input) {
|
|
8622
|
+
if (typeof input === "string") {
|
|
8623
|
+
if (input.includes("BEGIN CERTIFICATE")) return input;
|
|
8624
|
+
const b64 = input.replace(/\s+/g, "");
|
|
8625
|
+
return derBufferToPem(Buffer.from(b64, "base64"));
|
|
8626
|
+
}
|
|
8627
|
+
return derBufferToPem(input);
|
|
8628
|
+
}
|
|
8629
|
+
function bundleP12(options) {
|
|
8630
|
+
const certPem = normalizeCertPem(options.certificate);
|
|
8631
|
+
const cert = forge.pki.certificateFromPem(certPem);
|
|
8632
|
+
const privateKey = forge.pki.privateKeyFromPem(options.privateKeyPem);
|
|
8633
|
+
const friendlyName = options.friendlyName ?? "fastnode signing identity";
|
|
8634
|
+
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(
|
|
8635
|
+
privateKey,
|
|
8636
|
+
[cert],
|
|
8637
|
+
options.password,
|
|
8638
|
+
{
|
|
8639
|
+
friendlyName,
|
|
8640
|
+
generateLocalKeyId: true,
|
|
8641
|
+
algorithm: "3des"
|
|
8642
|
+
}
|
|
8643
|
+
);
|
|
8644
|
+
const der = forge.asn1.toDer(p12Asn1).getBytes();
|
|
8645
|
+
return Buffer.from(der, "binary");
|
|
8646
|
+
}
|
|
7962
8647
|
|
|
7963
8648
|
// src/index.ts
|
|
7964
8649
|
init_sigh();
|
|
@@ -8306,6 +8991,440 @@ init_lane();
|
|
|
8306
8991
|
init_lane();
|
|
8307
8992
|
init_context();
|
|
8308
8993
|
init_actions();
|
|
8994
|
+
|
|
8995
|
+
// src/setup/one-click.ts
|
|
8996
|
+
init_portal();
|
|
8997
|
+
init_iris();
|
|
8998
|
+
init_client();
|
|
8999
|
+
init_csr();
|
|
9000
|
+
var DIST_CERT_TYPE = PortalClient.CERT_TYPES.IOS_DISTRIBUTION;
|
|
9001
|
+
var PUSH_PROD_TYPE = PortalClient.CERT_TYPES.IOS_PRODUCTION_PUSH;
|
|
9002
|
+
var PUSH_DEV_TYPE = PortalClient.CERT_TYPES.IOS_DEVELOPMENT_PUSH;
|
|
9003
|
+
function randomPassword(length = 32) {
|
|
9004
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
9005
|
+
const bytes = randomBytes(length);
|
|
9006
|
+
let out = "";
|
|
9007
|
+
for (let i = 0; i < length; i++) out += alphabet[bytes[i] % alphabet.length];
|
|
9008
|
+
return out;
|
|
9009
|
+
}
|
|
9010
|
+
function extractProfileMetadata(profile) {
|
|
9011
|
+
const text = profile.toString("binary");
|
|
9012
|
+
const start = text.indexOf("<?xml");
|
|
9013
|
+
const end = text.indexOf("</plist>");
|
|
9014
|
+
if (start < 0 || end < 0) return {};
|
|
9015
|
+
const plistXml = text.slice(start, end + "</plist>".length);
|
|
9016
|
+
const nameMatch = plistXml.match(/<key>Name<\/key>\s*<string>([^<]+)<\/string>/);
|
|
9017
|
+
const uuidMatch = plistXml.match(/<key>UUID<\/key>\s*<string>([^<]+)<\/string>/);
|
|
9018
|
+
return { name: nameMatch?.[1], uuid: uuidMatch?.[1] };
|
|
9019
|
+
}
|
|
9020
|
+
function classifyPushCertType(typeString) {
|
|
9021
|
+
if (!typeString) return "other";
|
|
9022
|
+
const t = typeString.toLowerCase();
|
|
9023
|
+
if (t.includes("production")) return "production";
|
|
9024
|
+
if (t.includes("development") || t.includes("sandbox")) return "sandbox";
|
|
9025
|
+
return "other";
|
|
9026
|
+
}
|
|
9027
|
+
function buildProfileName(appName, existingNames) {
|
|
9028
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
9029
|
+
const base = `${appName} App Store ${today}`;
|
|
9030
|
+
const taken = new Set(existingNames);
|
|
9031
|
+
if (!taken.has(base)) return base;
|
|
9032
|
+
let n = 1;
|
|
9033
|
+
while (taken.has(`${base} (${n})`)) n++;
|
|
9034
|
+
return `${base} (${n})`;
|
|
9035
|
+
}
|
|
9036
|
+
async function oneClickListTeams(session) {
|
|
9037
|
+
const portal = new PortalClient(session, "");
|
|
9038
|
+
const teams = await portal.listTeams();
|
|
9039
|
+
return (teams ?? []).map((t) => ({
|
|
9040
|
+
teamId: t.teamId,
|
|
9041
|
+
name: t.name,
|
|
9042
|
+
role: t.userRole?.name ?? t.type,
|
|
9043
|
+
status: t.status
|
|
9044
|
+
}));
|
|
9045
|
+
}
|
|
9046
|
+
async function oneClickInventory(session, teamId) {
|
|
9047
|
+
const portal = new PortalClient(session, teamId);
|
|
9048
|
+
await portal.init();
|
|
9049
|
+
const [appIds, allCerts, profiles, pushCerts, apiKeys] = await Promise.all([
|
|
9050
|
+
portal.listAppIds(),
|
|
9051
|
+
portal.listCertificates([DIST_CERT_TYPE]),
|
|
9052
|
+
portal.listProfiles(),
|
|
9053
|
+
portal.listPushCertificates(),
|
|
9054
|
+
portal.listApiKeys().catch(() => ({ keys: [] }))
|
|
9055
|
+
]);
|
|
9056
|
+
const distributionCerts = (allCerts ?? []).map((c) => ({
|
|
9057
|
+
certificateId: c.certificateId,
|
|
9058
|
+
name: c.name ?? "Distribution",
|
|
9059
|
+
serialNumber: c.serialNum,
|
|
9060
|
+
expirationDate: c.expirationDateString,
|
|
9061
|
+
ownerName: c.ownerName
|
|
9062
|
+
}));
|
|
9063
|
+
const apnsAuthKeys = (apiKeys.keys ?? []).filter(
|
|
9064
|
+
(k) => (k.services ?? []).some(
|
|
9065
|
+
(s) => s.id === PortalClient.APNS_SERVICE_ID || /apns/i.test(s.name ?? "")
|
|
9066
|
+
)
|
|
9067
|
+
).map((k) => ({
|
|
9068
|
+
keyId: k.keyId,
|
|
9069
|
+
keyName: k.keyName,
|
|
9070
|
+
dateCreated: k.dateCreated
|
|
9071
|
+
}));
|
|
9072
|
+
return {
|
|
9073
|
+
team: { id: teamId },
|
|
9074
|
+
bundleIds: (appIds ?? []).map((a) => ({
|
|
9075
|
+
appIdId: a.appIdId,
|
|
9076
|
+
identifier: a.identifier,
|
|
9077
|
+
name: a.name
|
|
9078
|
+
})),
|
|
9079
|
+
distributionCerts,
|
|
9080
|
+
distCertCapacity: { used: distributionCerts.length, max: 3 },
|
|
9081
|
+
apnsAuthKeys,
|
|
9082
|
+
apnsKeyCapacity: { used: apnsAuthKeys.length, max: 2 },
|
|
9083
|
+
pushCerts: (pushCerts ?? []).map((c) => ({
|
|
9084
|
+
certificateId: c.certificateId,
|
|
9085
|
+
name: c.name ?? "Push",
|
|
9086
|
+
type: classifyPushCertType(c.typeString),
|
|
9087
|
+
bundleIdentifier: c.appId?.identifier,
|
|
9088
|
+
expirationDate: c.expirationDateString
|
|
9089
|
+
})),
|
|
9090
|
+
profiles: (profiles ?? []).map((p) => ({
|
|
9091
|
+
provisioningProfileId: p.provisioningProfileId,
|
|
9092
|
+
name: p.name,
|
|
9093
|
+
type: p.type,
|
|
9094
|
+
status: p.status,
|
|
9095
|
+
expirationDate: p.expirationDate
|
|
9096
|
+
}))
|
|
9097
|
+
};
|
|
9098
|
+
}
|
|
9099
|
+
async function ensureBundleId(portal, identifier, name, warnings) {
|
|
9100
|
+
const existing = await portal.listAppIds();
|
|
9101
|
+
const hit = existing.find((a) => a.identifier === identifier);
|
|
9102
|
+
if (hit) {
|
|
9103
|
+
warnings.push(`Bundle ID ${identifier} already existed and was reused.`);
|
|
9104
|
+
return hit.appIdId;
|
|
9105
|
+
}
|
|
9106
|
+
const created = await portal.createAppId({ identifier, name, enablePush: true });
|
|
9107
|
+
return created.appIdId;
|
|
9108
|
+
}
|
|
9109
|
+
async function createDistCert(portal) {
|
|
9110
|
+
const { csr, privateKey } = generateRSACSR();
|
|
9111
|
+
const cert = await portal.createCertificate({ type: DIST_CERT_TYPE, csrContent: csr });
|
|
9112
|
+
let certContent;
|
|
9113
|
+
if (cert.certificateContent) {
|
|
9114
|
+
certContent = Buffer.from(cert.certificateContent, "base64");
|
|
9115
|
+
} else {
|
|
9116
|
+
certContent = await portal.downloadCertificate(cert.certificateId, DIST_CERT_TYPE);
|
|
9117
|
+
}
|
|
9118
|
+
return { certificateId: cert.certificateId, privateKeyPem: privateKey, certContent };
|
|
9119
|
+
}
|
|
9120
|
+
async function createPushCert(portal, appIdId, type) {
|
|
9121
|
+
const { csr, privateKey } = generateRSACSR();
|
|
9122
|
+
const cert = await portal.createCertificate({ type, csrContent: csr, appIdId });
|
|
9123
|
+
let certContent;
|
|
9124
|
+
if (cert.certificateContent) {
|
|
9125
|
+
certContent = Buffer.from(cert.certificateContent, "base64");
|
|
9126
|
+
} else {
|
|
9127
|
+
certContent = await portal.downloadCertificate(cert.certificateId, type);
|
|
9128
|
+
}
|
|
9129
|
+
return { certificateId: cert.certificateId, privateKeyPem: privateKey, certContent };
|
|
9130
|
+
}
|
|
9131
|
+
function findExistingPushCertForBundle(pushCerts, appIdId, bundleIdentifier, kind) {
|
|
9132
|
+
for (const c of pushCerts) {
|
|
9133
|
+
const appMatches = c.appId?.appIdId === appIdId || c.appId?.identifier === bundleIdentifier;
|
|
9134
|
+
if (!appMatches) continue;
|
|
9135
|
+
const ts = (c.typeString ?? "").toLowerCase();
|
|
9136
|
+
const name = (c.name ?? "").toLowerCase();
|
|
9137
|
+
const looksLikePush = ts.includes("push") || ts.includes("apns") || name.includes("apple push services");
|
|
9138
|
+
if (!looksLikePush) continue;
|
|
9139
|
+
const isProduction = ts.includes("production");
|
|
9140
|
+
const isSandbox = ts.includes("development") || ts.includes("sandbox");
|
|
9141
|
+
const isUnified = !isProduction && !isSandbox;
|
|
9142
|
+
if (!isUnified) {
|
|
9143
|
+
if (kind === "production" && !isProduction) continue;
|
|
9144
|
+
if (kind === "sandbox" && !isSandbox) continue;
|
|
9145
|
+
}
|
|
9146
|
+
return {
|
|
9147
|
+
certificateId: c.certificateId,
|
|
9148
|
+
name: c.name ?? "Apple Push Services",
|
|
9149
|
+
expirationDate: c.expirationDateString
|
|
9150
|
+
};
|
|
9151
|
+
}
|
|
9152
|
+
return null;
|
|
9153
|
+
}
|
|
9154
|
+
async function oneClickSetup(input) {
|
|
9155
|
+
const opts = {
|
|
9156
|
+
generateDistributionCert: input.options?.generateDistributionCert ?? true,
|
|
9157
|
+
generateStoreProfile: input.options?.generateStoreProfile ?? true,
|
|
9158
|
+
generateAscApiKey: input.options?.generateAscApiKey ?? true,
|
|
9159
|
+
generateApnsAuthKey: input.options?.generateApnsAuthKey ?? true,
|
|
9160
|
+
generatePushProductionCert: input.options?.generatePushProductionCert ?? false,
|
|
9161
|
+
generatePushSandboxCert: input.options?.generatePushSandboxCert ?? false,
|
|
9162
|
+
enableSignInWithApple: input.options?.enableSignInWithApple ?? true,
|
|
9163
|
+
enablePushNotifications: input.options?.enablePushNotifications ?? true,
|
|
9164
|
+
revokeCertIds: input.options?.revokeCertIds ?? [],
|
|
9165
|
+
revokeApnsKeyIds: input.options?.revokeApnsKeyIds ?? []
|
|
9166
|
+
};
|
|
9167
|
+
const onProgress = input.onProgress ?? (() => {
|
|
9168
|
+
});
|
|
9169
|
+
const warnings = [];
|
|
9170
|
+
const created = { revokedCertIds: [] };
|
|
9171
|
+
const additionalCerts = [];
|
|
9172
|
+
if (!opts.generateDistributionCert || !opts.generateStoreProfile) {
|
|
9173
|
+
throw new Error("Distribution cert and store profile are both required for an apple_credentials row.");
|
|
9174
|
+
}
|
|
9175
|
+
async function step(id, label, fn) {
|
|
9176
|
+
onProgress({ type: "step_start", step: id, label });
|
|
9177
|
+
try {
|
|
9178
|
+
const result = await fn();
|
|
9179
|
+
onProgress({ type: "step_done", step: id, label });
|
|
9180
|
+
return result;
|
|
9181
|
+
} catch (err) {
|
|
9182
|
+
onProgress({
|
|
9183
|
+
type: "step_failed",
|
|
9184
|
+
step: id,
|
|
9185
|
+
label,
|
|
9186
|
+
error: err instanceof Error ? err.message : String(err)
|
|
9187
|
+
});
|
|
9188
|
+
throw err;
|
|
9189
|
+
}
|
|
9190
|
+
}
|
|
9191
|
+
const portal = new PortalClient(input.session, input.teamId);
|
|
9192
|
+
await step("init", "Initialize Apple Developer portal session", () => portal.init());
|
|
9193
|
+
for (const certId of opts.revokeCertIds) {
|
|
9194
|
+
await step(`revoke:${certId}`, `Revoke distribution certificate ${certId}`, async () => {
|
|
9195
|
+
await portal.revokeCertificate(certId, DIST_CERT_TYPE);
|
|
9196
|
+
created.revokedCertIds.push(certId);
|
|
9197
|
+
});
|
|
9198
|
+
}
|
|
9199
|
+
const appIdId = await step(
|
|
9200
|
+
"bundle_id",
|
|
9201
|
+
`Bundle identifier ${input.bundleIdentifier}`,
|
|
9202
|
+
() => ensureBundleId(portal, input.bundleIdentifier, input.appName, warnings)
|
|
9203
|
+
);
|
|
9204
|
+
created.bundleIdId = appIdId;
|
|
9205
|
+
const dist = await step("dist_cert", "Distribution certificate", () => createDistCert(portal));
|
|
9206
|
+
created.certificateId = dist.certificateId;
|
|
9207
|
+
const certPassword = input.certPassword ?? randomPassword();
|
|
9208
|
+
const distP12 = await step(
|
|
9209
|
+
"p12",
|
|
9210
|
+
"Bundle distribution .p12",
|
|
9211
|
+
async () => bundleP12({
|
|
9212
|
+
certificate: dist.certContent,
|
|
9213
|
+
privateKeyPem: dist.privateKeyPem,
|
|
9214
|
+
password: certPassword,
|
|
9215
|
+
friendlyName: `${input.appName} Distribution`
|
|
9216
|
+
})
|
|
9217
|
+
);
|
|
9218
|
+
const existingProfiles = await portal.listProfiles();
|
|
9219
|
+
const profileName = buildProfileName(
|
|
9220
|
+
input.appName,
|
|
9221
|
+
(existingProfiles ?? []).map((p) => p.name)
|
|
9222
|
+
);
|
|
9223
|
+
const profile = await step("profile", `App Store provisioning profile (${profileName})`, async () => {
|
|
9224
|
+
const p = await portal.createProfile({
|
|
9225
|
+
appIdId,
|
|
9226
|
+
name: profileName,
|
|
9227
|
+
distributionType: "store",
|
|
9228
|
+
certificateIds: [dist.certificateId]
|
|
9229
|
+
});
|
|
9230
|
+
const content = await portal.downloadProfile(p.provisioningProfileId);
|
|
9231
|
+
return { id: p.provisioningProfileId, content };
|
|
9232
|
+
});
|
|
9233
|
+
created.provisioningProfileId = profile.id;
|
|
9234
|
+
const profileMeta = extractProfileMetadata(profile.content);
|
|
9235
|
+
if (profileMeta.uuid) created.profileUuid = profileMeta.uuid;
|
|
9236
|
+
for (const keyId of opts.revokeApnsKeyIds) {
|
|
9237
|
+
await step(`revoke_apns:${keyId}`, `Revoke APNs auth key ${keyId}`, async () => {
|
|
9238
|
+
await portal.revokeApiKey(keyId);
|
|
9239
|
+
});
|
|
9240
|
+
}
|
|
9241
|
+
if (opts.generateApnsAuthKey) {
|
|
9242
|
+
await step("apns_auth_key", "APNs Authentication Key (.p8)", async () => {
|
|
9243
|
+
try {
|
|
9244
|
+
const key = await portal.createApnsAuthKey(`${input.appName} APNs`);
|
|
9245
|
+
created.apnsAuthKeyId = key.keyId;
|
|
9246
|
+
additionalCerts.push({
|
|
9247
|
+
cert_type: "apns_key",
|
|
9248
|
+
cert_base64: Buffer.from(key.pem, "utf-8").toString("base64"),
|
|
9249
|
+
cert_password: key.keyId
|
|
9250
|
+
});
|
|
9251
|
+
} catch (err) {
|
|
9252
|
+
warnings.push(
|
|
9253
|
+
`APNs Authentication Key could not be created: ${err.message}. You can upload an existing .p8 manually from the credentials list.`
|
|
9254
|
+
);
|
|
9255
|
+
}
|
|
9256
|
+
});
|
|
9257
|
+
}
|
|
9258
|
+
let pushCertCache = null;
|
|
9259
|
+
const loadPushCerts = async () => {
|
|
9260
|
+
if (pushCertCache == null) {
|
|
9261
|
+
pushCertCache = await portal.listPushCertificates();
|
|
9262
|
+
}
|
|
9263
|
+
return pushCertCache;
|
|
9264
|
+
};
|
|
9265
|
+
async function provisionLegacyPushCert(stepId, label, kind, pushType, certType, friendlyName) {
|
|
9266
|
+
await step(stepId, label, async () => {
|
|
9267
|
+
try {
|
|
9268
|
+
const existing = findExistingPushCertForBundle(
|
|
9269
|
+
await loadPushCerts(),
|
|
9270
|
+
appIdId,
|
|
9271
|
+
input.bundleIdentifier,
|
|
9272
|
+
kind
|
|
9273
|
+
);
|
|
9274
|
+
if (existing) {
|
|
9275
|
+
warnings.push(
|
|
9276
|
+
`${label} already exists for ${input.bundleIdentifier} ("${existing.name}"` + (existing.expirationDate ? `, expires ${existing.expirationDate}` : "") + `). Skipped \u2014 Apple caps these at 1 per bundle per environment and we can't recover the existing private key. Revoke it manually in Apple Developer Portal and re-run if you need a sitepong-managed copy.`
|
|
9277
|
+
);
|
|
9278
|
+
return;
|
|
9279
|
+
}
|
|
9280
|
+
const c = await createPushCert(portal, appIdId, pushType);
|
|
9281
|
+
const pushPassword = randomPassword();
|
|
9282
|
+
const p12 = bundleP12({
|
|
9283
|
+
certificate: c.certContent,
|
|
9284
|
+
privateKeyPem: c.privateKeyPem,
|
|
9285
|
+
password: pushPassword,
|
|
9286
|
+
friendlyName
|
|
9287
|
+
});
|
|
9288
|
+
additionalCerts.push({
|
|
9289
|
+
cert_type: certType,
|
|
9290
|
+
cert_base64: p12.toString("base64"),
|
|
9291
|
+
cert_password: pushPassword
|
|
9292
|
+
});
|
|
9293
|
+
} catch (err) {
|
|
9294
|
+
warnings.push(
|
|
9295
|
+
`${label} could not be created: ${err.message}. You can upload an existing .p12 manually from the credentials list.`
|
|
9296
|
+
);
|
|
9297
|
+
}
|
|
9298
|
+
});
|
|
9299
|
+
}
|
|
9300
|
+
if (opts.generatePushProductionCert) {
|
|
9301
|
+
await provisionLegacyPushCert(
|
|
9302
|
+
"push_prod_cert",
|
|
9303
|
+
"APS Production push certificate (.p12)",
|
|
9304
|
+
"production",
|
|
9305
|
+
PUSH_PROD_TYPE,
|
|
9306
|
+
"aps_production",
|
|
9307
|
+
`${input.appName} APNs Production`
|
|
9308
|
+
);
|
|
9309
|
+
}
|
|
9310
|
+
if (opts.generatePushSandboxCert) {
|
|
9311
|
+
await provisionLegacyPushCert(
|
|
9312
|
+
"push_sandbox_cert",
|
|
9313
|
+
"APS Sandbox push certificate (.p12)",
|
|
9314
|
+
"sandbox",
|
|
9315
|
+
PUSH_DEV_TYPE,
|
|
9316
|
+
"aps_sandbox",
|
|
9317
|
+
`${input.appName} APNs Sandbox`
|
|
9318
|
+
);
|
|
9319
|
+
}
|
|
9320
|
+
let ascKeyId = "";
|
|
9321
|
+
let ascIssuerId = "";
|
|
9322
|
+
let ascP8Base64 = "";
|
|
9323
|
+
let ascP8Pem = "";
|
|
9324
|
+
if (opts.generateAscApiKey) {
|
|
9325
|
+
await step("asc_api_key", "App Store Connect API key (.p8)", async () => {
|
|
9326
|
+
try {
|
|
9327
|
+
const iris = new IrisClient(input.session);
|
|
9328
|
+
const createdKey = await iris.createApiKey({
|
|
9329
|
+
nickname: `${input.appName} SitePong`,
|
|
9330
|
+
roles: ["APP_MANAGER"],
|
|
9331
|
+
allAppsVisible: true,
|
|
9332
|
+
keyType: "PUBLIC_API"
|
|
9333
|
+
});
|
|
9334
|
+
created.ascApiKeyId = createdKey.id;
|
|
9335
|
+
const downloaded = await iris.fetchApiKeyPrivateKey(createdKey.id);
|
|
9336
|
+
if (!downloaded) {
|
|
9337
|
+
throw new Error(
|
|
9338
|
+
`Created key ${createdKey.kid} but Apple did not return the .p8. Revoke it at appstoreconnect.apple.com/access/integrations/api/${createdKey.id} before retrying.`
|
|
9339
|
+
);
|
|
9340
|
+
}
|
|
9341
|
+
if (!downloaded.issuerId) {
|
|
9342
|
+
throw new Error(
|
|
9343
|
+
`Created key ${createdKey.kid} and got the .p8 but Apple did not return an issuer id. Revoke it at appstoreconnect.apple.com/access/integrations/api/${createdKey.id} and check ApiKey.infoAsync semantics.`
|
|
9344
|
+
);
|
|
9345
|
+
}
|
|
9346
|
+
ascKeyId = createdKey.kid;
|
|
9347
|
+
ascIssuerId = downloaded.issuerId;
|
|
9348
|
+
ascP8Pem = downloaded.pem;
|
|
9349
|
+
ascP8Base64 = Buffer.from(downloaded.pem, "utf-8").toString("base64");
|
|
9350
|
+
} catch (err) {
|
|
9351
|
+
warnings.push(
|
|
9352
|
+
`Could not auto-create ASC API key: ${err.message}. Upload a .p8 manually after the wizard finishes.`
|
|
9353
|
+
);
|
|
9354
|
+
}
|
|
9355
|
+
});
|
|
9356
|
+
}
|
|
9357
|
+
if ((opts.enablePushNotifications || opts.enableSignInWithApple) && ascKeyId && ascIssuerId && ascP8Pem) {
|
|
9358
|
+
await step("capabilities", "Enable bundle capabilities (Push, Sign in with Apple)", async () => {
|
|
9359
|
+
try {
|
|
9360
|
+
const ascClient = new AppStoreConnectClient({
|
|
9361
|
+
keyId: ascKeyId,
|
|
9362
|
+
issuerId: ascIssuerId,
|
|
9363
|
+
privateKey: ascP8Pem
|
|
9364
|
+
});
|
|
9365
|
+
const restBundle = await ascClient.bundleIds.find(input.bundleIdentifier);
|
|
9366
|
+
if (!restBundle) {
|
|
9367
|
+
warnings.push(
|
|
9368
|
+
`Could not look up bundle id ${input.bundleIdentifier} via REST API \u2014 capability flips skipped.`
|
|
9369
|
+
);
|
|
9370
|
+
return;
|
|
9371
|
+
}
|
|
9372
|
+
const existing = await ascClient.capabilities.listForBundleId(restBundle.id);
|
|
9373
|
+
const existingTypes = new Set(existing.map((c) => c.attributes?.capabilityType));
|
|
9374
|
+
const wanted = [];
|
|
9375
|
+
if (opts.enablePushNotifications) {
|
|
9376
|
+
wanted.push({ type: "PUSH_NOTIFICATIONS", label: "Push Notifications" });
|
|
9377
|
+
}
|
|
9378
|
+
if (opts.enableSignInWithApple) {
|
|
9379
|
+
wanted.push({
|
|
9380
|
+
type: "APPLE_ID_AUTH",
|
|
9381
|
+
label: "Sign in with Apple",
|
|
9382
|
+
settings: [
|
|
9383
|
+
{
|
|
9384
|
+
key: "APPLE_ID_AUTH_APP_CONSENT",
|
|
9385
|
+
options: [{ key: "PRIMARY_APP_CONSENT" }]
|
|
9386
|
+
}
|
|
9387
|
+
]
|
|
9388
|
+
});
|
|
9389
|
+
}
|
|
9390
|
+
for (const w of wanted) {
|
|
9391
|
+
if (existingTypes.has(w.type)) continue;
|
|
9392
|
+
try {
|
|
9393
|
+
await ascClient.capabilities.enable({
|
|
9394
|
+
bundleIdId: restBundle.id,
|
|
9395
|
+
capabilityType: w.type,
|
|
9396
|
+
settings: w.settings
|
|
9397
|
+
});
|
|
9398
|
+
} catch (err) {
|
|
9399
|
+
warnings.push(`Could not enable ${w.label}: ${err.message}`);
|
|
9400
|
+
}
|
|
9401
|
+
}
|
|
9402
|
+
} catch (err) {
|
|
9403
|
+
warnings.push(`Capability setup failed: ${err.message}`);
|
|
9404
|
+
}
|
|
9405
|
+
});
|
|
9406
|
+
} else if ((opts.enablePushNotifications || opts.enableSignInWithApple) && !ascKeyId) {
|
|
9407
|
+
warnings.push(
|
|
9408
|
+
"Push / Sign in with Apple capabilities couldn't be flipped automatically (no ASC API key). Enable them manually in your Apple Developer Portal."
|
|
9409
|
+
);
|
|
9410
|
+
}
|
|
9411
|
+
return {
|
|
9412
|
+
team_id: input.teamId,
|
|
9413
|
+
bundle_identifier: input.bundleIdentifier,
|
|
9414
|
+
name: `${input.appName} \u2014 Distribution`,
|
|
9415
|
+
dist_cert_base64: distP12.toString("base64"),
|
|
9416
|
+
private_key_base64: Buffer.from(dist.privateKeyPem, "utf-8").toString("base64"),
|
|
9417
|
+
cert_password: certPassword,
|
|
9418
|
+
provisioning_profile_base64: profile.content.toString("base64"),
|
|
9419
|
+
profile_name: profileMeta.name ?? profileName,
|
|
9420
|
+
asc_key_id: ascKeyId,
|
|
9421
|
+
asc_issuer_id: ascIssuerId,
|
|
9422
|
+
asc_api_key_base64: ascP8Base64,
|
|
9423
|
+
additional_certs: additionalCerts,
|
|
9424
|
+
created,
|
|
9425
|
+
warnings
|
|
9426
|
+
};
|
|
9427
|
+
}
|
|
8309
9428
|
function createRunCommand() {
|
|
8310
9429
|
const cmd = new Command("run").description("Execute a lane from Fastfile.ts").argument("<lane>", "Lane name to execute").argument("[params...]", "Key:value parameters").option("-p, --platform <platform>", "Platform (ios, android)").action(async (laneName, params, opts) => {
|
|
8311
9430
|
const { loadFastfile: loadFastfile2 } = await Promise.resolve().then(() => (init_fastfile(), fastfile_exports));
|
|
@@ -8605,6 +9724,6 @@ function createCli() {
|
|
|
8605
9724
|
return program;
|
|
8606
9725
|
}
|
|
8607
9726
|
|
|
8608
|
-
export { AppScreenshotsResource, AppStoreConnectClient, AppStoreConnectError, AppStoreVersionsResource, AppsResource, BetaGroupsResource, BuildsResource, BundleIdsResource, CapabilitiesResource, CertificatesResource, DevicesResource, LaneContext, ProfilesResource, RateLimitError, RequestClient, ReviewSubmissionsResource, TokenManager, action, afterAll, afterEach, beforeAll, beforeEach, cert_exports2 as cert, core_exports as core, createCli, deliver_exports as deliver, ensureActionsRegistered, frameit_exports as frameit, generateFastfileTemplate, getAction, gym_exports as gym, lane, listActions, listAvailableLanes, loadFastfile, match_exports as match, pem_exports as pem, pilot_exports as pilot, precheck_exports as precheck, privateLane, produce_exports as produce, registerAction, runLane, scan_exports as scan, sigh_exports as sigh, snapshot_exports as snapshot, trainer_exports as trainer, xcode_exports as xcode };
|
|
9727
|
+
export { AppScreenshotsResource, AppStoreConnectClient, AppStoreConnectError, AppStoreVersionsResource, AppsResource, BetaGroupsResource, BuildsResource, BundleIdsResource, CapabilitiesResource, CertificatesResource, DevicesResource, LaneContext, ProfilesResource, RateLimitError, RequestClient, ReviewSubmissionsResource, TokenManager, action, afterAll, afterEach, beforeAll, beforeEach, cert_exports2 as cert, core_exports as core, createCli, deliver_exports as deliver, ensureActionsRegistered, frameit_exports as frameit, generateFastfileTemplate, getAction, gym_exports as gym, lane, listActions, listAvailableLanes, loadFastfile, match_exports as match, oneClickInventory, oneClickListTeams, oneClickSetup, pem_exports as pem, pilot_exports as pilot, precheck_exports as precheck, privateLane, produce_exports as produce, registerAction, runLane, scan_exports as scan, sigh_exports as sigh, snapshot_exports as snapshot, trainer_exports as trainer, xcode_exports as xcode };
|
|
8609
9728
|
//# sourceMappingURL=index.mjs.map
|
|
8610
9729
|
//# sourceMappingURL=index.mjs.map
|