@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
@@ -209,8 +209,8 @@ async function revokeExpiredCertificates(options) {
209
209
  return count;
210
210
  }
211
211
  async function importToKeychain(certData, keychainPath, keychainPassword) {
212
- const { tmpdir: tmpdir7 } = await import('os');
213
- const tempCertPath = path.join(tmpdir7(), `fastnode-cert-${Date.now()}.cer`);
212
+ const { tmpdir: tmpdir8 } = await import('os');
213
+ const tempCertPath = path.join(tmpdir8(), `fastnode-cert-${Date.now()}.cer`);
214
214
  await promises.writeFile(tempCertPath, certData);
215
215
  try {
216
216
  const args = [
@@ -1642,14 +1642,14 @@ var init_building = __esm({
1642
1642
  name: "clean_build_artifacts",
1643
1643
  description: "Remove build artifacts (IPA, dSYM, etc.)",
1644
1644
  async run(params, ctx) {
1645
- const { rm: rm2 } = await import('fs/promises');
1645
+ const { rm: rm4 } = await import('fs/promises');
1646
1646
  const paths = [
1647
1647
  ctx.get("IPA_OUTPUT_PATH"),
1648
1648
  ctx.get("PKG_OUTPUT_PATH"),
1649
1649
  ctx.get("DSYM_OUTPUT_PATH")
1650
1650
  ].filter(Boolean);
1651
1651
  for (const p of paths) {
1652
- await rm2(p, { force: true }).catch(() => {
1652
+ await rm4(p, { force: true }).catch(() => {
1653
1653
  });
1654
1654
  }
1655
1655
  return { cleaned: paths };
@@ -1808,9 +1808,9 @@ Please accept the agreement at https://developer.apple.com/account before contin
1808
1808
  throw new Error(msg);
1809
1809
  }
1810
1810
  }
1811
- async function loginWithAppleId(config) {
1812
- const log = config.onLog ?? console.log;
1813
- const promptFn = config.onPrompt ?? defaultPrompt;
1811
+ async function beginLogin(config) {
1812
+ const log = config.onLog ?? (() => {
1813
+ });
1814
1814
  log("Fetching Apple service key...");
1815
1815
  const serviceKey = await fetchServiceKey();
1816
1816
  const initRes = await fetch(`${AUTH_BASE}/signin?widgetKey=${serviceKey}`, {
@@ -1827,27 +1827,94 @@ async function loginWithAppleId(config) {
1827
1827
  const cookieStr = Object.entries(initCookies).map(([k, v]) => k.startsWith("DES") ? `${k}="${v}"` : `${k}=${v}`).join("; ");
1828
1828
  log("Starting SIRP Apple ID login...");
1829
1829
  const response = await doSirp(config.email, config.password, serviceKey, cookieStr, hashcash);
1830
- const session = {
1830
+ const baseState = {
1831
1831
  cookies: { ...initCookies, ...response.cookies },
1832
1832
  sessionId: response.sessionId,
1833
1833
  scnt: response.scnt,
1834
1834
  serviceKey,
1835
- userEmail: config.email,
1836
- pendingAgreements: []
1835
+ userEmail: config.email
1837
1836
  };
1838
1837
  if (response.status === 403) {
1839
1838
  throw new Error(`Invalid username and password combination. Used '${config.email}' as the username.`);
1840
- } else if (response.status === 200) {
1841
- log("Login successful.");
1842
- await fetchOlympusSession(session);
1843
- } else if (response.status === 409) {
1844
- await handleTwoStepOrFactor(session, log, promptFn);
1845
- await fetchOlympusSession(session);
1846
- } else if (response.status === 412) {
1839
+ }
1840
+ if (response.status === 412) {
1847
1841
  throw new Error("Need to acknowledge Apple's Apple ID and Privacy statement. Please manually log into https://appleid.apple.com.");
1842
+ }
1843
+ if (response.status === 200) {
1844
+ log("Login successful (no 2FA).");
1845
+ const session = { ...baseState, pendingAgreements: [] };
1846
+ await finalizeSession(session, log);
1847
+ return { kind: "logged_in", session };
1848
+ }
1849
+ if (response.status === 409) {
1850
+ log("2FA required, fetching challenge...");
1851
+ const challenge = await fetchTwoFactorChallenge(baseState);
1852
+ return { kind: "needs_2fa", pending: baseState, challenge };
1853
+ }
1854
+ throw new Error(`Unexpected login response (${response.status}): ${JSON.stringify(response.body).slice(0, 500)}`);
1855
+ }
1856
+ async function fetchTwoFactorChallenge(pending) {
1857
+ const r = await fetch(`${AUTH_BASE}`, { headers: buildAuthHeadersFromPending(pending) });
1858
+ const body = await r.json();
1859
+ const trustedPhoneNumbers = (body.trustedPhoneNumbers ?? []).map((p) => ({
1860
+ id: p.id,
1861
+ numberWithDialCode: p.numberWithDialCode,
1862
+ pushMode: p.pushMode
1863
+ }));
1864
+ const phoneOnly = body.noTrustedDevices === true;
1865
+ const codeLength = body.securityCode?.length ?? 6;
1866
+ return {
1867
+ kind: phoneOnly ? "phone_only" : "device",
1868
+ codeLength,
1869
+ trustedPhoneNumbers
1870
+ };
1871
+ }
1872
+ async function requestSmsCode(input) {
1873
+ await fetch(`${AUTH_BASE}/verify/phone`, {
1874
+ method: "PUT",
1875
+ headers: { ...buildAuthHeadersFromPending(input.pending), "Content-Type": "application/json" },
1876
+ body: JSON.stringify({ phoneNumber: { id: input.phoneId }, mode: input.mode ?? "sms" })
1877
+ });
1878
+ }
1879
+ async function submitTwoFactorCode(input) {
1880
+ const log = input.onLog ?? (() => {
1881
+ });
1882
+ let path;
1883
+ let body;
1884
+ if (input.source === "device") {
1885
+ path = `${AUTH_BASE}/verify/trusteddevice/securitycode`;
1886
+ body = JSON.stringify({ securityCode: { code: input.code } });
1848
1887
  } else {
1849
- throw new Error(`Unexpected login response (${response.status}): ${JSON.stringify(response.body).slice(0, 500)}`);
1888
+ if (input.phoneId == null) throw new Error("phoneId is required when source='phone'");
1889
+ path = `${AUTH_BASE}/verify/phone/securitycode`;
1890
+ body = JSON.stringify({
1891
+ securityCode: { code: input.code },
1892
+ phoneNumber: { id: input.phoneId },
1893
+ mode: input.mode ?? "sms"
1894
+ });
1850
1895
  }
1896
+ log("Submitting 2FA code...");
1897
+ const verifyRes = await fetch(path, {
1898
+ method: "POST",
1899
+ headers: { ...buildAuthHeadersFromPending(input.pending), "Content-Type": "application/json" },
1900
+ body
1901
+ });
1902
+ if (!verifyRes.ok) {
1903
+ const err = await verifyRes.json().catch(() => ({}));
1904
+ const msgs = (err?.service_errors ?? err?.serviceErrors ?? []).map((e) => e.message).filter(Boolean);
1905
+ throw new Error(msgs.join("; ") || `Verification failed (${verifyRes.status})`);
1906
+ }
1907
+ const session = {
1908
+ ...input.pending,
1909
+ cookies: { ...input.pending.cookies, ...parseCookies(verifyRes.headers) },
1910
+ pendingAgreements: []
1911
+ };
1912
+ await trustSession(session);
1913
+ await finalizeSession(session, log);
1914
+ return session;
1915
+ }
1916
+ async function finalizeSession(session, log) {
1917
+ await fetchOlympusSession(session);
1851
1918
  const agreements = await fetchContractMessages(session);
1852
1919
  if (agreements.length > 0) {
1853
1920
  for (const msg of agreements) {
@@ -1856,7 +1923,53 @@ async function loginWithAppleId(config) {
1856
1923
  }
1857
1924
  session.pendingAgreements = agreements;
1858
1925
  log(`Logged in as ${session.userEmail}`);
1859
- return session;
1926
+ }
1927
+ async function loginWithAppleId(config) {
1928
+ const log = config.onLog ?? console.log;
1929
+ const promptFn = config.onPrompt ?? defaultPrompt;
1930
+ const result = await beginLogin({ email: config.email, password: config.password, onLog: log });
1931
+ if (result.kind === "logged_in") return result.session;
1932
+ const { pending, challenge } = result;
1933
+ const phones = challenge.trustedPhoneNumbers;
1934
+ if (challenge.kind === "device") {
1935
+ log("Two-factor Authentication (6 digit code) is enabled for this account");
1936
+ log("(Input 'sms' to escape this prompt and select a trusted phone number to send the code as a text message)");
1937
+ const input = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
1938
+ if (input.toLowerCase() === "sms") {
1939
+ log("Please select a trusted phone number:");
1940
+ for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
1941
+ const choice2 = await promptFn("Enter number: ");
1942
+ const phone2 = phones[Math.max(0, parseInt(choice2, 10) - 1)] ?? phones[0];
1943
+ await requestSmsCode({ pending, phoneId: phone2.id, mode: phone2.pushMode });
1944
+ log(`Code sent to ${phone2.numberWithDialCode}`);
1945
+ const code2 = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
1946
+ return submitTwoFactorCode({ pending, code: code2, source: "phone", phoneId: phone2.id, mode: phone2.pushMode ?? "sms", onLog: log });
1947
+ }
1948
+ return submitTwoFactorCode({ pending, code: input, source: "device", onLog: log });
1949
+ }
1950
+ log("Two-factor Authentication (6 digit code) is enabled for this account");
1951
+ if (phones.length === 1) {
1952
+ const phone2 = phones[0];
1953
+ log(`Code automatically sent to ${phone2.numberWithDialCode}`);
1954
+ const code2 = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
1955
+ return submitTwoFactorCode({ pending, code: code2, source: "phone", phoneId: phone2.id, mode: phone2.pushMode ?? "sms", onLog: log });
1956
+ }
1957
+ log("Please select a trusted phone number:");
1958
+ for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
1959
+ const choice = await promptFn("Enter number: ");
1960
+ const phone = phones[Math.max(0, parseInt(choice, 10) - 1)] ?? phones[0];
1961
+ await requestSmsCode({ pending, phoneId: phone.id, mode: phone.pushMode });
1962
+ log(`Code sent to ${phone.numberWithDialCode}`);
1963
+ const code = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
1964
+ return submitTwoFactorCode({ pending, code, source: "phone", phoneId: phone.id, mode: phone.pushMode ?? "sms", onLog: log });
1965
+ }
1966
+ function buildAuthHeadersFromPending(pending) {
1967
+ const h = { "X-Apple-Widget-Key": pending.serviceKey, Accept: "application/json" };
1968
+ if (pending.sessionId) h["X-Apple-Id-Session-Id"] = pending.sessionId;
1969
+ if (pending.scnt) h["scnt"] = pending.scnt;
1970
+ const c = Object.entries(pending.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
1971
+ if (c) h["Cookie"] = c;
1972
+ return h;
1860
1973
  }
1861
1974
  async function doSirp(user, password, serviceKey, cookieStr, hashcash) {
1862
1975
  const client = new SRPClient(2048);
@@ -1912,99 +2025,6 @@ async function doSirp(user, password, serviceKey, cookieStr, hashcash) {
1912
2025
  scnt: completeRes.headers.get("scnt") ?? ""
1913
2026
  };
1914
2027
  }
1915
- async function handleTwoStepOrFactor(session, log, promptFn) {
1916
- const r = await fetch(`${AUTH_BASE}`, { headers: buildAuthHeaders(session) });
1917
- const body = await r.json();
1918
- if (body.trustedDevices?.length > 0) {
1919
- await handleTwoStep(session, body, log, promptFn);
1920
- } else if (body.trustedPhoneNumbers?.length > 0) {
1921
- await handleTwoFactor(session, body, log, promptFn);
1922
- } else {
1923
- throw new Error("2FA required but no trusted devices or phone numbers found.");
1924
- }
1925
- }
1926
- async function handleTwoFactor(session, authBody, log, promptFn) {
1927
- log("Two-factor Authentication (6 digit code) is enabled for this account");
1928
- const codeLength = authBody.securityCode?.length ?? 6;
1929
- const phones = authBody.trustedPhoneNumbers ?? [];
1930
- const noTrusted = authBody.noTrustedDevices === true;
1931
- let codeType;
1932
- let reqBody;
1933
- if (noTrusted && phones.length === 1) {
1934
- const phone = phones[0];
1935
- log(`Code automatically sent to ${phone.numberWithDialCode}`);
1936
- const code = await promptFn(`Please enter the ${codeLength} digit code you received at ${phone.numberWithDialCode}: `);
1937
- codeType = "phone";
1938
- reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
1939
- } else if (noTrusted) {
1940
- const phone = await choosePhone(phones, log, promptFn);
1941
- await requestPhoneCode(session, phone);
1942
- log(`Successfully requested text message to ${phone.numberWithDialCode}`);
1943
- const code = await promptFn(`Please enter the ${codeLength} digit code: `);
1944
- codeType = "phone";
1945
- reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
1946
- } else {
1947
- log("(Input 'sms' to escape this prompt and select a trusted phone number to send the code as a text message)");
1948
- const input = await promptFn(`Please enter the ${codeLength} digit code: `);
1949
- if (input.toLowerCase() === "sms") {
1950
- const phone = await choosePhone(phones, log, promptFn);
1951
- await requestPhoneCode(session, phone);
1952
- const code = await promptFn(`Please enter the ${codeLength} digit code sent to ${phone.numberWithDialCode}: `);
1953
- codeType = "phone";
1954
- reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
1955
- } else {
1956
- codeType = "trusteddevice";
1957
- reqBody = JSON.stringify({ securityCode: { code: input } });
1958
- }
1959
- }
1960
- log("Requesting session...");
1961
- const verifyRes = await fetch(`${AUTH_BASE}/verify/${codeType}/securitycode`, {
1962
- method: "POST",
1963
- headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
1964
- body: reqBody
1965
- });
1966
- if (!verifyRes.ok) {
1967
- const err = await verifyRes.json().catch(() => ({}));
1968
- const msgs = (err?.service_errors ?? err?.serviceErrors ?? []).map((e) => e.message).filter(Boolean);
1969
- throw new Error(msgs.join("; ") || `Verification failed (${verifyRes.status})`);
1970
- }
1971
- Object.assign(session.cookies, parseCookies(verifyRes.headers));
1972
- await trustSession(session);
1973
- }
1974
- async function handleTwoStep(session, authBody, log, promptFn) {
1975
- log("Two-step Verification (4 digit code) is enabled for this account");
1976
- const devices = authBody.trustedDevices ?? [];
1977
- log("Please select a trusted device:");
1978
- for (let i = 0; i < devices.length; i++) {
1979
- log(` ${i + 1}. ${devices[i].name} ${devices[i].modelName ?? "SMS"} (${devices[i].id})`);
1980
- }
1981
- const choice = await promptFn("Enter device number: ");
1982
- const device = devices[Math.max(0, parseInt(choice, 10) - 1)] ?? devices[0];
1983
- await fetch(`${AUTH_BASE}/verify/device/${device.id}/securitycode`, { method: "PUT", headers: buildAuthHeaders(session) });
1984
- log("Successfully requested notification");
1985
- const code = await promptFn("Please enter the 4 digit code: ");
1986
- const verifyRes = await fetch(`${AUTH_BASE}/verify/phone/securitycode`, {
1987
- method: "POST",
1988
- headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
1989
- body: JSON.stringify({ phoneNumber: { id: device.id }, securityCode: { code }, mode: "sms" })
1990
- });
1991
- if (!verifyRes.ok) throw new Error("Incorrect verification code.");
1992
- Object.assign(session.cookies, parseCookies(verifyRes.headers));
1993
- await trustSession(session);
1994
- }
1995
- async function choosePhone(phones, log, promptFn) {
1996
- log("Please select a trusted phone number:");
1997
- for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
1998
- const choice = await promptFn("Enter number: ");
1999
- return phones[Math.max(0, parseInt(choice, 10) - 1)] ?? phones[0];
2000
- }
2001
- async function requestPhoneCode(session, phone) {
2002
- await fetch(`${AUTH_BASE}/verify/phone`, {
2003
- method: "PUT",
2004
- headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
2005
- body: JSON.stringify({ phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" })
2006
- });
2007
- }
2008
2028
  async function trustSession(session) {
2009
2029
  const r = await fetch(`${AUTH_BASE}/2sv/trust`, { headers: buildAuthHeaders(session) });
2010
2030
  Object.assign(session.cookies, parseCookies(r.headers));
@@ -2296,23 +2316,13 @@ var init_request = __esm({
2296
2316
  };
2297
2317
  }
2298
2318
  });
2299
-
2300
- // src/pilot/index.ts
2301
- var pilot_exports = {};
2302
- __export(pilot_exports, {
2303
- distributeToGroups: () => distributeToGroups,
2304
- expireBuild: () => expireBuild,
2305
- setChangelog: () => setChangelog,
2306
- uploadToTestFlight: () => uploadToTestFlight,
2307
- waitForProcessing: () => waitForProcessing
2308
- });
2309
- async function uploadToTestFlight(options) {
2319
+ async function uploadWithAltool(options) {
2310
2320
  if (!fs.existsSync(options.ipaPath)) {
2311
2321
  throw new Error(`File not found: ${options.ipaPath}`);
2312
2322
  }
2313
2323
  const platform = options.platform ?? "ios";
2314
2324
  const timeout = options.timeout ?? 18e5;
2315
- const keyDir = await promises.mkdtemp(path.join(os.tmpdir(), "fastnode-key-"));
2325
+ const keyDir = await promises.mkdtemp(path.join(os.tmpdir(), "fastnode-altool-"));
2316
2326
  const keyPath = path.join(keyDir, `AuthKey_${options.apiKeyId}.p8`);
2317
2327
  await promises.writeFile(keyPath, options.apiPrivateKey);
2318
2328
  return new Promise((resolve4) => {
@@ -2336,24 +2346,228 @@ async function uploadToTestFlight(options) {
2336
2346
  const timer = setTimeout(() => {
2337
2347
  child.kill("SIGTERM");
2338
2348
  }, timeout);
2339
- child.stdout.on("data", (data) => {
2349
+ const onData = (data) => {
2340
2350
  const chunk = data.toString();
2341
2351
  output += chunk;
2342
2352
  options.onOutput?.(chunk);
2353
+ };
2354
+ child.stdout.on("data", onData);
2355
+ child.stderr.on("data", onData);
2356
+ child.on("close", async (code) => {
2357
+ clearTimeout(timer);
2358
+ await promises.unlink(keyPath).catch(() => {
2359
+ });
2360
+ await promises.rm(keyDir, { recursive: true, force: true }).catch(() => {
2361
+ });
2362
+ resolve4({ success: code === 0, output, backend: "altool" });
2343
2363
  });
2344
- child.stderr.on("data", (data) => {
2364
+ });
2365
+ }
2366
+ var init_upload_altool = __esm({
2367
+ "src/pilot/upload-altool.ts"() {
2368
+ }
2369
+ });
2370
+ async function resolveTransporter() {
2371
+ const override = process.env.ITMSTRANSPORTER_BIN;
2372
+ if (override) {
2373
+ if (!fs.existsSync(override)) {
2374
+ throw new Error(
2375
+ `ITMSTRANSPORTER_BIN is set to "${override}" but that path does not exist.`
2376
+ );
2377
+ }
2378
+ return override;
2379
+ }
2380
+ const candidates = process.platform === "darwin" ? [
2381
+ // Xcode 15+: Transporter.app bundled with Xcode
2382
+ "/Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/itms/bin/iTMSTransporter",
2383
+ // Standalone Transporter.app from the Mac App Store
2384
+ "/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter"
2385
+ ] : [
2386
+ "/usr/local/itms/bin/iTMSTransporter",
2387
+ "/opt/itms/bin/iTMSTransporter",
2388
+ "/usr/local/bin/iTMSTransporter"
2389
+ ];
2390
+ for (const path of candidates) {
2391
+ if (fs.existsSync(path)) return path;
2392
+ }
2393
+ throw new Error(
2394
+ [
2395
+ "iTMSTransporter not found.",
2396
+ "",
2397
+ "Install options:",
2398
+ process.platform === "darwin" ? " - Install Xcode (includes Transporter)" : " - Download Apple's standalone iTMSTransporter package for Linux",
2399
+ " - Or set ITMSTRANSPORTER_BIN to the absolute path of the binary",
2400
+ "",
2401
+ `Searched: ${candidates.join(", ")}`
2402
+ ].join("\n")
2403
+ );
2404
+ }
2405
+ async function verifyJava() {
2406
+ return new Promise((resolve4, reject) => {
2407
+ const child = child_process.spawn("java", ["-version"], { stdio: ["ignore", "pipe", "pipe"] });
2408
+ let stderr = "";
2409
+ child.stderr.on("data", (d) => {
2410
+ stderr += d.toString();
2411
+ });
2412
+ child.on("error", (err) => {
2413
+ reject(
2414
+ new Error(
2415
+ `java not found on PATH (${err.message}). Install Java 17 LTS and ensure it is on PATH.`
2416
+ )
2417
+ );
2418
+ });
2419
+ child.on("close", (code) => {
2420
+ if (code !== 0) {
2421
+ reject(new Error(`java -version exited ${code}: ${stderr}`));
2422
+ return;
2423
+ }
2424
+ const match = stderr.match(/version "([^"]+)"/);
2425
+ resolve4({ version: match?.[1] ?? "unknown" });
2426
+ });
2427
+ });
2428
+ }
2429
+ var init_transporter_install = __esm({
2430
+ "src/pilot/transporter-install.ts"() {
2431
+ }
2432
+ });
2433
+ async function uploadWithTransporter(options) {
2434
+ if (!fs.existsSync(options.ipaPath)) {
2435
+ throw new Error(`File not found: ${options.ipaPath}`);
2436
+ }
2437
+ await verifyJava();
2438
+ const transporterBin = await resolveTransporter();
2439
+ const platform = options.platform ?? "ios";
2440
+ const timeout = options.timeout ?? 18e5;
2441
+ const keyDir = await promises.mkdtemp(path.join(os.tmpdir(), "fastnode-itms-"));
2442
+ const keyPath = path.join(keyDir, `AuthKey_${options.apiKeyId}.p8`);
2443
+ await promises.writeFile(keyPath, options.apiPrivateKey);
2444
+ return new Promise((resolve4) => {
2445
+ const args = [
2446
+ "-m",
2447
+ "upload",
2448
+ "-assetFile",
2449
+ options.ipaPath,
2450
+ "-apiKey",
2451
+ options.apiKeyId,
2452
+ "-apiIssuer",
2453
+ options.apiIssuerId,
2454
+ // WONoPause prevents the JVM from waiting on stdin in non-TTY
2455
+ // environments (CI, LaunchAgent) where it would hang forever.
2456
+ "-WONoPause",
2457
+ "true"
2458
+ ];
2459
+ if (platform !== "ios") {
2460
+ args.push("-bundleType", platform);
2461
+ }
2462
+ const child = child_process.spawn(transporterBin, args, {
2463
+ env: {
2464
+ ...process.env,
2465
+ API_PRIVATE_KEYS_DIR: keyDir
2466
+ },
2467
+ stdio: ["ignore", "pipe", "pipe"]
2468
+ });
2469
+ let output = "";
2470
+ const timer = setTimeout(() => {
2471
+ child.kill("SIGTERM");
2472
+ }, timeout);
2473
+ const onData = (data) => {
2345
2474
  const chunk = data.toString();
2346
2475
  output += chunk;
2347
2476
  options.onOutput?.(chunk);
2348
- });
2477
+ };
2478
+ child.stdout.on("data", onData);
2479
+ child.stderr.on("data", onData);
2349
2480
  child.on("close", async (code) => {
2350
2481
  clearTimeout(timer);
2351
- await promises.unlink(keyPath).catch(() => {
2482
+ await promises.rm(keyDir, { recursive: true, force: true }).catch(() => {
2483
+ });
2484
+ resolve4({ success: code === 0, output, backend: "transporter" });
2485
+ });
2486
+ child.on("error", async (err) => {
2487
+ clearTimeout(timer);
2488
+ await promises.rm(keyDir, { recursive: true, force: true }).catch(() => {
2489
+ });
2490
+ resolve4({
2491
+ success: false,
2492
+ output: output + `
2493
+ Process error: ${err.message}`,
2494
+ backend: "transporter"
2352
2495
  });
2353
- resolve4({ success: code === 0, output });
2354
2496
  });
2355
2497
  });
2356
2498
  }
2499
+ var init_upload_transporter = __esm({
2500
+ "src/pilot/upload-transporter.ts"() {
2501
+ init_transporter_install();
2502
+ }
2503
+ });
2504
+
2505
+ // src/pilot/classify.ts
2506
+ function classifyUploadError(output) {
2507
+ for (const { pattern, kind } of RULES) {
2508
+ if (pattern.test(output)) return kind;
2509
+ }
2510
+ return null;
2511
+ }
2512
+ function isTransientUploadError(output) {
2513
+ return classifyUploadError(output) !== null;
2514
+ }
2515
+ var RULES;
2516
+ var init_classify = __esm({
2517
+ "src/pilot/classify.ts"() {
2518
+ RULES = [
2519
+ // Apple's finalize-upload call returning 5xx — the most common transient
2520
+ // failure we see in practice. Example:
2521
+ // "CHANGE UPLOAD STATE TO COMPLETE (UPLOADING ASSET DESCRIPTION):
2522
+ // received status code 500; internal server error."
2523
+ { pattern: /CHANGE UPLOAD STATE TO COMPLETE/i, kind: "finalize_failed" },
2524
+ // Generic 5xx from ASC backend services
2525
+ { pattern: /status code 5\d\d/i, kind: "apple_5xx" },
2526
+ { pattern: /internal server error/i, kind: "apple_5xx" },
2527
+ { pattern: /Service Unavailable/i, kind: "apple_5xx" },
2528
+ { pattern: /Bad Gateway/i, kind: "apple_5xx" },
2529
+ // Network-level interruptions during the upload body transfer
2530
+ { pattern: /Connection reset/i, kind: "connection_reset" },
2531
+ { pattern: /SocketException/i, kind: "connection_reset" },
2532
+ { pattern: /Broken pipe/i, kind: "connection_reset" },
2533
+ // Timeouts — distinct from resets because the other side may have
2534
+ // actually processed the request, so dedupe logic may matter.
2535
+ { pattern: /Read timed out/i, kind: "network_timeout" },
2536
+ { pattern: /SocketTimeoutException/i, kind: "network_timeout" },
2537
+ { pattern: /Operation timed out/i, kind: "network_timeout" },
2538
+ // JVM-internal errors from iTMSTransporter (OOM, thread interruption, etc.)
2539
+ { pattern: /java\.lang\.OutOfMemoryError/i, kind: "transporter_jvm_error" },
2540
+ { pattern: /EOFException/i, kind: "transporter_jvm_error" }
2541
+ ];
2542
+ }
2543
+ });
2544
+
2545
+ // src/pilot/index.ts
2546
+ var pilot_exports = {};
2547
+ __export(pilot_exports, {
2548
+ classifyUploadError: () => classifyUploadError,
2549
+ distributeToGroups: () => distributeToGroups,
2550
+ expireBuild: () => expireBuild,
2551
+ isTransientUploadError: () => isTransientUploadError,
2552
+ resolveTransporter: () => resolveTransporter,
2553
+ setChangelog: () => setChangelog,
2554
+ uploadToTestFlight: () => uploadToTestFlight,
2555
+ uploadWithAltool: () => uploadWithAltool,
2556
+ uploadWithTransporter: () => uploadWithTransporter,
2557
+ verifyJava: () => verifyJava,
2558
+ waitForProcessing: () => waitForProcessing
2559
+ });
2560
+ async function uploadToTestFlight(options) {
2561
+ const backend = selectBackend(options.backend ?? "auto");
2562
+ if (backend === "altool") {
2563
+ return uploadWithAltool(options);
2564
+ }
2565
+ return uploadWithTransporter(options);
2566
+ }
2567
+ function selectBackend(pref) {
2568
+ if (pref !== "auto") return pref;
2569
+ return process.platform === "darwin" ? "altool" : "transporter";
2570
+ }
2357
2571
  async function waitForProcessing(client, params) {
2358
2572
  const timeout = params.timeout ?? 18e5;
2359
2573
  const pollInterval = params.pollInterval ?? 3e4;
@@ -2474,6 +2688,12 @@ function sleep2(ms) {
2474
2688
  var init_pilot = __esm({
2475
2689
  "src/pilot/index.ts"() {
2476
2690
  init_request();
2691
+ init_upload_altool();
2692
+ init_upload_transporter();
2693
+ init_upload_altool();
2694
+ init_upload_transporter();
2695
+ init_classify();
2696
+ init_transporter_install();
2477
2697
  }
2478
2698
  });
2479
2699
  async function loadMetadata(metadataPath) {
@@ -2561,9 +2781,9 @@ var init_metadata = __esm({
2561
2781
  // src/xcode/upload.ts
2562
2782
  var upload_exports = {};
2563
2783
  __export(upload_exports, {
2564
- uploadWithAltool: () => uploadWithAltool
2784
+ uploadWithAltool: () => uploadWithAltool2
2565
2785
  });
2566
- async function uploadWithAltool(options) {
2786
+ async function uploadWithAltool2(options) {
2567
2787
  requireMacOS("altool upload");
2568
2788
  let keyDir;
2569
2789
  if (options.apiPrivateKey && !options.apiKeyPath) {
@@ -2672,8 +2892,8 @@ async function uploadToAppStore(options) {
2672
2892
  if (!options.skipBinaryUpload) {
2673
2893
  const binaryPath = options.ipaPath ?? options.pkgPath;
2674
2894
  if (binaryPath && options.apiKeyId && options.apiIssuerId) {
2675
- const { uploadWithAltool: uploadWithAltool2 } = await Promise.resolve().then(() => (init_upload(), upload_exports));
2676
- await uploadWithAltool2({
2895
+ const { uploadWithAltool: uploadWithAltool3 } = await Promise.resolve().then(() => (init_upload(), upload_exports));
2896
+ await uploadWithAltool3({
2677
2897
  filePath: binaryPath,
2678
2898
  apiKeyId: options.apiKeyId,
2679
2899
  apiIssuerId: options.apiIssuerId,
@@ -3459,8 +3679,8 @@ async function captureScreenshots(options) {
3459
3679
  const outputDir = options.outputDirectory ?? path.join(process.cwd(), "screenshots");
3460
3680
  const retries = options.numberOfRetries ?? 1;
3461
3681
  if (options.clearPreviousScreenshots && fs.existsSync(outputDir)) {
3462
- const { rm: rm2 } = await import('fs/promises');
3463
- await rm2(outputDir, { recursive: true, force: true });
3682
+ const { rm: rm4 } = await import('fs/promises');
3683
+ await rm4(outputDir, { recursive: true, force: true });
3464
3684
  }
3465
3685
  fs.mkdirSync(outputDir, { recursive: true });
3466
3686
  const screenshots = {};
@@ -6167,6 +6387,264 @@ var init_portal = __esm({
6167
6387
  async init() {
6168
6388
  await this.post("account/listTeams.action", {});
6169
6389
  }
6390
+ /**
6391
+ * JSON POST — different content type than the standard form-encoded
6392
+ * `post()`. Used by the App Store Connect API key endpoints
6393
+ * (`account/auth/key/v2/create`), which Spaceship discovered take a
6394
+ * JSON body even though every other portal mutation is form-encoded.
6395
+ *
6396
+ * Like the form-encoded `post()`, this also runs the response through
6397
+ * `checkPortalResponse` — Apple returns HTTP 200 even for errors and
6398
+ * encodes the error in `resultCode`/`userString` fields, so a raw
6399
+ * status check would silently miss real failures.
6400
+ */
6401
+ async postJson(path, body) {
6402
+ const headers = {
6403
+ Cookie: this.cookieStr,
6404
+ "Content-Type": "application/json",
6405
+ Accept: "application/json"
6406
+ };
6407
+ if (this.csrf) {
6408
+ headers["csrf"] = this.csrf;
6409
+ headers["csrf_ts"] = this.csrfTs;
6410
+ }
6411
+ console.log(`[portal] POST(json) ${path}`);
6412
+ const res = await fetch(`${PORTAL_BASE}/${path}`, {
6413
+ method: "POST",
6414
+ headers,
6415
+ body: JSON.stringify(body)
6416
+ });
6417
+ if (res.headers.get("csrf")) {
6418
+ this.csrf = res.headers.get("csrf");
6419
+ this.csrfTs = res.headers.get("csrf_ts") ?? "";
6420
+ }
6421
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
6422
+ const m = raw.match(/^([^=]+)=([^;]*)/);
6423
+ if (m && !this.cookieStr.includes(m[1] + "=")) this.cookieStr += "; " + m[1] + "=" + m[2];
6424
+ }
6425
+ const text = await res.text();
6426
+ let data = null;
6427
+ try {
6428
+ data = text ? JSON.parse(text) : null;
6429
+ } catch {
6430
+ }
6431
+ console.log(`[portal] POST(json) ${path} \u2192 ${res.status} ${text.slice(0, 400)}`);
6432
+ checkPortalResponse(data, this.session);
6433
+ return { status: res.status, body: data, rawText: text };
6434
+ }
6435
+ /**
6436
+ * GET helper for the portal endpoints that take query parameters
6437
+ * (currently just `account/auth/key/download`). Returns the raw text
6438
+ * since download endpoints can return PEMs or JSON depending on Apple's
6439
+ * mood — caller decides how to parse.
6440
+ */
6441
+ async getRaw(path, params = {}) {
6442
+ const url = new URL(`${PORTAL_BASE}/${path}`);
6443
+ for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
6444
+ const headers = {
6445
+ Cookie: this.cookieStr,
6446
+ Accept: "application/json, text/plain, application/x-pem-file, */*"
6447
+ };
6448
+ if (this.csrf) {
6449
+ headers["csrf"] = this.csrf;
6450
+ headers["csrf_ts"] = this.csrfTs;
6451
+ }
6452
+ console.log(`[portal] GET ${url.pathname}${url.search}`);
6453
+ const res = await fetch(url.toString(), { headers });
6454
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
6455
+ const m = raw.match(/^([^=]+)=([^;]*)/);
6456
+ if (m && !this.cookieStr.includes(m[1] + "=")) this.cookieStr += "; " + m[1] + "=" + m[2];
6457
+ }
6458
+ const text = await res.text();
6459
+ const contentType = res.headers.get("content-type") ?? "";
6460
+ console.log(`[portal] GET ${url.pathname} \u2192 ${res.status} ${contentType} ${text.length}b`);
6461
+ return { status: res.status, rawText: text, contentType };
6462
+ }
6463
+ // ── App Store Connect API Keys (.p8) ────────────────────────────────────
6464
+ /**
6465
+ * Create an App Store Connect API key via the developer portal's
6466
+ * `account/auth/key/v2/create` endpoint. Reverse-engineered from the
6467
+ * _current_ @expo/apple-utils npm package (v2.1.21), which is what
6468
+ * EAS CLI uses to manage Apple credentials in production.
6469
+ *
6470
+ * This differs from older published reverse-engineering guides:
6471
+ * • URL uses the `/v2/create` suffix (Apple kept it)
6472
+ * • Field is `serviceConfigurations`, shape is an object keyed by
6473
+ * service id (NOT `serviceConfigurationsRequests: [...]` array)
6474
+ * • Body REQUIRES `scope: "team"`
6475
+ * • Needs a scope-specific CSRF warmup via the `keys` endpoint
6476
+ * before the create call (EAS's `ensureCSRFAsync("keys", …)`)
6477
+ *
6478
+ * For an App Store Connect API key (PUBLIC_API, used by altool for
6479
+ * TestFlight uploads), `serviceConfigurations` is an empty object `{}`.
6480
+ * For an APNs auth key, it's `{ "U27F4V844T": [] }`. For MusicKit:
6481
+ * `{ "6A7HVUVQ3M": [musicId] }`, etc.
6482
+ *
6483
+ * Apple's name validator rejects parentheses, brackets, and most
6484
+ * punctuation — sanitize to alphanumeric + space + dash + underscore
6485
+ * + period and collapse whitespace.
6486
+ */
6487
+ async createApiKey(rawName, serviceConfigurations = {}) {
6488
+ const name = rawName.replace(/[^A-Za-z0-9 _\-.]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
6489
+ if (!name) throw new Error("createApiKey: name is empty after sanitization");
6490
+ try {
6491
+ await this.listApiKeys();
6492
+ } catch {
6493
+ }
6494
+ const body = {
6495
+ teamId: this.teamId,
6496
+ name,
6497
+ serviceConfigurations,
6498
+ scope: "team"
6499
+ };
6500
+ const result = await this.postJson("account/auth/key/v2/create", body);
6501
+ const created = result.body?.keys?.[0] ?? result.body?.key ?? result.body?.data ?? result.body;
6502
+ const keyId = created?.keyId ?? created?.id ?? created?.kid ?? created?.attributes?.kid;
6503
+ if (!keyId) {
6504
+ throw new Error(`createApiKey: no key id in response. Body: ${result.rawText.slice(0, 400)}`);
6505
+ }
6506
+ return { keyId, raw: created };
6507
+ }
6508
+ /**
6509
+ * Fetch a single API key by id via `account/auth/key/get`. Reverse-
6510
+ * engineered from Spaceship's `get_key`. Returns the raw key record so
6511
+ * the caller can extract whatever fields it needs (e.g. issuerId).
6512
+ *
6513
+ * Form-encoded POST — same shape as every other PortalClient mutation
6514
+ * except the JSON `create` endpoint.
6515
+ */
6516
+ async getApiKey(keyId) {
6517
+ const data = await this.post("account/auth/key/get", {
6518
+ teamId: this.teamId,
6519
+ keyId
6520
+ });
6521
+ return data?.keys?.[0] ?? data?.key ?? data?.data ?? data;
6522
+ }
6523
+ /**
6524
+ * Revoke an existing key. Form-encoded POST per Spaceship's
6525
+ * `revoke_key!`. Used for cleaning up orphan keys created during
6526
+ * failed wizard runs.
6527
+ */
6528
+ async revokeApiKey(keyId) {
6529
+ await this.post("account/auth/key/revoke", {
6530
+ teamId: this.teamId,
6531
+ keyId
6532
+ });
6533
+ }
6534
+ // ── APNs Authentication Keys (.p8) ──────────────────────────────────────
6535
+ /** Apple's hardcoded service id for APNs auth keys (from Spaceship). */
6536
+ static APNS_SERVICE_ID = "U27F4V844T";
6537
+ /**
6538
+ * Create an APNs Authentication Key (.p8) via the developer portal.
6539
+ *
6540
+ * Apple requires BOTH `serviceConfigurations` (the map) AND
6541
+ * `serviceConfigurationsRequests` (the array of full config objects)
6542
+ * in the v2/create body. Just sending the map alone results in
6543
+ * "No value was provided for the parameter 'serviceConfigurations'".
6544
+ *
6545
+ * The exact body comes from @expo/apple-utils v2.1.21 — both fields
6546
+ * are produced by separate functions (`d` for the map, `h` for the
6547
+ * array) and merged into the create payload.
6548
+ */
6549
+ async createApnsAuthKey(rawName) {
6550
+ const name = rawName.replace(/[^A-Za-z0-9 _\-.]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
6551
+ if (!name) throw new Error("createApnsAuthKey: name is empty after sanitization");
6552
+ try {
6553
+ await this.listApiKeys();
6554
+ } catch {
6555
+ }
6556
+ const body = {
6557
+ teamId: this.teamId,
6558
+ name,
6559
+ serviceConfigurations: {
6560
+ [_PortalClient.APNS_SERVICE_ID]: []
6561
+ },
6562
+ serviceConfigurationsRequests: [
6563
+ {
6564
+ isNew: true,
6565
+ serviceId: _PortalClient.APNS_SERVICE_ID,
6566
+ identifiers: {},
6567
+ environment: "all",
6568
+ scope: "team"
6569
+ }
6570
+ ],
6571
+ scope: "team"
6572
+ };
6573
+ const result = await this.postJson("account/auth/key/v2/create", body);
6574
+ const created = result.body?.keys?.[0];
6575
+ if (!created?.keyId) {
6576
+ throw new Error(`createApnsAuthKey: no key id in response. Body: ${result.rawText.slice(0, 400)}`);
6577
+ }
6578
+ const pem = await this.downloadApiKey(created.keyId);
6579
+ return { keyId: created.keyId, pem, raw: created };
6580
+ }
6581
+ /**
6582
+ * Find existing APNs auth keys in the team. Used by the wizard to
6583
+ * detect "you already have one" cases and offer to reuse rather than
6584
+ * proliferate keys.
6585
+ */
6586
+ async listApnsAuthKeys() {
6587
+ const list = await this.listApiKeys();
6588
+ return (list.keys ?? []).filter(
6589
+ (k) => (k.services ?? []).some((s) => s.id === _PortalClient.APNS_SERVICE_ID || /apns/i.test(s.name ?? ""))
6590
+ ).map((k) => ({
6591
+ keyId: k.keyId,
6592
+ keyName: k.keyName,
6593
+ canDownload: Boolean(k.canDownload)
6594
+ }));
6595
+ }
6596
+ /**
6597
+ * Download an existing App Store Connect API key as PEM. Spaceship's
6598
+ * `download_key` endpoint — GET with teamId + keyId in query string.
6599
+ * Returns the raw .p8 PEM text. Apple shows the .p8 exactly once
6600
+ * (after creation), so call this immediately after createApiKey.
6601
+ */
6602
+ async downloadApiKey(keyId) {
6603
+ const { status, rawText, contentType } = await this.getRaw("account/auth/key/download", {
6604
+ teamId: this.teamId,
6605
+ keyId
6606
+ });
6607
+ if (status !== 200) {
6608
+ throw new Error(`downloadApiKey failed (${status}): ${rawText.slice(0, 200)}`);
6609
+ }
6610
+ if (rawText.includes("BEGIN PRIVATE KEY")) return rawText;
6611
+ if (contentType.includes("json")) {
6612
+ try {
6613
+ const parsed = JSON.parse(rawText);
6614
+ const candidates = [
6615
+ parsed?.privateKey,
6616
+ parsed?.key?.privateKey,
6617
+ parsed?.keys?.[0]?.privateKey,
6618
+ parsed?.data?.attributes?.privateKey,
6619
+ parsed?.value
6620
+ ];
6621
+ for (const c of candidates) {
6622
+ if (typeof c === "string" && c.includes("BEGIN PRIVATE KEY")) return c;
6623
+ }
6624
+ } catch {
6625
+ }
6626
+ }
6627
+ throw new Error(`downloadApiKey returned no PEM. Content-Type=${contentType} body=${rawText.slice(0, 300)}`);
6628
+ }
6629
+ /**
6630
+ * List existing API keys via the portal. Used to discover the team's
6631
+ * issuer id — every key in the team shares the same issuer id, and
6632
+ * altool needs it alongside the key id and .p8 file.
6633
+ *
6634
+ * Form-encoded POST (NOT JSON), with pagination params, per Spaceship's
6635
+ * `list_keys` method. JSON returned 415 from this endpoint.
6636
+ */
6637
+ async listApiKeys() {
6638
+ const data = await this.post("account/auth/key/list", {
6639
+ teamId: this.teamId,
6640
+ pageNumber: "1",
6641
+ pageSize: "500",
6642
+ sort: "name=asc"
6643
+ });
6644
+ const keys = data?.keys ?? [];
6645
+ const issuerId = data?.issuerId ?? data?.issuer ?? data?.team?.issuerId ?? keys[0]?.issuerId ?? keys[0]?.issuer;
6646
+ return { keys, issuerId };
6647
+ }
6170
6648
  async post(path, params) {
6171
6649
  const body = new URLSearchParams();
6172
6650
  for (const [k, v] of Object.entries(params)) {
@@ -6234,6 +6712,31 @@ var init_portal = __esm({
6234
6712
  async deleteAppId(appIdId) {
6235
6713
  await this.post("account/ios/identifiers/deleteAppId.action", { teamId: this.teamId, appIdId });
6236
6714
  }
6715
+ /**
6716
+ * Toggle a capability (a.k.a. "service") on a bundle ID. Mirrors
6717
+ * Spaceship::Portal's `update_service` — the legacy portal endpoint
6718
+ * still works and is the only session-cookie-authenticated way to flip
6719
+ * capabilities like Push Notifications and Sign in with Apple from
6720
+ * outside the Apple Developer web UI.
6721
+ *
6722
+ * Common service codes (from Spaceship portal/portal_client.rb):
6723
+ * push → push notifications
6724
+ * APG3427HIY → Sign in with Apple
6725
+ * IAD53UNK2F → in-app purchase
6726
+ * homeKit → HomeKit
6727
+ * healthKit → HealthKit
6728
+ *
6729
+ * @param featureType Apple's internal capability key (string code or short name)
6730
+ * @param enabled true → turn on, false → turn off
6731
+ */
6732
+ async setBundleCapability(params) {
6733
+ await this.post("account/ios/identifiers/updateAppService.action", {
6734
+ teamId: this.teamId,
6735
+ displayId: params.appIdId,
6736
+ featureType: params.featureType,
6737
+ featureValue: params.enabled ? "on" : "off"
6738
+ });
6739
+ }
6237
6740
  // ── Certificates ───────────────────────────────────────────────────────
6238
6741
  async listCertificates(types) {
6239
6742
  const iosTypes = types ?? [
@@ -6251,12 +6754,21 @@ var init_portal = __esm({
6251
6754
  return data.certRequests ?? [];
6252
6755
  }
6253
6756
  async listPushCertificates() {
6757
+ const types = Array.from(
6758
+ /* @__PURE__ */ new Set([
6759
+ _PortalClient.CERT_TYPES.IOS_PRODUCTION_PUSH,
6760
+ _PortalClient.CERT_TYPES.IOS_DEVELOPMENT_PUSH,
6761
+ // Legacy push codes from OLDER_IOS_CERTIFICATE_TYPES
6762
+ "3BQKVH9I2X",
6763
+ "BKLRAVXMGM"
6764
+ ])
6765
+ );
6254
6766
  const data = await this.post("account/ios/certificate/listCertRequests.action", {
6255
6767
  teamId: this.teamId,
6256
6768
  pageSize: "500",
6257
6769
  pageNumber: "1",
6258
6770
  sort: "certRequestStatusCode=asc",
6259
- types: [_PortalClient.CERT_TYPES.IOS_PRODUCTION_PUSH, _PortalClient.CERT_TYPES.IOS_DEVELOPMENT_PUSH].join(",")
6771
+ types: types.join(",")
6260
6772
  });
6261
6773
  return data.certRequests ?? [];
6262
6774
  }
@@ -6398,18 +6910,332 @@ var init_portal = __esm({
6398
6910
  }
6399
6911
  });
6400
6912
 
6913
+ // src/client/iris.ts
6914
+ var IRIS_BASE, IrisClient, IrisAuthError;
6915
+ var init_iris = __esm({
6916
+ "src/client/iris.ts"() {
6917
+ IRIS_BASE = "https://appstoreconnect.apple.com/iris/v1";
6918
+ IrisClient = class {
6919
+ constructor(session) {
6920
+ this.session = session;
6921
+ this.cookieStr = Object.entries(session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
6922
+ }
6923
+ session;
6924
+ cookieStr;
6925
+ buildHeaders() {
6926
+ return {
6927
+ Cookie: this.cookieStr,
6928
+ Accept: "application/vnd.api+json, application/json, text/plain, */*",
6929
+ "Content-Type": "application/vnd.api+json",
6930
+ "X-Requested-With": "XMLHttpRequest",
6931
+ // Mimic the App Store Connect web UI's user agent
6932
+ "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"
6933
+ };
6934
+ }
6935
+ /**
6936
+ * Issue a request and absorb any new cookies (Apple rotates session
6937
+ * cookies on a lot of iris calls).
6938
+ */
6939
+ async request(method, path, body) {
6940
+ const headers = this.buildHeaders();
6941
+ if (process.env.FASTNODE_DEBUG) {
6942
+ console.log(`[iris] ${method} ${path}${body ? ` body=${JSON.stringify(body).slice(0, 200)}` : ""}`);
6943
+ }
6944
+ const res = await fetch(`${IRIS_BASE}${path}`, {
6945
+ method,
6946
+ headers,
6947
+ body: body ? JSON.stringify(body) : void 0
6948
+ });
6949
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
6950
+ const m = raw.match(/^([^=]+)=([^;]*)/);
6951
+ if (m) {
6952
+ this.session.cookies[m[1]] = m[2];
6953
+ }
6954
+ }
6955
+ this.cookieStr = Object.entries(this.session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
6956
+ const text = await res.text();
6957
+ let parsed = null;
6958
+ try {
6959
+ parsed = text ? JSON.parse(text) : null;
6960
+ } catch {
6961
+ }
6962
+ if (process.env.FASTNODE_DEBUG) {
6963
+ console.log(`[iris] \u2192 ${res.status} ${text.slice(0, 300)}`);
6964
+ }
6965
+ return { status: res.status, body: parsed, rawText: text };
6966
+ }
6967
+ /**
6968
+ * List existing API keys. Used both as a sanity check (the user has
6969
+ * permission) and to discover the team's issuer id (which is the same
6970
+ * for every key in the team and is needed by altool).
6971
+ *
6972
+ * Important: Apple's iris endpoint rejects most query parameters with
6973
+ * `PARAMETER_ERROR.INVALID`. Call with no params and let the API return
6974
+ * its default page.
6975
+ */
6976
+ async listApiKeys() {
6977
+ const { status, body, rawText } = await this.request("GET", "/apiKeys");
6978
+ if (status === 403) {
6979
+ throw new IrisAuthError(
6980
+ "You don't have permission to manage App Store Connect API keys (this requires the Account Holder or Admin role)."
6981
+ );
6982
+ }
6983
+ if (status >= 400 || !body) {
6984
+ throw new Error(`iris listApiKeys failed (${status}): ${rawText.slice(0, 200)}`);
6985
+ }
6986
+ const issuerId = body?.meta?.issuerId ?? body?.data?.[0]?.attributes?.issuerId;
6987
+ const keys = (body.data ?? []).map((d) => ({
6988
+ id: d.id,
6989
+ kid: d.attributes?.kid ?? d.id,
6990
+ nickname: d.attributes?.nickname ?? "",
6991
+ issuerId: d.attributes?.issuerId ?? issuerId,
6992
+ roles: d.attributes?.roles ?? [],
6993
+ allAppsVisible: d.attributes?.allAppsVisible ?? true
6994
+ }));
6995
+ return { keys, issuerId };
6996
+ }
6997
+ /**
6998
+ * Create a new API key. Apple returns the .p8 PEM exactly once in the
6999
+ * create response — capture it immediately or the key is dead weight.
7000
+ *
7001
+ * `keyType` is required by Apple's iris API:
7002
+ * • PUBLIC_API — the standard team-wide API key (what TestFlight uploads need)
7003
+ * • MARKETING_API — for App Analytics access only
7004
+ */
7005
+ async createApiKey(input) {
7006
+ const requestBody = {
7007
+ data: {
7008
+ type: "apiKeys",
7009
+ attributes: {
7010
+ nickname: input.nickname,
7011
+ roles: input.roles ?? ["APP_MANAGER"],
7012
+ allAppsVisible: input.allAppsVisible ?? true,
7013
+ keyType: input.keyType ?? "PUBLIC_API"
7014
+ }
7015
+ }
7016
+ };
7017
+ console.log("[iris] createApiKey request:", JSON.stringify(requestBody));
7018
+ const { status, body, rawText } = await this.request("POST", "/apiKeys", requestBody);
7019
+ console.log(`[iris] createApiKey response: ${status} ${rawText.slice(0, 800)}`);
7020
+ if (status === 403) {
7021
+ throw new IrisAuthError(
7022
+ "You don't have permission to create App Store Connect API keys (this requires the Account Holder or Admin role)."
7023
+ );
7024
+ }
7025
+ if (status >= 400 || !body) {
7026
+ const msg = body?.errors?.[0]?.detail ?? body?.errors?.[0]?.title ?? rawText.slice(0, 300) ?? `iris createApiKey failed (${status})`;
7027
+ throw new Error(msg);
7028
+ }
7029
+ const data = body.data ?? body;
7030
+ if (!data?.id) {
7031
+ throw new Error(`iris createApiKey returned no key id. Raw response: ${rawText.slice(0, 400)}`);
7032
+ }
7033
+ const key = {
7034
+ id: data.id,
7035
+ kid: data.attributes?.kid ?? data.id,
7036
+ nickname: data.attributes?.nickname ?? input.nickname,
7037
+ privateKey: data.attributes?.privateKey,
7038
+ issuerId: data.attributes?.issuerId,
7039
+ roles: data.attributes?.roles ?? input.roles ?? ["APP_MANAGER"],
7040
+ allAppsVisible: data.attributes?.allAppsVisible ?? true
7041
+ };
7042
+ return key;
7043
+ }
7044
+ /**
7045
+ * Fetch the .p8 PEM AND the team's issuer id for a previously-created
7046
+ * ASC API key. Both come back in a single request via:
7047
+ *
7048
+ * GET /iris/v1/apiKeys/{id}?fields[apiKeys]=privateKey&include=provider
7049
+ *
7050
+ * Sparse fieldset requesting ONLY `privateKey` is the magic that makes
7051
+ * Apple actually include the (base64-encoded) PEM in the response —
7052
+ * the default view returns `privateKey: null`. The `include=provider`
7053
+ * pulls the team's contentProvider record into `included[]`, and its
7054
+ * `id` field IS the issuer id altool needs (a UUID like
7055
+ * `cfadaf94-44ca-4c2c-8fa6-ecc28d3c7d0d`).
7056
+ *
7057
+ * Reverse-engineered from @expo/apple-utils v2.1.21 (the package that
7058
+ * powers EAS CLI), specifically the `ApiKey.downloadAsync` method
7059
+ * combined with `ApiKey.infoAsync` reading `provider?.id`.
7060
+ *
7061
+ * Returns `{ pem, issuerId }`, or `null` if Apple won't return the
7062
+ * private key (typically because the key was already downloaded once).
7063
+ */
7064
+ async fetchApiKeyPrivateKey(keyId) {
7065
+ try {
7066
+ const url = `${IRIS_BASE}/apiKeys/${keyId}?fields%5BapiKeys%5D=privateKey&include=provider`;
7067
+ const res = await fetch(url, {
7068
+ headers: {
7069
+ ...this.buildHeaders(),
7070
+ Accept: "application/vnd.api+json, application/json, text/plain, */*"
7071
+ }
7072
+ });
7073
+ const text = await res.text();
7074
+ console.log(`[iris] fetchApiKeyPrivateKey \u2192 ${res.status} (${text.length} bytes)`);
7075
+ if (res.status !== 200) {
7076
+ console.log(`[iris] err: ${text.slice(0, 300)}`);
7077
+ return null;
7078
+ }
7079
+ const body = JSON.parse(text);
7080
+ const encoded = body?.data?.attributes?.privateKey;
7081
+ if (typeof encoded !== "string" || encoded.length === 0) {
7082
+ console.log(`[iris] no privateKey in response`);
7083
+ return null;
7084
+ }
7085
+ const pem = Buffer.from(encoded, "base64").toString("utf-8");
7086
+ if (!pem.includes("BEGIN PRIVATE KEY")) {
7087
+ console.log(`[iris] decoded body didn't contain PEM marker`);
7088
+ return null;
7089
+ }
7090
+ const provider = (body?.included ?? []).find((i) => i.type === "contentProviders");
7091
+ const issuerId = provider?.id;
7092
+ console.log(`[iris] \u2713 PEM ${pem.length} bytes, issuerId=${issuerId ?? "(missing)"}`);
7093
+ return { pem, issuerId };
7094
+ } catch (err) {
7095
+ console.log(`[iris] fetchApiKeyPrivateKey error: ${err.message}`);
7096
+ return null;
7097
+ }
7098
+ }
7099
+ /**
7100
+ * @deprecated Kept as a fallback probe for debugging. The real path
7101
+ * is the sparse fieldset above — everything in this brute-force list
7102
+ * returned 404 during testing. Left here so we can re-verify if Apple
7103
+ * changes anything.
7104
+ */
7105
+ async fetchApiKeyPrivateKeyBruteForce(keyId) {
7106
+ const attempts = [
7107
+ // Sparse fieldsets — JSON-API spec lets us request specific attributes.
7108
+ // Apple may exclude privateKey by default and return it only when asked.
7109
+ { kind: "GET", path: `/apiKeys/${keyId}?fields[apiKeys]=privateKey,kid,issuerId,nickname,roles` },
7110
+ { kind: "GET", path: `/apiKeys/${keyId}?include=privateKey` },
7111
+ { kind: "GET", path: `/apiKeys/${keyId}?fields[apiKeys]=privateKey` },
7112
+ // Custom Accept headers — maybe Apple negotiates content type
7113
+ { kind: "GET", path: `/apiKeys/${keyId}`, accept: "application/x-pem-file" },
7114
+ { kind: "GET", path: `/apiKeys/${keyId}`, accept: "application/octet-stream" },
7115
+ { kind: "GET", path: `/apiKeys/${keyId}/p8`, accept: "application/x-pem-file" },
7116
+ // POST variants — Apple sometimes uses POST to mint a download token
7117
+ { kind: "POST", path: `/apiKeys/${keyId}/secrets`, body: {} },
7118
+ { kind: "POST", path: `/apiKeys/${keyId}/regenerate`, body: {} },
7119
+ { kind: "POST", path: `/apiKeys/${keyId}/p8`, body: {} },
7120
+ // Bare GET (last) — confirms canonical fields, in case my parser missed any
7121
+ { kind: "GET", path: `/apiKeys/${keyId}` },
7122
+ // Different host — the legacy developer.apple.com user-and-access namespace
7123
+ { kind: "ABSOLUTE", url: `https://appstoreconnect.apple.com/api/v1/apiKeys/${keyId}/p8` },
7124
+ { kind: "ABSOLUTE", url: `https://appstoreconnect.apple.com/access/api/keys/${keyId}/download` }
7125
+ ];
7126
+ for (const attempt of attempts) {
7127
+ try {
7128
+ let result;
7129
+ if (attempt.kind === "GET") {
7130
+ result = await this.requestWithHeaders("GET", attempt.path, void 0, {
7131
+ ...attempt.accept ? { Accept: attempt.accept } : {}
7132
+ });
7133
+ console.log(`[iris] fetchApiKeyPrivateKey GET ${attempt.path}${attempt.accept ? ` accept=${attempt.accept}` : ""} \u2192 ${result.status} (${result.rawText.length} bytes)`);
7134
+ } else if (attempt.kind === "POST") {
7135
+ result = await this.request("POST", attempt.path, attempt.body);
7136
+ console.log(`[iris] fetchApiKeyPrivateKey POST ${attempt.path} \u2192 ${result.status} (${result.rawText.length} bytes)`);
7137
+ } else {
7138
+ result = await this.requestAbsolute("GET", attempt.url);
7139
+ console.log(`[iris] fetchApiKeyPrivateKey GET ${attempt.url} \u2192 ${result.status} (${result.rawText.length} bytes)`);
7140
+ }
7141
+ if (result.status === 200) {
7142
+ console.log(`[iris] body: ${result.rawText.slice(0, 1500)}`);
7143
+ }
7144
+ if (result.status !== 200) continue;
7145
+ if (result.rawText && result.rawText.includes("BEGIN PRIVATE KEY")) {
7146
+ console.log(`[iris] \u2713 found PEM (plain text response)`);
7147
+ return result.rawText;
7148
+ }
7149
+ if (result.body) {
7150
+ const candidates = [
7151
+ result.body?.data?.attributes?.privateKey,
7152
+ result.body?.data?.attributes?.p8,
7153
+ result.body?.data?.attributes?.privateKeyContent,
7154
+ result.body?.data?.attributes?.kpem,
7155
+ result.body?.data?.attributes?.secret,
7156
+ result.body?.data?.attributes?.keyContent,
7157
+ result.body?.privateKey,
7158
+ result.body?.p8,
7159
+ result.body?.value,
7160
+ result.body?.secret
7161
+ ];
7162
+ for (const c of candidates) {
7163
+ if (typeof c === "string" && c.includes("BEGIN PRIVATE KEY")) {
7164
+ console.log(`[iris] \u2713 found PEM in JSON response`);
7165
+ return c;
7166
+ }
7167
+ }
7168
+ }
7169
+ } catch (err) {
7170
+ console.log(`[iris] fetchApiKeyPrivateKey attempt failed: ${err.message}`);
7171
+ }
7172
+ }
7173
+ console.log(`[iris] fetchApiKeyPrivateKey: no working download path for key ${keyId}`);
7174
+ return null;
7175
+ }
7176
+ /** Internal: like request() but lets us override headers. */
7177
+ async requestWithHeaders(method, path, body, extraHeaders = {}) {
7178
+ const headers = { ...this.buildHeaders(), ...extraHeaders };
7179
+ const res = await fetch(`${IRIS_BASE}${path}`, {
7180
+ method,
7181
+ headers,
7182
+ body: body ? JSON.stringify(body) : void 0
7183
+ });
7184
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
7185
+ const m = raw.match(/^([^=]+)=([^;]*)/);
7186
+ if (m) this.session.cookies[m[1]] = m[2];
7187
+ }
7188
+ this.cookieStr = Object.entries(this.session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
7189
+ const text = await res.text();
7190
+ let parsed = null;
7191
+ try {
7192
+ parsed = text ? JSON.parse(text) : null;
7193
+ } catch {
7194
+ }
7195
+ return { status: res.status, body: parsed, rawText: text };
7196
+ }
7197
+ /** Internal: fetch an absolute URL with the session cookies. */
7198
+ async requestAbsolute(method, url) {
7199
+ const headers = this.buildHeaders();
7200
+ const res = await fetch(url, { method, headers });
7201
+ const text = await res.text();
7202
+ let parsed = null;
7203
+ try {
7204
+ parsed = text ? JSON.parse(text) : null;
7205
+ } catch {
7206
+ }
7207
+ return { status: res.status, body: parsed, rawText: text };
7208
+ }
7209
+ async deleteApiKey(keyId) {
7210
+ await this.request("DELETE", `/apiKeys/${keyId}`);
7211
+ }
7212
+ };
7213
+ IrisAuthError = class extends Error {
7214
+ constructor(message) {
7215
+ super(message);
7216
+ this.name = "IrisAuthError";
7217
+ }
7218
+ };
7219
+ }
7220
+ });
7221
+
6401
7222
  // src/client/index.ts
6402
7223
  var client_exports = {};
6403
7224
  __export(client_exports, {
6404
7225
  AppStoreConnectClient: () => AppStoreConnectClient,
6405
7226
  AppStoreConnectError: () => AppStoreConnectError,
7227
+ IrisAuthError: () => IrisAuthError,
7228
+ IrisClient: () => IrisClient,
6406
7229
  PortalClient: () => PortalClient,
6407
7230
  ProgramLicenseAgreementError: () => ProgramLicenseAgreementError,
6408
7231
  RateLimitError: () => RateLimitError,
6409
7232
  RequestClient: () => RequestClient,
6410
7233
  TokenManager: () => TokenManager,
7234
+ beginLogin: () => beginLogin,
6411
7235
  checkPortalResponse: () => checkPortalResponse,
6412
- loginWithAppleId: () => loginWithAppleId
7236
+ loginWithAppleId: () => loginWithAppleId,
7237
+ requestSmsCode: () => requestSmsCode,
7238
+ submitTwoFactorCode: () => submitTwoFactorCode
6413
7239
  });
6414
7240
  var AppStoreConnectClient;
6415
7241
  var init_client = __esm({
@@ -6454,6 +7280,7 @@ var init_client = __esm({
6454
7280
  init_request();
6455
7281
  init_apple_session();
6456
7282
  init_portal();
7283
+ init_iris();
6457
7284
  AppStoreConnectClient = class {
6458
7285
  http;
6459
7286
  certificates;