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