@sitepong/fastnode 0.1.1 → 0.2.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.
Files changed (76) hide show
  1. package/README.md +148 -0
  2. package/dist/actions/index.cjs +964 -137
  3. package/dist/actions/index.cjs.map +1 -1
  4. package/dist/actions/index.mjs +964 -137
  5. package/dist/actions/index.mjs.map +1 -1
  6. package/dist/cert/index.cjs +40 -0
  7. package/dist/cert/index.cjs.map +1 -1
  8. package/dist/cert/index.d.cts +3 -3
  9. package/dist/cert/index.d.ts +3 -3
  10. package/dist/cert/index.mjs +37 -1
  11. package/dist/cert/index.mjs.map +1 -1
  12. package/dist/{cert-BmJGtYtb.d.cts → cert-6m1R8UeW.d.cts} +1 -1
  13. package/dist/{cert-owkOFNW8.d.ts → cert-DtYDJBjG.d.ts} +1 -1
  14. package/dist/cli/index.cjs +964 -137
  15. package/dist/cli/index.cjs.map +1 -1
  16. package/dist/cli/index.d.cts +1 -1
  17. package/dist/cli/index.d.ts +1 -1
  18. package/dist/cli/index.mjs +964 -137
  19. package/dist/cli/index.mjs.map +1 -1
  20. package/dist/client/index.cjs +730 -109
  21. package/dist/client/index.cjs.map +1 -1
  22. package/dist/client/index.d.cts +1 -1
  23. package/dist/client/index.d.ts +1 -1
  24. package/dist/client/index.mjs +726 -110
  25. package/dist/client/index.mjs.map +1 -1
  26. package/dist/deliver/index.d.cts +2 -2
  27. package/dist/deliver/index.d.ts +2 -2
  28. package/dist/index-AD8QXY1T.d.cts +158 -0
  29. package/dist/{index-DfCUVJX1.d.cts → index-BW8wQUGC.d.cts} +1 -1
  30. package/dist/{index-EcVvViW8.d.cts → index-BWK-T2-S.d.cts} +1 -1
  31. package/dist/{index-B0yhu_LZ.d.ts → index-BpiEBPVd.d.ts} +1 -1
  32. package/dist/index-CCs5irG7.d.cts +61 -0
  33. package/dist/{index-B9yXicrQ.d.ts → index-CLsvyTkh.d.ts} +1 -1
  34. package/dist/{index-BLubSlgK.d.cts → index-CMH_GpN9.d.cts} +358 -2
  35. package/dist/index-CQKpbHZt.d.ts +61 -0
  36. package/dist/{index-BQJIJlK7.d.cts → index-CQd4bo3G.d.cts} +3 -3
  37. package/dist/{index-CWNLnSOL.d.cts → index-CdYdwxpQ.d.cts} +1 -1
  38. package/dist/{index-BFdNOmPJ.d.cts → index-Ce-WMV7c.d.cts} +1 -1
  39. package/dist/{index-TfXqwuac.d.ts → index-CojSGJ7m.d.ts} +1 -1
  40. package/dist/{index-1Vu2cRQ2.d.ts → index-CsFIp7OO.d.ts} +1 -1
  41. package/dist/{index-D1Izm7qt.d.ts → index-D2z7RiAM.d.ts} +358 -2
  42. package/dist/index-D82CmjMP.d.ts +158 -0
  43. package/dist/{index-BN0ubxjZ.d.ts → index-DZaPkUnC.d.ts} +3 -3
  44. package/dist/{index-D5eihzwM.d.cts → index-KCB2q1-8.d.cts} +1 -1
  45. package/dist/{index-A5MimeQk.d.ts → index-Kq3rbG0u.d.ts} +1 -1
  46. package/dist/index.cjs +1322 -138
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.cts +199 -10
  49. package/dist/index.d.ts +199 -10
  50. package/dist/index.mjs +1319 -139
  51. package/dist/index.mjs.map +1 -1
  52. package/dist/lane/index.cjs +964 -137
  53. package/dist/lane/index.cjs.map +1 -1
  54. package/dist/lane/index.mjs +964 -137
  55. package/dist/lane/index.mjs.map +1 -1
  56. package/dist/match/index.d.cts +4 -4
  57. package/dist/match/index.d.ts +4 -4
  58. package/dist/pem/index.d.cts +2 -2
  59. package/dist/pem/index.d.ts +2 -2
  60. package/dist/pilot/index.cjs +190 -12
  61. package/dist/pilot/index.cjs.map +1 -1
  62. package/dist/pilot/index.d.cts +2 -2
  63. package/dist/pilot/index.d.ts +2 -2
  64. package/dist/pilot/index.mjs +186 -14
  65. package/dist/pilot/index.mjs.map +1 -1
  66. package/dist/precheck/index.d.cts +2 -2
  67. package/dist/precheck/index.d.ts +2 -2
  68. package/dist/produce/index.d.cts +2 -2
  69. package/dist/produce/index.d.ts +2 -2
  70. package/dist/sigh/index.d.cts +2 -2
  71. package/dist/sigh/index.d.ts +2 -2
  72. package/package.json +3 -1
  73. package/dist/index-DfirmFbC.d.ts +0 -32
  74. package/dist/index-DkvinY7_.d.ts +0 -54
  75. package/dist/index-HRiaaX-Q.d.cts +0 -54
  76. 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 loginWithAppleId(config) {
223
- const log = config.onLog ?? console.log;
224
- const promptFn = config.onPrompt ?? defaultPrompt;
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 session = {
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
- } else if (response.status === 200) {
252
- log("Login successful.");
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(`Unexpected login response (${response.status}): ${JSON.stringify(response.body).slice(0, 500)}`);
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
- return session;
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));
@@ -2475,6 +2496,264 @@ var init_portal = __esm({
2475
2496
  async init() {
2476
2497
  await this.post("account/listTeams.action", {});
2477
2498
  }
2499
+ /**
2500
+ * JSON POST — different content type than the standard form-encoded
2501
+ * `post()`. Used by the App Store Connect API key endpoints
2502
+ * (`account/auth/key/v2/create`), which Spaceship discovered take a
2503
+ * JSON body even though every other portal mutation is form-encoded.
2504
+ *
2505
+ * Like the form-encoded `post()`, this also runs the response through
2506
+ * `checkPortalResponse` — Apple returns HTTP 200 even for errors and
2507
+ * encodes the error in `resultCode`/`userString` fields, so a raw
2508
+ * status check would silently miss real failures.
2509
+ */
2510
+ async postJson(path, body) {
2511
+ const headers = {
2512
+ Cookie: this.cookieStr,
2513
+ "Content-Type": "application/json",
2514
+ Accept: "application/json"
2515
+ };
2516
+ if (this.csrf) {
2517
+ headers["csrf"] = this.csrf;
2518
+ headers["csrf_ts"] = this.csrfTs;
2519
+ }
2520
+ console.log(`[portal] POST(json) ${path}`);
2521
+ const res = await fetch(`${PORTAL_BASE}/${path}`, {
2522
+ method: "POST",
2523
+ headers,
2524
+ body: JSON.stringify(body)
2525
+ });
2526
+ if (res.headers.get("csrf")) {
2527
+ this.csrf = res.headers.get("csrf");
2528
+ this.csrfTs = res.headers.get("csrf_ts") ?? "";
2529
+ }
2530
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
2531
+ const m = raw.match(/^([^=]+)=([^;]*)/);
2532
+ if (m && !this.cookieStr.includes(m[1] + "=")) this.cookieStr += "; " + m[1] + "=" + m[2];
2533
+ }
2534
+ const text = await res.text();
2535
+ let data = null;
2536
+ try {
2537
+ data = text ? JSON.parse(text) : null;
2538
+ } catch {
2539
+ }
2540
+ console.log(`[portal] POST(json) ${path} \u2192 ${res.status} ${text.slice(0, 400)}`);
2541
+ checkPortalResponse(data, this.session);
2542
+ return { status: res.status, body: data, rawText: text };
2543
+ }
2544
+ /**
2545
+ * GET helper for the portal endpoints that take query parameters
2546
+ * (currently just `account/auth/key/download`). Returns the raw text
2547
+ * since download endpoints can return PEMs or JSON depending on Apple's
2548
+ * mood — caller decides how to parse.
2549
+ */
2550
+ async getRaw(path, params = {}) {
2551
+ const url = new URL(`${PORTAL_BASE}/${path}`);
2552
+ for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
2553
+ const headers = {
2554
+ Cookie: this.cookieStr,
2555
+ Accept: "application/json, text/plain, application/x-pem-file, */*"
2556
+ };
2557
+ if (this.csrf) {
2558
+ headers["csrf"] = this.csrf;
2559
+ headers["csrf_ts"] = this.csrfTs;
2560
+ }
2561
+ console.log(`[portal] GET ${url.pathname}${url.search}`);
2562
+ const res = await fetch(url.toString(), { headers });
2563
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
2564
+ const m = raw.match(/^([^=]+)=([^;]*)/);
2565
+ if (m && !this.cookieStr.includes(m[1] + "=")) this.cookieStr += "; " + m[1] + "=" + m[2];
2566
+ }
2567
+ const text = await res.text();
2568
+ const contentType = res.headers.get("content-type") ?? "";
2569
+ console.log(`[portal] GET ${url.pathname} \u2192 ${res.status} ${contentType} ${text.length}b`);
2570
+ return { status: res.status, rawText: text, contentType };
2571
+ }
2572
+ // ── App Store Connect API Keys (.p8) ────────────────────────────────────
2573
+ /**
2574
+ * Create an App Store Connect API key via the developer portal's
2575
+ * `account/auth/key/v2/create` endpoint. Reverse-engineered from the
2576
+ * _current_ @expo/apple-utils npm package (v2.1.21), which is what
2577
+ * EAS CLI uses to manage Apple credentials in production.
2578
+ *
2579
+ * This differs from older published reverse-engineering guides:
2580
+ * • URL uses the `/v2/create` suffix (Apple kept it)
2581
+ * • Field is `serviceConfigurations`, shape is an object keyed by
2582
+ * service id (NOT `serviceConfigurationsRequests: [...]` array)
2583
+ * • Body REQUIRES `scope: "team"`
2584
+ * • Needs a scope-specific CSRF warmup via the `keys` endpoint
2585
+ * before the create call (EAS's `ensureCSRFAsync("keys", …)`)
2586
+ *
2587
+ * For an App Store Connect API key (PUBLIC_API, used by altool for
2588
+ * TestFlight uploads), `serviceConfigurations` is an empty object `{}`.
2589
+ * For an APNs auth key, it's `{ "U27F4V844T": [] }`. For MusicKit:
2590
+ * `{ "6A7HVUVQ3M": [musicId] }`, etc.
2591
+ *
2592
+ * Apple's name validator rejects parentheses, brackets, and most
2593
+ * punctuation — sanitize to alphanumeric + space + dash + underscore
2594
+ * + period and collapse whitespace.
2595
+ */
2596
+ async createApiKey(rawName, serviceConfigurations = {}) {
2597
+ const name = rawName.replace(/[^A-Za-z0-9 _\-.]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
2598
+ if (!name) throw new Error("createApiKey: name is empty after sanitization");
2599
+ try {
2600
+ await this.listApiKeys();
2601
+ } catch {
2602
+ }
2603
+ const body = {
2604
+ teamId: this.teamId,
2605
+ name,
2606
+ serviceConfigurations,
2607
+ scope: "team"
2608
+ };
2609
+ const result = await this.postJson("account/auth/key/v2/create", body);
2610
+ const created = result.body?.keys?.[0] ?? result.body?.key ?? result.body?.data ?? result.body;
2611
+ const keyId = created?.keyId ?? created?.id ?? created?.kid ?? created?.attributes?.kid;
2612
+ if (!keyId) {
2613
+ throw new Error(`createApiKey: no key id in response. Body: ${result.rawText.slice(0, 400)}`);
2614
+ }
2615
+ return { keyId, raw: created };
2616
+ }
2617
+ /**
2618
+ * Fetch a single API key by id via `account/auth/key/get`. Reverse-
2619
+ * engineered from Spaceship's `get_key`. Returns the raw key record so
2620
+ * the caller can extract whatever fields it needs (e.g. issuerId).
2621
+ *
2622
+ * Form-encoded POST — same shape as every other PortalClient mutation
2623
+ * except the JSON `create` endpoint.
2624
+ */
2625
+ async getApiKey(keyId) {
2626
+ const data = await this.post("account/auth/key/get", {
2627
+ teamId: this.teamId,
2628
+ keyId
2629
+ });
2630
+ return data?.keys?.[0] ?? data?.key ?? data?.data ?? data;
2631
+ }
2632
+ /**
2633
+ * Revoke an existing key. Form-encoded POST per Spaceship's
2634
+ * `revoke_key!`. Used for cleaning up orphan keys created during
2635
+ * failed wizard runs.
2636
+ */
2637
+ async revokeApiKey(keyId) {
2638
+ await this.post("account/auth/key/revoke", {
2639
+ teamId: this.teamId,
2640
+ keyId
2641
+ });
2642
+ }
2643
+ // ── APNs Authentication Keys (.p8) ──────────────────────────────────────
2644
+ /** Apple's hardcoded service id for APNs auth keys (from Spaceship). */
2645
+ static APNS_SERVICE_ID = "U27F4V844T";
2646
+ /**
2647
+ * Create an APNs Authentication Key (.p8) via the developer portal.
2648
+ *
2649
+ * Apple requires BOTH `serviceConfigurations` (the map) AND
2650
+ * `serviceConfigurationsRequests` (the array of full config objects)
2651
+ * in the v2/create body. Just sending the map alone results in
2652
+ * "No value was provided for the parameter 'serviceConfigurations'".
2653
+ *
2654
+ * The exact body comes from @expo/apple-utils v2.1.21 — both fields
2655
+ * are produced by separate functions (`d` for the map, `h` for the
2656
+ * array) and merged into the create payload.
2657
+ */
2658
+ async createApnsAuthKey(rawName) {
2659
+ const name = rawName.replace(/[^A-Za-z0-9 _\-.]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
2660
+ if (!name) throw new Error("createApnsAuthKey: name is empty after sanitization");
2661
+ try {
2662
+ await this.listApiKeys();
2663
+ } catch {
2664
+ }
2665
+ const body = {
2666
+ teamId: this.teamId,
2667
+ name,
2668
+ serviceConfigurations: {
2669
+ [_PortalClient.APNS_SERVICE_ID]: []
2670
+ },
2671
+ serviceConfigurationsRequests: [
2672
+ {
2673
+ isNew: true,
2674
+ serviceId: _PortalClient.APNS_SERVICE_ID,
2675
+ identifiers: {},
2676
+ environment: "all",
2677
+ scope: "team"
2678
+ }
2679
+ ],
2680
+ scope: "team"
2681
+ };
2682
+ const result = await this.postJson("account/auth/key/v2/create", body);
2683
+ const created = result.body?.keys?.[0];
2684
+ if (!created?.keyId) {
2685
+ throw new Error(`createApnsAuthKey: no key id in response. Body: ${result.rawText.slice(0, 400)}`);
2686
+ }
2687
+ const pem = await this.downloadApiKey(created.keyId);
2688
+ return { keyId: created.keyId, pem, raw: created };
2689
+ }
2690
+ /**
2691
+ * Find existing APNs auth keys in the team. Used by the wizard to
2692
+ * detect "you already have one" cases and offer to reuse rather than
2693
+ * proliferate keys.
2694
+ */
2695
+ async listApnsAuthKeys() {
2696
+ const list = await this.listApiKeys();
2697
+ return (list.keys ?? []).filter(
2698
+ (k) => (k.services ?? []).some((s) => s.id === _PortalClient.APNS_SERVICE_ID || /apns/i.test(s.name ?? ""))
2699
+ ).map((k) => ({
2700
+ keyId: k.keyId,
2701
+ keyName: k.keyName,
2702
+ canDownload: Boolean(k.canDownload)
2703
+ }));
2704
+ }
2705
+ /**
2706
+ * Download an existing App Store Connect API key as PEM. Spaceship's
2707
+ * `download_key` endpoint — GET with teamId + keyId in query string.
2708
+ * Returns the raw .p8 PEM text. Apple shows the .p8 exactly once
2709
+ * (after creation), so call this immediately after createApiKey.
2710
+ */
2711
+ async downloadApiKey(keyId) {
2712
+ const { status, rawText, contentType } = await this.getRaw("account/auth/key/download", {
2713
+ teamId: this.teamId,
2714
+ keyId
2715
+ });
2716
+ if (status !== 200) {
2717
+ throw new Error(`downloadApiKey failed (${status}): ${rawText.slice(0, 200)}`);
2718
+ }
2719
+ if (rawText.includes("BEGIN PRIVATE KEY")) return rawText;
2720
+ if (contentType.includes("json")) {
2721
+ try {
2722
+ const parsed = JSON.parse(rawText);
2723
+ const candidates = [
2724
+ parsed?.privateKey,
2725
+ parsed?.key?.privateKey,
2726
+ parsed?.keys?.[0]?.privateKey,
2727
+ parsed?.data?.attributes?.privateKey,
2728
+ parsed?.value
2729
+ ];
2730
+ for (const c of candidates) {
2731
+ if (typeof c === "string" && c.includes("BEGIN PRIVATE KEY")) return c;
2732
+ }
2733
+ } catch {
2734
+ }
2735
+ }
2736
+ throw new Error(`downloadApiKey returned no PEM. Content-Type=${contentType} body=${rawText.slice(0, 300)}`);
2737
+ }
2738
+ /**
2739
+ * List existing API keys via the portal. Used to discover the team's
2740
+ * issuer id — every key in the team shares the same issuer id, and
2741
+ * altool needs it alongside the key id and .p8 file.
2742
+ *
2743
+ * Form-encoded POST (NOT JSON), with pagination params, per Spaceship's
2744
+ * `list_keys` method. JSON returned 415 from this endpoint.
2745
+ */
2746
+ async listApiKeys() {
2747
+ const data = await this.post("account/auth/key/list", {
2748
+ teamId: this.teamId,
2749
+ pageNumber: "1",
2750
+ pageSize: "500",
2751
+ sort: "name=asc"
2752
+ });
2753
+ const keys = data?.keys ?? [];
2754
+ const issuerId = data?.issuerId ?? data?.issuer ?? data?.team?.issuerId ?? keys[0]?.issuerId ?? keys[0]?.issuer;
2755
+ return { keys, issuerId };
2756
+ }
2478
2757
  async post(path, params) {
2479
2758
  const body = new URLSearchParams();
2480
2759
  for (const [k, v] of Object.entries(params)) {
@@ -2542,6 +2821,31 @@ var init_portal = __esm({
2542
2821
  async deleteAppId(appIdId) {
2543
2822
  await this.post("account/ios/identifiers/deleteAppId.action", { teamId: this.teamId, appIdId });
2544
2823
  }
2824
+ /**
2825
+ * Toggle a capability (a.k.a. "service") on a bundle ID. Mirrors
2826
+ * Spaceship::Portal's `update_service` — the legacy portal endpoint
2827
+ * still works and is the only session-cookie-authenticated way to flip
2828
+ * capabilities like Push Notifications and Sign in with Apple from
2829
+ * outside the Apple Developer web UI.
2830
+ *
2831
+ * Common service codes (from Spaceship portal/portal_client.rb):
2832
+ * push → push notifications
2833
+ * APG3427HIY → Sign in with Apple
2834
+ * IAD53UNK2F → in-app purchase
2835
+ * homeKit → HomeKit
2836
+ * healthKit → HealthKit
2837
+ *
2838
+ * @param featureType Apple's internal capability key (string code or short name)
2839
+ * @param enabled true → turn on, false → turn off
2840
+ */
2841
+ async setBundleCapability(params) {
2842
+ await this.post("account/ios/identifiers/updateAppService.action", {
2843
+ teamId: this.teamId,
2844
+ displayId: params.appIdId,
2845
+ featureType: params.featureType,
2846
+ featureValue: params.enabled ? "on" : "off"
2847
+ });
2848
+ }
2545
2849
  // ── Certificates ───────────────────────────────────────────────────────
2546
2850
  async listCertificates(types) {
2547
2851
  const iosTypes = types ?? [
@@ -2559,12 +2863,21 @@ var init_portal = __esm({
2559
2863
  return data.certRequests ?? [];
2560
2864
  }
2561
2865
  async listPushCertificates() {
2866
+ const types = Array.from(
2867
+ /* @__PURE__ */ new Set([
2868
+ _PortalClient.CERT_TYPES.IOS_PRODUCTION_PUSH,
2869
+ _PortalClient.CERT_TYPES.IOS_DEVELOPMENT_PUSH,
2870
+ // Legacy push codes from OLDER_IOS_CERTIFICATE_TYPES
2871
+ "3BQKVH9I2X",
2872
+ "BKLRAVXMGM"
2873
+ ])
2874
+ );
2562
2875
  const data = await this.post("account/ios/certificate/listCertRequests.action", {
2563
2876
  teamId: this.teamId,
2564
2877
  pageSize: "500",
2565
2878
  pageNumber: "1",
2566
2879
  sort: "certRequestStatusCode=asc",
2567
- types: [_PortalClient.CERT_TYPES.IOS_PRODUCTION_PUSH, _PortalClient.CERT_TYPES.IOS_DEVELOPMENT_PUSH].join(",")
2880
+ types: types.join(",")
2568
2881
  });
2569
2882
  return data.certRequests ?? [];
2570
2883
  }
@@ -2706,18 +3019,332 @@ var init_portal = __esm({
2706
3019
  }
2707
3020
  });
2708
3021
 
3022
+ // src/client/iris.ts
3023
+ var IRIS_BASE, IrisClient, IrisAuthError;
3024
+ var init_iris = __esm({
3025
+ "src/client/iris.ts"() {
3026
+ IRIS_BASE = "https://appstoreconnect.apple.com/iris/v1";
3027
+ IrisClient = class {
3028
+ constructor(session) {
3029
+ this.session = session;
3030
+ this.cookieStr = Object.entries(session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
3031
+ }
3032
+ session;
3033
+ cookieStr;
3034
+ buildHeaders() {
3035
+ return {
3036
+ Cookie: this.cookieStr,
3037
+ Accept: "application/vnd.api+json, application/json, text/plain, */*",
3038
+ "Content-Type": "application/vnd.api+json",
3039
+ "X-Requested-With": "XMLHttpRequest",
3040
+ // Mimic the App Store Connect web UI's user agent
3041
+ "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"
3042
+ };
3043
+ }
3044
+ /**
3045
+ * Issue a request and absorb any new cookies (Apple rotates session
3046
+ * cookies on a lot of iris calls).
3047
+ */
3048
+ async request(method, path, body) {
3049
+ const headers = this.buildHeaders();
3050
+ if (process.env.FASTNODE_DEBUG) {
3051
+ console.log(`[iris] ${method} ${path}${body ? ` body=${JSON.stringify(body).slice(0, 200)}` : ""}`);
3052
+ }
3053
+ const res = await fetch(`${IRIS_BASE}${path}`, {
3054
+ method,
3055
+ headers,
3056
+ body: body ? JSON.stringify(body) : void 0
3057
+ });
3058
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
3059
+ const m = raw.match(/^([^=]+)=([^;]*)/);
3060
+ if (m) {
3061
+ this.session.cookies[m[1]] = m[2];
3062
+ }
3063
+ }
3064
+ this.cookieStr = Object.entries(this.session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
3065
+ const text = await res.text();
3066
+ let parsed = null;
3067
+ try {
3068
+ parsed = text ? JSON.parse(text) : null;
3069
+ } catch {
3070
+ }
3071
+ if (process.env.FASTNODE_DEBUG) {
3072
+ console.log(`[iris] \u2192 ${res.status} ${text.slice(0, 300)}`);
3073
+ }
3074
+ return { status: res.status, body: parsed, rawText: text };
3075
+ }
3076
+ /**
3077
+ * List existing API keys. Used both as a sanity check (the user has
3078
+ * permission) and to discover the team's issuer id (which is the same
3079
+ * for every key in the team and is needed by altool).
3080
+ *
3081
+ * Important: Apple's iris endpoint rejects most query parameters with
3082
+ * `PARAMETER_ERROR.INVALID`. Call with no params and let the API return
3083
+ * its default page.
3084
+ */
3085
+ async listApiKeys() {
3086
+ const { status, body, rawText } = await this.request("GET", "/apiKeys");
3087
+ if (status === 403) {
3088
+ throw new IrisAuthError(
3089
+ "You don't have permission to manage App Store Connect API keys (this requires the Account Holder or Admin role)."
3090
+ );
3091
+ }
3092
+ if (status >= 400 || !body) {
3093
+ throw new Error(`iris listApiKeys failed (${status}): ${rawText.slice(0, 200)}`);
3094
+ }
3095
+ const issuerId = body?.meta?.issuerId ?? body?.data?.[0]?.attributes?.issuerId;
3096
+ const keys = (body.data ?? []).map((d) => ({
3097
+ id: d.id,
3098
+ kid: d.attributes?.kid ?? d.id,
3099
+ nickname: d.attributes?.nickname ?? "",
3100
+ issuerId: d.attributes?.issuerId ?? issuerId,
3101
+ roles: d.attributes?.roles ?? [],
3102
+ allAppsVisible: d.attributes?.allAppsVisible ?? true
3103
+ }));
3104
+ return { keys, issuerId };
3105
+ }
3106
+ /**
3107
+ * Create a new API key. Apple returns the .p8 PEM exactly once in the
3108
+ * create response — capture it immediately or the key is dead weight.
3109
+ *
3110
+ * `keyType` is required by Apple's iris API:
3111
+ * • PUBLIC_API — the standard team-wide API key (what TestFlight uploads need)
3112
+ * • MARKETING_API — for App Analytics access only
3113
+ */
3114
+ async createApiKey(input) {
3115
+ const requestBody = {
3116
+ data: {
3117
+ type: "apiKeys",
3118
+ attributes: {
3119
+ nickname: input.nickname,
3120
+ roles: input.roles ?? ["APP_MANAGER"],
3121
+ allAppsVisible: input.allAppsVisible ?? true,
3122
+ keyType: input.keyType ?? "PUBLIC_API"
3123
+ }
3124
+ }
3125
+ };
3126
+ console.log("[iris] createApiKey request:", JSON.stringify(requestBody));
3127
+ const { status, body, rawText } = await this.request("POST", "/apiKeys", requestBody);
3128
+ console.log(`[iris] createApiKey response: ${status} ${rawText.slice(0, 800)}`);
3129
+ if (status === 403) {
3130
+ throw new IrisAuthError(
3131
+ "You don't have permission to create App Store Connect API keys (this requires the Account Holder or Admin role)."
3132
+ );
3133
+ }
3134
+ if (status >= 400 || !body) {
3135
+ const msg = body?.errors?.[0]?.detail ?? body?.errors?.[0]?.title ?? rawText.slice(0, 300) ?? `iris createApiKey failed (${status})`;
3136
+ throw new Error(msg);
3137
+ }
3138
+ const data = body.data ?? body;
3139
+ if (!data?.id) {
3140
+ throw new Error(`iris createApiKey returned no key id. Raw response: ${rawText.slice(0, 400)}`);
3141
+ }
3142
+ const key = {
3143
+ id: data.id,
3144
+ kid: data.attributes?.kid ?? data.id,
3145
+ nickname: data.attributes?.nickname ?? input.nickname,
3146
+ privateKey: data.attributes?.privateKey,
3147
+ issuerId: data.attributes?.issuerId,
3148
+ roles: data.attributes?.roles ?? input.roles ?? ["APP_MANAGER"],
3149
+ allAppsVisible: data.attributes?.allAppsVisible ?? true
3150
+ };
3151
+ return key;
3152
+ }
3153
+ /**
3154
+ * Fetch the .p8 PEM AND the team's issuer id for a previously-created
3155
+ * ASC API key. Both come back in a single request via:
3156
+ *
3157
+ * GET /iris/v1/apiKeys/{id}?fields[apiKeys]=privateKey&include=provider
3158
+ *
3159
+ * Sparse fieldset requesting ONLY `privateKey` is the magic that makes
3160
+ * Apple actually include the (base64-encoded) PEM in the response —
3161
+ * the default view returns `privateKey: null`. The `include=provider`
3162
+ * pulls the team's contentProvider record into `included[]`, and its
3163
+ * `id` field IS the issuer id altool needs (a UUID like
3164
+ * `cfadaf94-44ca-4c2c-8fa6-ecc28d3c7d0d`).
3165
+ *
3166
+ * Reverse-engineered from @expo/apple-utils v2.1.21 (the package that
3167
+ * powers EAS CLI), specifically the `ApiKey.downloadAsync` method
3168
+ * combined with `ApiKey.infoAsync` reading `provider?.id`.
3169
+ *
3170
+ * Returns `{ pem, issuerId }`, or `null` if Apple won't return the
3171
+ * private key (typically because the key was already downloaded once).
3172
+ */
3173
+ async fetchApiKeyPrivateKey(keyId) {
3174
+ try {
3175
+ const url = `${IRIS_BASE}/apiKeys/${keyId}?fields%5BapiKeys%5D=privateKey&include=provider`;
3176
+ const res = await fetch(url, {
3177
+ headers: {
3178
+ ...this.buildHeaders(),
3179
+ Accept: "application/vnd.api+json, application/json, text/plain, */*"
3180
+ }
3181
+ });
3182
+ const text = await res.text();
3183
+ console.log(`[iris] fetchApiKeyPrivateKey \u2192 ${res.status} (${text.length} bytes)`);
3184
+ if (res.status !== 200) {
3185
+ console.log(`[iris] err: ${text.slice(0, 300)}`);
3186
+ return null;
3187
+ }
3188
+ const body = JSON.parse(text);
3189
+ const encoded = body?.data?.attributes?.privateKey;
3190
+ if (typeof encoded !== "string" || encoded.length === 0) {
3191
+ console.log(`[iris] no privateKey in response`);
3192
+ return null;
3193
+ }
3194
+ const pem = Buffer.from(encoded, "base64").toString("utf-8");
3195
+ if (!pem.includes("BEGIN PRIVATE KEY")) {
3196
+ console.log(`[iris] decoded body didn't contain PEM marker`);
3197
+ return null;
3198
+ }
3199
+ const provider = (body?.included ?? []).find((i) => i.type === "contentProviders");
3200
+ const issuerId = provider?.id;
3201
+ console.log(`[iris] \u2713 PEM ${pem.length} bytes, issuerId=${issuerId ?? "(missing)"}`);
3202
+ return { pem, issuerId };
3203
+ } catch (err) {
3204
+ console.log(`[iris] fetchApiKeyPrivateKey error: ${err.message}`);
3205
+ return null;
3206
+ }
3207
+ }
3208
+ /**
3209
+ * @deprecated Kept as a fallback probe for debugging. The real path
3210
+ * is the sparse fieldset above — everything in this brute-force list
3211
+ * returned 404 during testing. Left here so we can re-verify if Apple
3212
+ * changes anything.
3213
+ */
3214
+ async fetchApiKeyPrivateKeyBruteForce(keyId) {
3215
+ const attempts = [
3216
+ // Sparse fieldsets — JSON-API spec lets us request specific attributes.
3217
+ // Apple may exclude privateKey by default and return it only when asked.
3218
+ { kind: "GET", path: `/apiKeys/${keyId}?fields[apiKeys]=privateKey,kid,issuerId,nickname,roles` },
3219
+ { kind: "GET", path: `/apiKeys/${keyId}?include=privateKey` },
3220
+ { kind: "GET", path: `/apiKeys/${keyId}?fields[apiKeys]=privateKey` },
3221
+ // Custom Accept headers — maybe Apple negotiates content type
3222
+ { kind: "GET", path: `/apiKeys/${keyId}`, accept: "application/x-pem-file" },
3223
+ { kind: "GET", path: `/apiKeys/${keyId}`, accept: "application/octet-stream" },
3224
+ { kind: "GET", path: `/apiKeys/${keyId}/p8`, accept: "application/x-pem-file" },
3225
+ // POST variants — Apple sometimes uses POST to mint a download token
3226
+ { kind: "POST", path: `/apiKeys/${keyId}/secrets`, body: {} },
3227
+ { kind: "POST", path: `/apiKeys/${keyId}/regenerate`, body: {} },
3228
+ { kind: "POST", path: `/apiKeys/${keyId}/p8`, body: {} },
3229
+ // Bare GET (last) — confirms canonical fields, in case my parser missed any
3230
+ { kind: "GET", path: `/apiKeys/${keyId}` },
3231
+ // Different host — the legacy developer.apple.com user-and-access namespace
3232
+ { kind: "ABSOLUTE", url: `https://appstoreconnect.apple.com/api/v1/apiKeys/${keyId}/p8` },
3233
+ { kind: "ABSOLUTE", url: `https://appstoreconnect.apple.com/access/api/keys/${keyId}/download` }
3234
+ ];
3235
+ for (const attempt of attempts) {
3236
+ try {
3237
+ let result;
3238
+ if (attempt.kind === "GET") {
3239
+ result = await this.requestWithHeaders("GET", attempt.path, void 0, {
3240
+ ...attempt.accept ? { Accept: attempt.accept } : {}
3241
+ });
3242
+ console.log(`[iris] fetchApiKeyPrivateKey GET ${attempt.path}${attempt.accept ? ` accept=${attempt.accept}` : ""} \u2192 ${result.status} (${result.rawText.length} bytes)`);
3243
+ } else if (attempt.kind === "POST") {
3244
+ result = await this.request("POST", attempt.path, attempt.body);
3245
+ console.log(`[iris] fetchApiKeyPrivateKey POST ${attempt.path} \u2192 ${result.status} (${result.rawText.length} bytes)`);
3246
+ } else {
3247
+ result = await this.requestAbsolute("GET", attempt.url);
3248
+ console.log(`[iris] fetchApiKeyPrivateKey GET ${attempt.url} \u2192 ${result.status} (${result.rawText.length} bytes)`);
3249
+ }
3250
+ if (result.status === 200) {
3251
+ console.log(`[iris] body: ${result.rawText.slice(0, 1500)}`);
3252
+ }
3253
+ if (result.status !== 200) continue;
3254
+ if (result.rawText && result.rawText.includes("BEGIN PRIVATE KEY")) {
3255
+ console.log(`[iris] \u2713 found PEM (plain text response)`);
3256
+ return result.rawText;
3257
+ }
3258
+ if (result.body) {
3259
+ const candidates = [
3260
+ result.body?.data?.attributes?.privateKey,
3261
+ result.body?.data?.attributes?.p8,
3262
+ result.body?.data?.attributes?.privateKeyContent,
3263
+ result.body?.data?.attributes?.kpem,
3264
+ result.body?.data?.attributes?.secret,
3265
+ result.body?.data?.attributes?.keyContent,
3266
+ result.body?.privateKey,
3267
+ result.body?.p8,
3268
+ result.body?.value,
3269
+ result.body?.secret
3270
+ ];
3271
+ for (const c of candidates) {
3272
+ if (typeof c === "string" && c.includes("BEGIN PRIVATE KEY")) {
3273
+ console.log(`[iris] \u2713 found PEM in JSON response`);
3274
+ return c;
3275
+ }
3276
+ }
3277
+ }
3278
+ } catch (err) {
3279
+ console.log(`[iris] fetchApiKeyPrivateKey attempt failed: ${err.message}`);
3280
+ }
3281
+ }
3282
+ console.log(`[iris] fetchApiKeyPrivateKey: no working download path for key ${keyId}`);
3283
+ return null;
3284
+ }
3285
+ /** Internal: like request() but lets us override headers. */
3286
+ async requestWithHeaders(method, path, body, extraHeaders = {}) {
3287
+ const headers = { ...this.buildHeaders(), ...extraHeaders };
3288
+ const res = await fetch(`${IRIS_BASE}${path}`, {
3289
+ method,
3290
+ headers,
3291
+ body: body ? JSON.stringify(body) : void 0
3292
+ });
3293
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
3294
+ const m = raw.match(/^([^=]+)=([^;]*)/);
3295
+ if (m) this.session.cookies[m[1]] = m[2];
3296
+ }
3297
+ this.cookieStr = Object.entries(this.session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
3298
+ const text = await res.text();
3299
+ let parsed = null;
3300
+ try {
3301
+ parsed = text ? JSON.parse(text) : null;
3302
+ } catch {
3303
+ }
3304
+ return { status: res.status, body: parsed, rawText: text };
3305
+ }
3306
+ /** Internal: fetch an absolute URL with the session cookies. */
3307
+ async requestAbsolute(method, url) {
3308
+ const headers = this.buildHeaders();
3309
+ const res = await fetch(url, { method, headers });
3310
+ const text = await res.text();
3311
+ let parsed = null;
3312
+ try {
3313
+ parsed = text ? JSON.parse(text) : null;
3314
+ } catch {
3315
+ }
3316
+ return { status: res.status, body: parsed, rawText: text };
3317
+ }
3318
+ async deleteApiKey(keyId) {
3319
+ await this.request("DELETE", `/apiKeys/${keyId}`);
3320
+ }
3321
+ };
3322
+ IrisAuthError = class extends Error {
3323
+ constructor(message) {
3324
+ super(message);
3325
+ this.name = "IrisAuthError";
3326
+ }
3327
+ };
3328
+ }
3329
+ });
3330
+
2709
3331
  // src/client/index.ts
2710
3332
  var client_exports = {};
2711
3333
  __export(client_exports, {
2712
3334
  AppStoreConnectClient: () => AppStoreConnectClient,
2713
3335
  AppStoreConnectError: () => AppStoreConnectError,
3336
+ IrisAuthError: () => IrisAuthError,
3337
+ IrisClient: () => IrisClient,
2714
3338
  PortalClient: () => PortalClient,
2715
3339
  ProgramLicenseAgreementError: () => ProgramLicenseAgreementError,
2716
3340
  RateLimitError: () => RateLimitError,
2717
3341
  RequestClient: () => RequestClient,
2718
3342
  TokenManager: () => TokenManager,
3343
+ beginLogin: () => beginLogin,
2719
3344
  checkPortalResponse: () => checkPortalResponse,
2720
- loginWithAppleId: () => loginWithAppleId
3345
+ loginWithAppleId: () => loginWithAppleId,
3346
+ requestSmsCode: () => requestSmsCode,
3347
+ submitTwoFactorCode: () => submitTwoFactorCode
2721
3348
  });
2722
3349
  var AppStoreConnectClient;
2723
3350
  var init_client = __esm({
@@ -2762,6 +3389,7 @@ var init_client = __esm({
2762
3389
  init_request();
2763
3390
  init_apple_session();
2764
3391
  init_portal();
3392
+ init_iris();
2765
3393
  AppStoreConnectClient = class {
2766
3394
  http;
2767
3395
  certificates;
@@ -3004,8 +3632,8 @@ async function revokeExpiredCertificates(options) {
3004
3632
  return count;
3005
3633
  }
3006
3634
  async function importToKeychain(certData, keychainPath, keychainPassword) {
3007
- const { tmpdir: tmpdir9 } = await import('os');
3008
- const tempCertPath = join(tmpdir9(), `fastnode-cert-${Date.now()}.cer`);
3635
+ const { tmpdir: tmpdir10 } = await import('os');
3636
+ const tempCertPath = join(tmpdir10(), `fastnode-cert-${Date.now()}.cer`);
3009
3637
  await writeFile(tempCertPath, certData);
3010
3638
  try {
3011
3639
  const args = [
@@ -3994,23 +4622,13 @@ var init_produce = __esm({
3994
4622
  };
3995
4623
  }
3996
4624
  });
3997
-
3998
- // src/pilot/index.ts
3999
- var pilot_exports = {};
4000
- __export(pilot_exports, {
4001
- distributeToGroups: () => distributeToGroups,
4002
- expireBuild: () => expireBuild,
4003
- setChangelog: () => setChangelog,
4004
- uploadToTestFlight: () => uploadToTestFlight,
4005
- waitForProcessing: () => waitForProcessing
4006
- });
4007
- async function uploadToTestFlight(options) {
4625
+ async function uploadWithAltool(options) {
4008
4626
  if (!existsSync(options.ipaPath)) {
4009
4627
  throw new Error(`File not found: ${options.ipaPath}`);
4010
4628
  }
4011
4629
  const platform = options.platform ?? "ios";
4012
4630
  const timeout = options.timeout ?? 18e5;
4013
- const keyDir = await mkdtemp(join(tmpdir(), "fastnode-key-"));
4631
+ const keyDir = await mkdtemp(join(tmpdir(), "fastnode-altool-"));
4014
4632
  const keyPath = join(keyDir, `AuthKey_${options.apiKeyId}.p8`);
4015
4633
  await writeFile(keyPath, options.apiPrivateKey);
4016
4634
  return new Promise((resolve4) => {
@@ -4034,24 +4652,228 @@ async function uploadToTestFlight(options) {
4034
4652
  const timer = setTimeout(() => {
4035
4653
  child.kill("SIGTERM");
4036
4654
  }, timeout);
4037
- child.stdout.on("data", (data) => {
4655
+ const onData = (data) => {
4038
4656
  const chunk = data.toString();
4039
4657
  output += chunk;
4040
4658
  options.onOutput?.(chunk);
4659
+ };
4660
+ child.stdout.on("data", onData);
4661
+ child.stderr.on("data", onData);
4662
+ child.on("close", async (code) => {
4663
+ clearTimeout(timer);
4664
+ await unlink(keyPath).catch(() => {
4665
+ });
4666
+ await rm(keyDir, { recursive: true, force: true }).catch(() => {
4667
+ });
4668
+ resolve4({ success: code === 0, output, backend: "altool" });
4041
4669
  });
4042
- child.stderr.on("data", (data) => {
4670
+ });
4671
+ }
4672
+ var init_upload_altool = __esm({
4673
+ "src/pilot/upload-altool.ts"() {
4674
+ }
4675
+ });
4676
+ async function resolveTransporter() {
4677
+ const override = process.env.ITMSTRANSPORTER_BIN;
4678
+ if (override) {
4679
+ if (!existsSync(override)) {
4680
+ throw new Error(
4681
+ `ITMSTRANSPORTER_BIN is set to "${override}" but that path does not exist.`
4682
+ );
4683
+ }
4684
+ return override;
4685
+ }
4686
+ const candidates = process.platform === "darwin" ? [
4687
+ // Xcode 15+: Transporter.app bundled with Xcode
4688
+ "/Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/itms/bin/iTMSTransporter",
4689
+ // Standalone Transporter.app from the Mac App Store
4690
+ "/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter"
4691
+ ] : [
4692
+ "/usr/local/itms/bin/iTMSTransporter",
4693
+ "/opt/itms/bin/iTMSTransporter",
4694
+ "/usr/local/bin/iTMSTransporter"
4695
+ ];
4696
+ for (const path of candidates) {
4697
+ if (existsSync(path)) return path;
4698
+ }
4699
+ throw new Error(
4700
+ [
4701
+ "iTMSTransporter not found.",
4702
+ "",
4703
+ "Install options:",
4704
+ process.platform === "darwin" ? " - Install Xcode (includes Transporter)" : " - Download Apple's standalone iTMSTransporter package for Linux",
4705
+ " - Or set ITMSTRANSPORTER_BIN to the absolute path of the binary",
4706
+ "",
4707
+ `Searched: ${candidates.join(", ")}`
4708
+ ].join("\n")
4709
+ );
4710
+ }
4711
+ async function verifyJava() {
4712
+ return new Promise((resolve4, reject) => {
4713
+ const child = spawn("java", ["-version"], { stdio: ["ignore", "pipe", "pipe"] });
4714
+ let stderr = "";
4715
+ child.stderr.on("data", (d) => {
4716
+ stderr += d.toString();
4717
+ });
4718
+ child.on("error", (err) => {
4719
+ reject(
4720
+ new Error(
4721
+ `java not found on PATH (${err.message}). Install Java 17 LTS and ensure it is on PATH.`
4722
+ )
4723
+ );
4724
+ });
4725
+ child.on("close", (code) => {
4726
+ if (code !== 0) {
4727
+ reject(new Error(`java -version exited ${code}: ${stderr}`));
4728
+ return;
4729
+ }
4730
+ const match = stderr.match(/version "([^"]+)"/);
4731
+ resolve4({ version: match?.[1] ?? "unknown" });
4732
+ });
4733
+ });
4734
+ }
4735
+ var init_transporter_install = __esm({
4736
+ "src/pilot/transporter-install.ts"() {
4737
+ }
4738
+ });
4739
+ async function uploadWithTransporter(options) {
4740
+ if (!existsSync(options.ipaPath)) {
4741
+ throw new Error(`File not found: ${options.ipaPath}`);
4742
+ }
4743
+ await verifyJava();
4744
+ const transporterBin = await resolveTransporter();
4745
+ const platform = options.platform ?? "ios";
4746
+ const timeout = options.timeout ?? 18e5;
4747
+ const keyDir = await mkdtemp(join(tmpdir(), "fastnode-itms-"));
4748
+ const keyPath = join(keyDir, `AuthKey_${options.apiKeyId}.p8`);
4749
+ await writeFile(keyPath, options.apiPrivateKey);
4750
+ return new Promise((resolve4) => {
4751
+ const args = [
4752
+ "-m",
4753
+ "upload",
4754
+ "-assetFile",
4755
+ options.ipaPath,
4756
+ "-apiKey",
4757
+ options.apiKeyId,
4758
+ "-apiIssuer",
4759
+ options.apiIssuerId,
4760
+ // WONoPause prevents the JVM from waiting on stdin in non-TTY
4761
+ // environments (CI, LaunchAgent) where it would hang forever.
4762
+ "-WONoPause",
4763
+ "true"
4764
+ ];
4765
+ if (platform !== "ios") {
4766
+ args.push("-bundleType", platform);
4767
+ }
4768
+ const child = spawn(transporterBin, args, {
4769
+ env: {
4770
+ ...process.env,
4771
+ API_PRIVATE_KEYS_DIR: keyDir
4772
+ },
4773
+ stdio: ["ignore", "pipe", "pipe"]
4774
+ });
4775
+ let output = "";
4776
+ const timer = setTimeout(() => {
4777
+ child.kill("SIGTERM");
4778
+ }, timeout);
4779
+ const onData = (data) => {
4043
4780
  const chunk = data.toString();
4044
4781
  output += chunk;
4045
4782
  options.onOutput?.(chunk);
4046
- });
4783
+ };
4784
+ child.stdout.on("data", onData);
4785
+ child.stderr.on("data", onData);
4047
4786
  child.on("close", async (code) => {
4048
4787
  clearTimeout(timer);
4049
- await unlink(keyPath).catch(() => {
4788
+ await rm(keyDir, { recursive: true, force: true }).catch(() => {
4789
+ });
4790
+ resolve4({ success: code === 0, output, backend: "transporter" });
4791
+ });
4792
+ child.on("error", async (err) => {
4793
+ clearTimeout(timer);
4794
+ await rm(keyDir, { recursive: true, force: true }).catch(() => {
4795
+ });
4796
+ resolve4({
4797
+ success: false,
4798
+ output: output + `
4799
+ Process error: ${err.message}`,
4800
+ backend: "transporter"
4050
4801
  });
4051
- resolve4({ success: code === 0, output });
4052
4802
  });
4053
4803
  });
4054
4804
  }
4805
+ var init_upload_transporter = __esm({
4806
+ "src/pilot/upload-transporter.ts"() {
4807
+ init_transporter_install();
4808
+ }
4809
+ });
4810
+
4811
+ // src/pilot/classify.ts
4812
+ function classifyUploadError(output) {
4813
+ for (const { pattern, kind } of RULES) {
4814
+ if (pattern.test(output)) return kind;
4815
+ }
4816
+ return null;
4817
+ }
4818
+ function isTransientUploadError(output) {
4819
+ return classifyUploadError(output) !== null;
4820
+ }
4821
+ var RULES;
4822
+ var init_classify = __esm({
4823
+ "src/pilot/classify.ts"() {
4824
+ RULES = [
4825
+ // Apple's finalize-upload call returning 5xx — the most common transient
4826
+ // failure we see in practice. Example:
4827
+ // "CHANGE UPLOAD STATE TO COMPLETE (UPLOADING ASSET DESCRIPTION):
4828
+ // received status code 500; internal server error."
4829
+ { pattern: /CHANGE UPLOAD STATE TO COMPLETE/i, kind: "finalize_failed" },
4830
+ // Generic 5xx from ASC backend services
4831
+ { pattern: /status code 5\d\d/i, kind: "apple_5xx" },
4832
+ { pattern: /internal server error/i, kind: "apple_5xx" },
4833
+ { pattern: /Service Unavailable/i, kind: "apple_5xx" },
4834
+ { pattern: /Bad Gateway/i, kind: "apple_5xx" },
4835
+ // Network-level interruptions during the upload body transfer
4836
+ { pattern: /Connection reset/i, kind: "connection_reset" },
4837
+ { pattern: /SocketException/i, kind: "connection_reset" },
4838
+ { pattern: /Broken pipe/i, kind: "connection_reset" },
4839
+ // Timeouts — distinct from resets because the other side may have
4840
+ // actually processed the request, so dedupe logic may matter.
4841
+ { pattern: /Read timed out/i, kind: "network_timeout" },
4842
+ { pattern: /SocketTimeoutException/i, kind: "network_timeout" },
4843
+ { pattern: /Operation timed out/i, kind: "network_timeout" },
4844
+ // JVM-internal errors from iTMSTransporter (OOM, thread interruption, etc.)
4845
+ { pattern: /java\.lang\.OutOfMemoryError/i, kind: "transporter_jvm_error" },
4846
+ { pattern: /EOFException/i, kind: "transporter_jvm_error" }
4847
+ ];
4848
+ }
4849
+ });
4850
+
4851
+ // src/pilot/index.ts
4852
+ var pilot_exports = {};
4853
+ __export(pilot_exports, {
4854
+ classifyUploadError: () => classifyUploadError,
4855
+ distributeToGroups: () => distributeToGroups,
4856
+ expireBuild: () => expireBuild,
4857
+ isTransientUploadError: () => isTransientUploadError,
4858
+ resolveTransporter: () => resolveTransporter,
4859
+ setChangelog: () => setChangelog,
4860
+ uploadToTestFlight: () => uploadToTestFlight,
4861
+ uploadWithAltool: () => uploadWithAltool,
4862
+ uploadWithTransporter: () => uploadWithTransporter,
4863
+ verifyJava: () => verifyJava,
4864
+ waitForProcessing: () => waitForProcessing
4865
+ });
4866
+ async function uploadToTestFlight(options) {
4867
+ const backend = selectBackend(options.backend ?? "auto");
4868
+ if (backend === "altool") {
4869
+ return uploadWithAltool(options);
4870
+ }
4871
+ return uploadWithTransporter(options);
4872
+ }
4873
+ function selectBackend(pref) {
4874
+ if (pref !== "auto") return pref;
4875
+ return process.platform === "darwin" ? "altool" : "transporter";
4876
+ }
4055
4877
  async function waitForProcessing(client, params) {
4056
4878
  const timeout = params.timeout ?? 18e5;
4057
4879
  const pollInterval = params.pollInterval ?? 3e4;
@@ -4172,6 +4994,12 @@ function sleep2(ms) {
4172
4994
  var init_pilot = __esm({
4173
4995
  "src/pilot/index.ts"() {
4174
4996
  init_request();
4997
+ init_upload_altool();
4998
+ init_upload_transporter();
4999
+ init_upload_altool();
5000
+ init_upload_transporter();
5001
+ init_classify();
5002
+ init_transporter_install();
4175
5003
  }
4176
5004
  });
4177
5005
  async function loadMetadata(metadataPath) {
@@ -4275,9 +5103,9 @@ var init_platform = __esm({
4275
5103
  // src/xcode/upload.ts
4276
5104
  var upload_exports = {};
4277
5105
  __export(upload_exports, {
4278
- uploadWithAltool: () => uploadWithAltool
5106
+ uploadWithAltool: () => uploadWithAltool2
4279
5107
  });
4280
- async function uploadWithAltool(options) {
5108
+ async function uploadWithAltool2(options) {
4281
5109
  requireMacOS("altool upload");
4282
5110
  let keyDir;
4283
5111
  if (options.apiPrivateKey && !options.apiKeyPath) {
@@ -4386,8 +5214,8 @@ async function uploadToAppStore(options) {
4386
5214
  if (!options.skipBinaryUpload) {
4387
5215
  const binaryPath = options.ipaPath ?? options.pkgPath;
4388
5216
  if (binaryPath && options.apiKeyId && options.apiIssuerId) {
4389
- const { uploadWithAltool: uploadWithAltool2 } = await Promise.resolve().then(() => (init_upload(), upload_exports));
4390
- await uploadWithAltool2({
5217
+ const { uploadWithAltool: uploadWithAltool3 } = await Promise.resolve().then(() => (init_upload(), upload_exports));
5218
+ await uploadWithAltool3({
4391
5219
  filePath: binaryPath,
4392
5220
  apiKeyId: options.apiKeyId,
4393
5221
  apiIssuerId: options.apiIssuerId,
@@ -5363,8 +6191,8 @@ async function captureScreenshots(options) {
5363
6191
  const outputDir = options.outputDirectory ?? join(process.cwd(), "screenshots");
5364
6192
  const retries = options.numberOfRetries ?? 1;
5365
6193
  if (options.clearPreviousScreenshots && existsSync(outputDir)) {
5366
- const { rm: rm3 } = await import('fs/promises');
5367
- await rm3(outputDir, { recursive: true, force: true });
6194
+ const { rm: rm5 } = await import('fs/promises');
6195
+ await rm5(outputDir, { recursive: true, force: true });
5368
6196
  }
5369
6197
  mkdirSync(outputDir, { recursive: true });
5370
6198
  const screenshots = {};
@@ -6743,14 +7571,14 @@ var init_building = __esm({
6743
7571
  name: "clean_build_artifacts",
6744
7572
  description: "Remove build artifacts (IPA, dSYM, etc.)",
6745
7573
  async run(params, ctx) {
6746
- const { rm: rm3 } = await import('fs/promises');
7574
+ const { rm: rm5 } = await import('fs/promises');
6747
7575
  const paths = [
6748
7576
  ctx.get("IPA_OUTPUT_PATH"),
6749
7577
  ctx.get("PKG_OUTPUT_PATH"),
6750
7578
  ctx.get("DSYM_OUTPUT_PATH")
6751
7579
  ].filter(Boolean);
6752
7580
  for (const p of paths) {
6753
- await rm3(p, { force: true }).catch(() => {
7581
+ await rm5(p, { force: true }).catch(() => {
6754
7582
  });
6755
7583
  }
6756
7584
  return { cleaned: paths };
@@ -7749,6 +8577,7 @@ init_review_submissions();
7749
8577
  // src/cert/index.ts
7750
8578
  var cert_exports2 = {};
7751
8579
  __export(cert_exports2, {
8580
+ bundleP12: () => bundleP12,
7752
8581
  createCertificate: () => createCertificate,
7753
8582
  downloadCertificate: () => downloadCertificate,
7754
8583
  findValidCertificates: () => findValidCertificates,
@@ -7759,6 +8588,41 @@ __export(cert_exports2, {
7759
8588
  });
7760
8589
  init_cert();
7761
8590
  init_csr();
8591
+ function derBufferToPem(der) {
8592
+ const b64 = der.toString("base64");
8593
+ const lines = [];
8594
+ for (let i = 0; i < b64.length; i += 64) lines.push(b64.slice(i, i + 64));
8595
+ return `-----BEGIN CERTIFICATE-----
8596
+ ${lines.join("\n")}
8597
+ -----END CERTIFICATE-----
8598
+ `;
8599
+ }
8600
+ function normalizeCertPem(input) {
8601
+ if (typeof input === "string") {
8602
+ if (input.includes("BEGIN CERTIFICATE")) return input;
8603
+ const b64 = input.replace(/\s+/g, "");
8604
+ return derBufferToPem(Buffer.from(b64, "base64"));
8605
+ }
8606
+ return derBufferToPem(input);
8607
+ }
8608
+ function bundleP12(options) {
8609
+ const certPem = normalizeCertPem(options.certificate);
8610
+ const cert = forge.pki.certificateFromPem(certPem);
8611
+ const privateKey = forge.pki.privateKeyFromPem(options.privateKeyPem);
8612
+ const friendlyName = options.friendlyName ?? "fastnode signing identity";
8613
+ const p12Asn1 = forge.pkcs12.toPkcs12Asn1(
8614
+ privateKey,
8615
+ [cert],
8616
+ options.password,
8617
+ {
8618
+ friendlyName,
8619
+ generateLocalKeyId: true,
8620
+ algorithm: "3des"
8621
+ }
8622
+ );
8623
+ const der = forge.asn1.toDer(p12Asn1).getBytes();
8624
+ return Buffer.from(der, "binary");
8625
+ }
7762
8626
 
7763
8627
  // src/index.ts
7764
8628
  init_sigh();
@@ -7787,7 +8651,7 @@ __export(xcode_exports, {
7787
8651
  setBuildNumber: () => setBuildNumber,
7788
8652
  setVersionNumber: () => setVersionNumber,
7789
8653
  updateSigningSettings: () => updateSigningSettings,
7790
- uploadWithAltool: () => uploadWithAltool
8654
+ uploadWithAltool: () => uploadWithAltool2
7791
8655
  });
7792
8656
  init_platform();
7793
8657
 
@@ -8106,6 +8970,322 @@ init_lane();
8106
8970
  init_lane();
8107
8971
  init_context();
8108
8972
  init_actions();
8973
+
8974
+ // src/setup/one-click.ts
8975
+ init_portal();
8976
+ init_iris();
8977
+ init_client();
8978
+ init_csr();
8979
+ var DIST_CERT_TYPE = PortalClient.CERT_TYPES.IOS_DISTRIBUTION;
8980
+ function randomPassword(length = 32) {
8981
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
8982
+ const bytes = randomBytes(length);
8983
+ let out = "";
8984
+ for (let i = 0; i < length; i++) out += alphabet[bytes[i] % alphabet.length];
8985
+ return out;
8986
+ }
8987
+ function extractProfileMetadata(profile) {
8988
+ const text = profile.toString("binary");
8989
+ const start = text.indexOf("<?xml");
8990
+ const end = text.indexOf("</plist>");
8991
+ if (start < 0 || end < 0) return {};
8992
+ const plistXml = text.slice(start, end + "</plist>".length);
8993
+ const nameMatch = plistXml.match(/<key>Name<\/key>\s*<string>([^<]+)<\/string>/);
8994
+ const uuidMatch = plistXml.match(/<key>UUID<\/key>\s*<string>([^<]+)<\/string>/);
8995
+ return { name: nameMatch?.[1], uuid: uuidMatch?.[1] };
8996
+ }
8997
+ function classifyPushCertType(typeString) {
8998
+ if (!typeString) return "other";
8999
+ const t = typeString.toLowerCase();
9000
+ if (t.includes("production")) return "production";
9001
+ if (t.includes("development") || t.includes("sandbox")) return "sandbox";
9002
+ return "other";
9003
+ }
9004
+ function buildProfileName(appName, existingNames) {
9005
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9006
+ const base = `${appName} App Store ${today}`;
9007
+ const taken = new Set(existingNames);
9008
+ if (!taken.has(base)) return base;
9009
+ let n = 1;
9010
+ while (taken.has(`${base} (${n})`)) n++;
9011
+ return `${base} (${n})`;
9012
+ }
9013
+ async function oneClickListTeams(session) {
9014
+ const portal = new PortalClient(session, "");
9015
+ const teams = await portal.listTeams();
9016
+ return (teams ?? []).map((t) => ({
9017
+ teamId: t.teamId,
9018
+ name: t.name,
9019
+ role: t.userRole?.name ?? t.type,
9020
+ status: t.status
9021
+ }));
9022
+ }
9023
+ async function oneClickInventory(session, teamId) {
9024
+ const portal = new PortalClient(session, teamId);
9025
+ await portal.init();
9026
+ const [appIds, allCerts, profiles, pushCerts] = await Promise.all([
9027
+ portal.listAppIds(),
9028
+ portal.listCertificates([DIST_CERT_TYPE]),
9029
+ portal.listProfiles(),
9030
+ portal.listPushCertificates()
9031
+ ]);
9032
+ const distributionCerts = (allCerts ?? []).map((c) => ({
9033
+ certificateId: c.certificateId,
9034
+ name: c.name ?? "Distribution",
9035
+ serialNumber: c.serialNum,
9036
+ expirationDate: c.expirationDateString,
9037
+ ownerName: c.ownerName
9038
+ }));
9039
+ return {
9040
+ team: { id: teamId },
9041
+ bundleIds: (appIds ?? []).map((a) => ({
9042
+ appIdId: a.appIdId,
9043
+ identifier: a.identifier,
9044
+ name: a.name
9045
+ })),
9046
+ distributionCerts,
9047
+ distCertCapacity: { used: distributionCerts.length, max: 3 },
9048
+ pushCerts: (pushCerts ?? []).map((c) => ({
9049
+ certificateId: c.certificateId,
9050
+ name: c.name ?? "Push",
9051
+ type: classifyPushCertType(c.typeString),
9052
+ bundleIdentifier: c.appId?.identifier,
9053
+ expirationDate: c.expirationDateString
9054
+ })),
9055
+ profiles: (profiles ?? []).map((p) => ({
9056
+ provisioningProfileId: p.provisioningProfileId,
9057
+ name: p.name,
9058
+ type: p.type,
9059
+ status: p.status,
9060
+ expirationDate: p.expirationDate
9061
+ }))
9062
+ };
9063
+ }
9064
+ async function ensureBundleId(portal, identifier, name, warnings) {
9065
+ const existing = await portal.listAppIds();
9066
+ const hit = existing.find((a) => a.identifier === identifier);
9067
+ if (hit) {
9068
+ warnings.push(`Bundle ID ${identifier} already existed and was reused.`);
9069
+ return hit.appIdId;
9070
+ }
9071
+ const created = await portal.createAppId({ identifier, name, enablePush: true });
9072
+ return created.appIdId;
9073
+ }
9074
+ async function createDistCert(portal) {
9075
+ const { csr, privateKey } = generateRSACSR();
9076
+ const cert = await portal.createCertificate({ type: DIST_CERT_TYPE, csrContent: csr });
9077
+ let certContent;
9078
+ if (cert.certificateContent) {
9079
+ certContent = Buffer.from(cert.certificateContent, "base64");
9080
+ } else {
9081
+ certContent = await portal.downloadCertificate(cert.certificateId, DIST_CERT_TYPE);
9082
+ }
9083
+ return { certificateId: cert.certificateId, privateKeyPem: privateKey, certContent };
9084
+ }
9085
+ async function oneClickSetup(input) {
9086
+ const opts = {
9087
+ generateDistributionCert: input.options?.generateDistributionCert ?? true,
9088
+ generateStoreProfile: input.options?.generateStoreProfile ?? true,
9089
+ generateAscApiKey: input.options?.generateAscApiKey ?? true,
9090
+ generateApnsAuthKey: input.options?.generateApnsAuthKey ?? true,
9091
+ enableSignInWithApple: input.options?.enableSignInWithApple ?? true,
9092
+ enablePushNotifications: input.options?.enablePushNotifications ?? true,
9093
+ revokeCertIds: input.options?.revokeCertIds ?? []
9094
+ };
9095
+ const onProgress = input.onProgress ?? (() => {
9096
+ });
9097
+ const warnings = [];
9098
+ const created = { revokedCertIds: [] };
9099
+ const additionalCerts = [];
9100
+ if (!opts.generateDistributionCert || !opts.generateStoreProfile) {
9101
+ throw new Error("Distribution cert and store profile are both required for an apple_credentials row.");
9102
+ }
9103
+ async function step(id, label, fn) {
9104
+ onProgress({ type: "step_start", step: id, label });
9105
+ try {
9106
+ const result = await fn();
9107
+ onProgress({ type: "step_done", step: id, label });
9108
+ return result;
9109
+ } catch (err) {
9110
+ onProgress({
9111
+ type: "step_failed",
9112
+ step: id,
9113
+ label,
9114
+ error: err instanceof Error ? err.message : String(err)
9115
+ });
9116
+ throw err;
9117
+ }
9118
+ }
9119
+ const portal = new PortalClient(input.session, input.teamId);
9120
+ await step("init", "Initialize Apple Developer portal session", () => portal.init());
9121
+ for (const certId of opts.revokeCertIds) {
9122
+ await step(`revoke:${certId}`, `Revoke distribution certificate ${certId}`, async () => {
9123
+ await portal.revokeCertificate(certId, DIST_CERT_TYPE);
9124
+ created.revokedCertIds.push(certId);
9125
+ });
9126
+ }
9127
+ const appIdId = await step(
9128
+ "bundle_id",
9129
+ `Bundle identifier ${input.bundleIdentifier}`,
9130
+ () => ensureBundleId(portal, input.bundleIdentifier, input.appName, warnings)
9131
+ );
9132
+ created.bundleIdId = appIdId;
9133
+ const dist = await step("dist_cert", "Distribution certificate", () => createDistCert(portal));
9134
+ created.certificateId = dist.certificateId;
9135
+ const certPassword = input.certPassword ?? randomPassword();
9136
+ const distP12 = await step(
9137
+ "p12",
9138
+ "Bundle distribution .p12",
9139
+ async () => bundleP12({
9140
+ certificate: dist.certContent,
9141
+ privateKeyPem: dist.privateKeyPem,
9142
+ password: certPassword,
9143
+ friendlyName: `${input.appName} Distribution`
9144
+ })
9145
+ );
9146
+ const existingProfiles = await portal.listProfiles();
9147
+ const profileName = buildProfileName(
9148
+ input.appName,
9149
+ (existingProfiles ?? []).map((p) => p.name)
9150
+ );
9151
+ const profile = await step("profile", `App Store provisioning profile (${profileName})`, async () => {
9152
+ const p = await portal.createProfile({
9153
+ appIdId,
9154
+ name: profileName,
9155
+ distributionType: "store",
9156
+ certificateIds: [dist.certificateId]
9157
+ });
9158
+ const content = await portal.downloadProfile(p.provisioningProfileId);
9159
+ return { id: p.provisioningProfileId, content };
9160
+ });
9161
+ created.provisioningProfileId = profile.id;
9162
+ const profileMeta = extractProfileMetadata(profile.content);
9163
+ if (profileMeta.uuid) created.profileUuid = profileMeta.uuid;
9164
+ if (opts.generateApnsAuthKey) {
9165
+ await step("apns_auth_key", "APNs Authentication Key (.p8)", async () => {
9166
+ try {
9167
+ const key = await portal.createApnsAuthKey(`${input.appName} APNs`);
9168
+ created.apnsAuthKeyId = key.keyId;
9169
+ additionalCerts.push({
9170
+ cert_type: "apns_key",
9171
+ cert_base64: Buffer.from(key.pem, "utf-8").toString("base64"),
9172
+ cert_password: key.keyId
9173
+ });
9174
+ } catch (err) {
9175
+ warnings.push(
9176
+ `APNs Authentication Key could not be created: ${err.message}. You can upload an existing .p8 manually from the credentials list.`
9177
+ );
9178
+ }
9179
+ });
9180
+ }
9181
+ let ascKeyId = "";
9182
+ let ascIssuerId = "";
9183
+ let ascP8Base64 = "";
9184
+ let ascP8Pem = "";
9185
+ if (opts.generateAscApiKey) {
9186
+ await step("asc_api_key", "App Store Connect API key (.p8)", async () => {
9187
+ try {
9188
+ const iris = new IrisClient(input.session);
9189
+ const createdKey = await iris.createApiKey({
9190
+ nickname: `${input.appName} SitePong`,
9191
+ roles: ["APP_MANAGER"],
9192
+ allAppsVisible: true,
9193
+ keyType: "PUBLIC_API"
9194
+ });
9195
+ created.ascApiKeyId = createdKey.id;
9196
+ const downloaded = await iris.fetchApiKeyPrivateKey(createdKey.id);
9197
+ if (!downloaded) {
9198
+ throw new Error(
9199
+ `Created key ${createdKey.kid} but Apple did not return the .p8. Revoke it at appstoreconnect.apple.com/access/integrations/api/${createdKey.id} before retrying.`
9200
+ );
9201
+ }
9202
+ if (!downloaded.issuerId) {
9203
+ throw new Error(
9204
+ `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.`
9205
+ );
9206
+ }
9207
+ ascKeyId = createdKey.kid;
9208
+ ascIssuerId = downloaded.issuerId;
9209
+ ascP8Pem = downloaded.pem;
9210
+ ascP8Base64 = Buffer.from(downloaded.pem, "utf-8").toString("base64");
9211
+ } catch (err) {
9212
+ warnings.push(
9213
+ `Could not auto-create ASC API key: ${err.message}. Upload a .p8 manually after the wizard finishes.`
9214
+ );
9215
+ }
9216
+ });
9217
+ }
9218
+ if ((opts.enablePushNotifications || opts.enableSignInWithApple) && ascKeyId && ascIssuerId && ascP8Pem) {
9219
+ await step("capabilities", "Enable bundle capabilities (Push, Sign in with Apple)", async () => {
9220
+ try {
9221
+ const ascClient = new AppStoreConnectClient({
9222
+ keyId: ascKeyId,
9223
+ issuerId: ascIssuerId,
9224
+ privateKey: ascP8Pem
9225
+ });
9226
+ const restBundle = await ascClient.bundleIds.find(input.bundleIdentifier);
9227
+ if (!restBundle) {
9228
+ warnings.push(
9229
+ `Could not look up bundle id ${input.bundleIdentifier} via REST API \u2014 capability flips skipped.`
9230
+ );
9231
+ return;
9232
+ }
9233
+ const existing = await ascClient.capabilities.listForBundleId(restBundle.id);
9234
+ const existingTypes = new Set(existing.map((c) => c.attributes?.capabilityType));
9235
+ const wanted = [];
9236
+ if (opts.enablePushNotifications) {
9237
+ wanted.push({ type: "PUSH_NOTIFICATIONS", label: "Push Notifications" });
9238
+ }
9239
+ if (opts.enableSignInWithApple) {
9240
+ wanted.push({
9241
+ type: "APPLE_ID_AUTH",
9242
+ label: "Sign in with Apple",
9243
+ settings: [
9244
+ {
9245
+ key: "APPLE_ID_AUTH_APP_CONSENT",
9246
+ options: [{ key: "PRIMARY_APP_CONSENT" }]
9247
+ }
9248
+ ]
9249
+ });
9250
+ }
9251
+ for (const w of wanted) {
9252
+ if (existingTypes.has(w.type)) continue;
9253
+ try {
9254
+ await ascClient.capabilities.enable({
9255
+ bundleIdId: restBundle.id,
9256
+ capabilityType: w.type,
9257
+ settings: w.settings
9258
+ });
9259
+ } catch (err) {
9260
+ warnings.push(`Could not enable ${w.label}: ${err.message}`);
9261
+ }
9262
+ }
9263
+ } catch (err) {
9264
+ warnings.push(`Capability setup failed: ${err.message}`);
9265
+ }
9266
+ });
9267
+ } else if ((opts.enablePushNotifications || opts.enableSignInWithApple) && !ascKeyId) {
9268
+ warnings.push(
9269
+ "Push / Sign in with Apple capabilities couldn't be flipped automatically (no ASC API key). Enable them manually in your Apple Developer Portal."
9270
+ );
9271
+ }
9272
+ return {
9273
+ team_id: input.teamId,
9274
+ bundle_identifier: input.bundleIdentifier,
9275
+ name: `${input.appName} \u2014 Distribution`,
9276
+ dist_cert_base64: distP12.toString("base64"),
9277
+ private_key_base64: Buffer.from(dist.privateKeyPem, "utf-8").toString("base64"),
9278
+ cert_password: certPassword,
9279
+ provisioning_profile_base64: profile.content.toString("base64"),
9280
+ profile_name: profileMeta.name ?? profileName,
9281
+ asc_key_id: ascKeyId,
9282
+ asc_issuer_id: ascIssuerId,
9283
+ asc_api_key_base64: ascP8Base64,
9284
+ additional_certs: additionalCerts,
9285
+ created,
9286
+ warnings
9287
+ };
9288
+ }
8109
9289
  function createRunCommand() {
8110
9290
  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) => {
8111
9291
  const { loadFastfile: loadFastfile2 } = await Promise.resolve().then(() => (init_fastfile(), fastfile_exports));
@@ -8405,6 +9585,6 @@ function createCli() {
8405
9585
  return program;
8406
9586
  }
8407
9587
 
8408
- 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 };
9588
+ 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 };
8409
9589
  //# sourceMappingURL=index.mjs.map
8410
9590
  //# sourceMappingURL=index.mjs.map