@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.
- package/README.md +148 -0
- package/dist/actions/index.cjs +964 -137
- package/dist/actions/index.cjs.map +1 -1
- package/dist/actions/index.mjs +964 -137
- package/dist/actions/index.mjs.map +1 -1
- package/dist/cert/index.cjs +40 -0
- package/dist/cert/index.cjs.map +1 -1
- package/dist/cert/index.d.cts +3 -3
- package/dist/cert/index.d.ts +3 -3
- package/dist/cert/index.mjs +37 -1
- package/dist/cert/index.mjs.map +1 -1
- package/dist/{cert-BmJGtYtb.d.cts → cert-6m1R8UeW.d.cts} +1 -1
- package/dist/{cert-owkOFNW8.d.ts → cert-DtYDJBjG.d.ts} +1 -1
- package/dist/cli/index.cjs +964 -137
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.mjs +964 -137
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.cjs +730 -109
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.mjs +726 -110
- package/dist/client/index.mjs.map +1 -1
- package/dist/deliver/index.d.cts +2 -2
- package/dist/deliver/index.d.ts +2 -2
- package/dist/index-AD8QXY1T.d.cts +158 -0
- package/dist/{index-DfCUVJX1.d.cts → index-BW8wQUGC.d.cts} +1 -1
- package/dist/{index-EcVvViW8.d.cts → index-BWK-T2-S.d.cts} +1 -1
- package/dist/{index-B0yhu_LZ.d.ts → index-BpiEBPVd.d.ts} +1 -1
- package/dist/index-CCs5irG7.d.cts +61 -0
- package/dist/{index-B9yXicrQ.d.ts → index-CLsvyTkh.d.ts} +1 -1
- package/dist/{index-BLubSlgK.d.cts → index-CMH_GpN9.d.cts} +358 -2
- package/dist/index-CQKpbHZt.d.ts +61 -0
- package/dist/{index-BQJIJlK7.d.cts → index-CQd4bo3G.d.cts} +3 -3
- package/dist/{index-CWNLnSOL.d.cts → index-CdYdwxpQ.d.cts} +1 -1
- package/dist/{index-BFdNOmPJ.d.cts → index-Ce-WMV7c.d.cts} +1 -1
- package/dist/{index-TfXqwuac.d.ts → index-CojSGJ7m.d.ts} +1 -1
- package/dist/{index-1Vu2cRQ2.d.ts → index-CsFIp7OO.d.ts} +1 -1
- package/dist/{index-D1Izm7qt.d.ts → index-D2z7RiAM.d.ts} +358 -2
- package/dist/index-D82CmjMP.d.ts +158 -0
- package/dist/{index-BN0ubxjZ.d.ts → index-DZaPkUnC.d.ts} +3 -3
- package/dist/{index-D5eihzwM.d.cts → index-KCB2q1-8.d.cts} +1 -1
- package/dist/{index-A5MimeQk.d.ts → index-Kq3rbG0u.d.ts} +1 -1
- package/dist/index.cjs +1322 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +199 -10
- package/dist/index.d.ts +199 -10
- package/dist/index.mjs +1319 -139
- package/dist/index.mjs.map +1 -1
- package/dist/lane/index.cjs +964 -137
- package/dist/lane/index.cjs.map +1 -1
- package/dist/lane/index.mjs +964 -137
- package/dist/lane/index.mjs.map +1 -1
- package/dist/match/index.d.cts +4 -4
- package/dist/match/index.d.ts +4 -4
- package/dist/pem/index.d.cts +2 -2
- package/dist/pem/index.d.ts +2 -2
- package/dist/pilot/index.cjs +190 -12
- package/dist/pilot/index.cjs.map +1 -1
- package/dist/pilot/index.d.cts +2 -2
- package/dist/pilot/index.d.ts +2 -2
- package/dist/pilot/index.mjs +186 -14
- package/dist/pilot/index.mjs.map +1 -1
- package/dist/precheck/index.d.cts +2 -2
- package/dist/precheck/index.d.ts +2 -2
- package/dist/produce/index.d.cts +2 -2
- package/dist/produce/index.d.ts +2 -2
- package/dist/sigh/index.d.cts +2 -2
- package/dist/sigh/index.d.ts +2 -2
- package/package.json +3 -1
- package/dist/index-DfirmFbC.d.ts +0 -32
- package/dist/index-DkvinY7_.d.ts +0 -54
- package/dist/index-HRiaaX-Q.d.cts +0 -54
- package/dist/index-lt819Jgj.d.cts +0 -32
package/dist/actions/index.cjs
CHANGED
|
@@ -178,8 +178,8 @@ async function revokeExpiredCertificates(options) {
|
|
|
178
178
|
return count;
|
|
179
179
|
}
|
|
180
180
|
async function importToKeychain(certData, keychainPath, keychainPassword) {
|
|
181
|
-
const { tmpdir:
|
|
182
|
-
const tempCertPath = path.join(
|
|
181
|
+
const { tmpdir: tmpdir8 } = await import('os');
|
|
182
|
+
const tempCertPath = path.join(tmpdir8(), `fastnode-cert-${Date.now()}.cer`);
|
|
183
183
|
await promises.writeFile(tempCertPath, certData);
|
|
184
184
|
try {
|
|
185
185
|
const args = [
|
|
@@ -1611,14 +1611,14 @@ var init_building = __esm({
|
|
|
1611
1611
|
name: "clean_build_artifacts",
|
|
1612
1612
|
description: "Remove build artifacts (IPA, dSYM, etc.)",
|
|
1613
1613
|
async run(params, ctx) {
|
|
1614
|
-
const { rm:
|
|
1614
|
+
const { rm: rm4 } = await import('fs/promises');
|
|
1615
1615
|
const paths = [
|
|
1616
1616
|
ctx.get("IPA_OUTPUT_PATH"),
|
|
1617
1617
|
ctx.get("PKG_OUTPUT_PATH"),
|
|
1618
1618
|
ctx.get("DSYM_OUTPUT_PATH")
|
|
1619
1619
|
].filter(Boolean);
|
|
1620
1620
|
for (const p of paths) {
|
|
1621
|
-
await
|
|
1621
|
+
await rm4(p, { force: true }).catch(() => {
|
|
1622
1622
|
});
|
|
1623
1623
|
}
|
|
1624
1624
|
return { cleaned: paths };
|
|
@@ -1777,9 +1777,9 @@ Please accept the agreement at https://developer.apple.com/account before contin
|
|
|
1777
1777
|
throw new Error(msg);
|
|
1778
1778
|
}
|
|
1779
1779
|
}
|
|
1780
|
-
async function
|
|
1781
|
-
const log = config.onLog ??
|
|
1782
|
-
|
|
1780
|
+
async function beginLogin(config) {
|
|
1781
|
+
const log = config.onLog ?? (() => {
|
|
1782
|
+
});
|
|
1783
1783
|
log("Fetching Apple service key...");
|
|
1784
1784
|
const serviceKey = await fetchServiceKey();
|
|
1785
1785
|
const initRes = await fetch(`${AUTH_BASE}/signin?widgetKey=${serviceKey}`, {
|
|
@@ -1796,27 +1796,94 @@ async function loginWithAppleId(config) {
|
|
|
1796
1796
|
const cookieStr = Object.entries(initCookies).map(([k, v]) => k.startsWith("DES") ? `${k}="${v}"` : `${k}=${v}`).join("; ");
|
|
1797
1797
|
log("Starting SIRP Apple ID login...");
|
|
1798
1798
|
const response = await doSirp(config.email, config.password, serviceKey, cookieStr, hashcash);
|
|
1799
|
-
const
|
|
1799
|
+
const baseState = {
|
|
1800
1800
|
cookies: { ...initCookies, ...response.cookies },
|
|
1801
1801
|
sessionId: response.sessionId,
|
|
1802
1802
|
scnt: response.scnt,
|
|
1803
1803
|
serviceKey,
|
|
1804
|
-
userEmail: config.email
|
|
1805
|
-
pendingAgreements: []
|
|
1804
|
+
userEmail: config.email
|
|
1806
1805
|
};
|
|
1807
1806
|
if (response.status === 403) {
|
|
1808
1807
|
throw new Error(`Invalid username and password combination. Used '${config.email}' as the username.`);
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
await fetchOlympusSession(session);
|
|
1812
|
-
} else if (response.status === 409) {
|
|
1813
|
-
await handleTwoStepOrFactor(session, log, promptFn);
|
|
1814
|
-
await fetchOlympusSession(session);
|
|
1815
|
-
} else if (response.status === 412) {
|
|
1808
|
+
}
|
|
1809
|
+
if (response.status === 412) {
|
|
1816
1810
|
throw new Error("Need to acknowledge Apple's Apple ID and Privacy statement. Please manually log into https://appleid.apple.com.");
|
|
1811
|
+
}
|
|
1812
|
+
if (response.status === 200) {
|
|
1813
|
+
log("Login successful (no 2FA).");
|
|
1814
|
+
const session = { ...baseState, pendingAgreements: [] };
|
|
1815
|
+
await finalizeSession(session, log);
|
|
1816
|
+
return { kind: "logged_in", session };
|
|
1817
|
+
}
|
|
1818
|
+
if (response.status === 409) {
|
|
1819
|
+
log("2FA required, fetching challenge...");
|
|
1820
|
+
const challenge = await fetchTwoFactorChallenge(baseState);
|
|
1821
|
+
return { kind: "needs_2fa", pending: baseState, challenge };
|
|
1822
|
+
}
|
|
1823
|
+
throw new Error(`Unexpected login response (${response.status}): ${JSON.stringify(response.body).slice(0, 500)}`);
|
|
1824
|
+
}
|
|
1825
|
+
async function fetchTwoFactorChallenge(pending) {
|
|
1826
|
+
const r = await fetch(`${AUTH_BASE}`, { headers: buildAuthHeadersFromPending(pending) });
|
|
1827
|
+
const body = await r.json();
|
|
1828
|
+
const trustedPhoneNumbers = (body.trustedPhoneNumbers ?? []).map((p) => ({
|
|
1829
|
+
id: p.id,
|
|
1830
|
+
numberWithDialCode: p.numberWithDialCode,
|
|
1831
|
+
pushMode: p.pushMode
|
|
1832
|
+
}));
|
|
1833
|
+
const phoneOnly = body.noTrustedDevices === true;
|
|
1834
|
+
const codeLength = body.securityCode?.length ?? 6;
|
|
1835
|
+
return {
|
|
1836
|
+
kind: phoneOnly ? "phone_only" : "device",
|
|
1837
|
+
codeLength,
|
|
1838
|
+
trustedPhoneNumbers
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
async function requestSmsCode(input) {
|
|
1842
|
+
await fetch(`${AUTH_BASE}/verify/phone`, {
|
|
1843
|
+
method: "PUT",
|
|
1844
|
+
headers: { ...buildAuthHeadersFromPending(input.pending), "Content-Type": "application/json" },
|
|
1845
|
+
body: JSON.stringify({ phoneNumber: { id: input.phoneId }, mode: input.mode ?? "sms" })
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
async function submitTwoFactorCode(input) {
|
|
1849
|
+
const log = input.onLog ?? (() => {
|
|
1850
|
+
});
|
|
1851
|
+
let path;
|
|
1852
|
+
let body;
|
|
1853
|
+
if (input.source === "device") {
|
|
1854
|
+
path = `${AUTH_BASE}/verify/trusteddevice/securitycode`;
|
|
1855
|
+
body = JSON.stringify({ securityCode: { code: input.code } });
|
|
1817
1856
|
} else {
|
|
1818
|
-
throw new Error(
|
|
1857
|
+
if (input.phoneId == null) throw new Error("phoneId is required when source='phone'");
|
|
1858
|
+
path = `${AUTH_BASE}/verify/phone/securitycode`;
|
|
1859
|
+
body = JSON.stringify({
|
|
1860
|
+
securityCode: { code: input.code },
|
|
1861
|
+
phoneNumber: { id: input.phoneId },
|
|
1862
|
+
mode: input.mode ?? "sms"
|
|
1863
|
+
});
|
|
1819
1864
|
}
|
|
1865
|
+
log("Submitting 2FA code...");
|
|
1866
|
+
const verifyRes = await fetch(path, {
|
|
1867
|
+
method: "POST",
|
|
1868
|
+
headers: { ...buildAuthHeadersFromPending(input.pending), "Content-Type": "application/json" },
|
|
1869
|
+
body
|
|
1870
|
+
});
|
|
1871
|
+
if (!verifyRes.ok) {
|
|
1872
|
+
const err = await verifyRes.json().catch(() => ({}));
|
|
1873
|
+
const msgs = (err?.service_errors ?? err?.serviceErrors ?? []).map((e) => e.message).filter(Boolean);
|
|
1874
|
+
throw new Error(msgs.join("; ") || `Verification failed (${verifyRes.status})`);
|
|
1875
|
+
}
|
|
1876
|
+
const session = {
|
|
1877
|
+
...input.pending,
|
|
1878
|
+
cookies: { ...input.pending.cookies, ...parseCookies(verifyRes.headers) },
|
|
1879
|
+
pendingAgreements: []
|
|
1880
|
+
};
|
|
1881
|
+
await trustSession(session);
|
|
1882
|
+
await finalizeSession(session, log);
|
|
1883
|
+
return session;
|
|
1884
|
+
}
|
|
1885
|
+
async function finalizeSession(session, log) {
|
|
1886
|
+
await fetchOlympusSession(session);
|
|
1820
1887
|
const agreements = await fetchContractMessages(session);
|
|
1821
1888
|
if (agreements.length > 0) {
|
|
1822
1889
|
for (const msg of agreements) {
|
|
@@ -1825,7 +1892,53 @@ async function loginWithAppleId(config) {
|
|
|
1825
1892
|
}
|
|
1826
1893
|
session.pendingAgreements = agreements;
|
|
1827
1894
|
log(`Logged in as ${session.userEmail}`);
|
|
1828
|
-
|
|
1895
|
+
}
|
|
1896
|
+
async function loginWithAppleId(config) {
|
|
1897
|
+
const log = config.onLog ?? console.log;
|
|
1898
|
+
const promptFn = config.onPrompt ?? defaultPrompt;
|
|
1899
|
+
const result = await beginLogin({ email: config.email, password: config.password, onLog: log });
|
|
1900
|
+
if (result.kind === "logged_in") return result.session;
|
|
1901
|
+
const { pending, challenge } = result;
|
|
1902
|
+
const phones = challenge.trustedPhoneNumbers;
|
|
1903
|
+
if (challenge.kind === "device") {
|
|
1904
|
+
log("Two-factor Authentication (6 digit code) is enabled for this account");
|
|
1905
|
+
log("(Input 'sms' to escape this prompt and select a trusted phone number to send the code as a text message)");
|
|
1906
|
+
const input = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
|
|
1907
|
+
if (input.toLowerCase() === "sms") {
|
|
1908
|
+
log("Please select a trusted phone number:");
|
|
1909
|
+
for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
|
|
1910
|
+
const choice2 = await promptFn("Enter number: ");
|
|
1911
|
+
const phone2 = phones[Math.max(0, parseInt(choice2, 10) - 1)] ?? phones[0];
|
|
1912
|
+
await requestSmsCode({ pending, phoneId: phone2.id, mode: phone2.pushMode });
|
|
1913
|
+
log(`Code sent to ${phone2.numberWithDialCode}`);
|
|
1914
|
+
const code2 = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
|
|
1915
|
+
return submitTwoFactorCode({ pending, code: code2, source: "phone", phoneId: phone2.id, mode: phone2.pushMode ?? "sms", onLog: log });
|
|
1916
|
+
}
|
|
1917
|
+
return submitTwoFactorCode({ pending, code: input, source: "device", onLog: log });
|
|
1918
|
+
}
|
|
1919
|
+
log("Two-factor Authentication (6 digit code) is enabled for this account");
|
|
1920
|
+
if (phones.length === 1) {
|
|
1921
|
+
const phone2 = phones[0];
|
|
1922
|
+
log(`Code automatically sent to ${phone2.numberWithDialCode}`);
|
|
1923
|
+
const code2 = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
|
|
1924
|
+
return submitTwoFactorCode({ pending, code: code2, source: "phone", phoneId: phone2.id, mode: phone2.pushMode ?? "sms", onLog: log });
|
|
1925
|
+
}
|
|
1926
|
+
log("Please select a trusted phone number:");
|
|
1927
|
+
for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
|
|
1928
|
+
const choice = await promptFn("Enter number: ");
|
|
1929
|
+
const phone = phones[Math.max(0, parseInt(choice, 10) - 1)] ?? phones[0];
|
|
1930
|
+
await requestSmsCode({ pending, phoneId: phone.id, mode: phone.pushMode });
|
|
1931
|
+
log(`Code sent to ${phone.numberWithDialCode}`);
|
|
1932
|
+
const code = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
|
|
1933
|
+
return submitTwoFactorCode({ pending, code, source: "phone", phoneId: phone.id, mode: phone.pushMode ?? "sms", onLog: log });
|
|
1934
|
+
}
|
|
1935
|
+
function buildAuthHeadersFromPending(pending) {
|
|
1936
|
+
const h = { "X-Apple-Widget-Key": pending.serviceKey, Accept: "application/json" };
|
|
1937
|
+
if (pending.sessionId) h["X-Apple-Id-Session-Id"] = pending.sessionId;
|
|
1938
|
+
if (pending.scnt) h["scnt"] = pending.scnt;
|
|
1939
|
+
const c = Object.entries(pending.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1940
|
+
if (c) h["Cookie"] = c;
|
|
1941
|
+
return h;
|
|
1829
1942
|
}
|
|
1830
1943
|
async function doSirp(user, password, serviceKey, cookieStr, hashcash) {
|
|
1831
1944
|
const client = new SRPClient(2048);
|
|
@@ -1881,99 +1994,6 @@ async function doSirp(user, password, serviceKey, cookieStr, hashcash) {
|
|
|
1881
1994
|
scnt: completeRes.headers.get("scnt") ?? ""
|
|
1882
1995
|
};
|
|
1883
1996
|
}
|
|
1884
|
-
async function handleTwoStepOrFactor(session, log, promptFn) {
|
|
1885
|
-
const r = await fetch(`${AUTH_BASE}`, { headers: buildAuthHeaders(session) });
|
|
1886
|
-
const body = await r.json();
|
|
1887
|
-
if (body.trustedDevices?.length > 0) {
|
|
1888
|
-
await handleTwoStep(session, body, log, promptFn);
|
|
1889
|
-
} else if (body.trustedPhoneNumbers?.length > 0) {
|
|
1890
|
-
await handleTwoFactor(session, body, log, promptFn);
|
|
1891
|
-
} else {
|
|
1892
|
-
throw new Error("2FA required but no trusted devices or phone numbers found.");
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
async function handleTwoFactor(session, authBody, log, promptFn) {
|
|
1896
|
-
log("Two-factor Authentication (6 digit code) is enabled for this account");
|
|
1897
|
-
const codeLength = authBody.securityCode?.length ?? 6;
|
|
1898
|
-
const phones = authBody.trustedPhoneNumbers ?? [];
|
|
1899
|
-
const noTrusted = authBody.noTrustedDevices === true;
|
|
1900
|
-
let codeType;
|
|
1901
|
-
let reqBody;
|
|
1902
|
-
if (noTrusted && phones.length === 1) {
|
|
1903
|
-
const phone = phones[0];
|
|
1904
|
-
log(`Code automatically sent to ${phone.numberWithDialCode}`);
|
|
1905
|
-
const code = await promptFn(`Please enter the ${codeLength} digit code you received at ${phone.numberWithDialCode}: `);
|
|
1906
|
-
codeType = "phone";
|
|
1907
|
-
reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
|
|
1908
|
-
} else if (noTrusted) {
|
|
1909
|
-
const phone = await choosePhone(phones, log, promptFn);
|
|
1910
|
-
await requestPhoneCode(session, phone);
|
|
1911
|
-
log(`Successfully requested text message to ${phone.numberWithDialCode}`);
|
|
1912
|
-
const code = await promptFn(`Please enter the ${codeLength} digit code: `);
|
|
1913
|
-
codeType = "phone";
|
|
1914
|
-
reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
|
|
1915
|
-
} else {
|
|
1916
|
-
log("(Input 'sms' to escape this prompt and select a trusted phone number to send the code as a text message)");
|
|
1917
|
-
const input = await promptFn(`Please enter the ${codeLength} digit code: `);
|
|
1918
|
-
if (input.toLowerCase() === "sms") {
|
|
1919
|
-
const phone = await choosePhone(phones, log, promptFn);
|
|
1920
|
-
await requestPhoneCode(session, phone);
|
|
1921
|
-
const code = await promptFn(`Please enter the ${codeLength} digit code sent to ${phone.numberWithDialCode}: `);
|
|
1922
|
-
codeType = "phone";
|
|
1923
|
-
reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
|
|
1924
|
-
} else {
|
|
1925
|
-
codeType = "trusteddevice";
|
|
1926
|
-
reqBody = JSON.stringify({ securityCode: { code: input } });
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
log("Requesting session...");
|
|
1930
|
-
const verifyRes = await fetch(`${AUTH_BASE}/verify/${codeType}/securitycode`, {
|
|
1931
|
-
method: "POST",
|
|
1932
|
-
headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
|
|
1933
|
-
body: reqBody
|
|
1934
|
-
});
|
|
1935
|
-
if (!verifyRes.ok) {
|
|
1936
|
-
const err = await verifyRes.json().catch(() => ({}));
|
|
1937
|
-
const msgs = (err?.service_errors ?? err?.serviceErrors ?? []).map((e) => e.message).filter(Boolean);
|
|
1938
|
-
throw new Error(msgs.join("; ") || `Verification failed (${verifyRes.status})`);
|
|
1939
|
-
}
|
|
1940
|
-
Object.assign(session.cookies, parseCookies(verifyRes.headers));
|
|
1941
|
-
await trustSession(session);
|
|
1942
|
-
}
|
|
1943
|
-
async function handleTwoStep(session, authBody, log, promptFn) {
|
|
1944
|
-
log("Two-step Verification (4 digit code) is enabled for this account");
|
|
1945
|
-
const devices = authBody.trustedDevices ?? [];
|
|
1946
|
-
log("Please select a trusted device:");
|
|
1947
|
-
for (let i = 0; i < devices.length; i++) {
|
|
1948
|
-
log(` ${i + 1}. ${devices[i].name} ${devices[i].modelName ?? "SMS"} (${devices[i].id})`);
|
|
1949
|
-
}
|
|
1950
|
-
const choice = await promptFn("Enter device number: ");
|
|
1951
|
-
const device = devices[Math.max(0, parseInt(choice, 10) - 1)] ?? devices[0];
|
|
1952
|
-
await fetch(`${AUTH_BASE}/verify/device/${device.id}/securitycode`, { method: "PUT", headers: buildAuthHeaders(session) });
|
|
1953
|
-
log("Successfully requested notification");
|
|
1954
|
-
const code = await promptFn("Please enter the 4 digit code: ");
|
|
1955
|
-
const verifyRes = await fetch(`${AUTH_BASE}/verify/phone/securitycode`, {
|
|
1956
|
-
method: "POST",
|
|
1957
|
-
headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
|
|
1958
|
-
body: JSON.stringify({ phoneNumber: { id: device.id }, securityCode: { code }, mode: "sms" })
|
|
1959
|
-
});
|
|
1960
|
-
if (!verifyRes.ok) throw new Error("Incorrect verification code.");
|
|
1961
|
-
Object.assign(session.cookies, parseCookies(verifyRes.headers));
|
|
1962
|
-
await trustSession(session);
|
|
1963
|
-
}
|
|
1964
|
-
async function choosePhone(phones, log, promptFn) {
|
|
1965
|
-
log("Please select a trusted phone number:");
|
|
1966
|
-
for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
|
|
1967
|
-
const choice = await promptFn("Enter number: ");
|
|
1968
|
-
return phones[Math.max(0, parseInt(choice, 10) - 1)] ?? phones[0];
|
|
1969
|
-
}
|
|
1970
|
-
async function requestPhoneCode(session, phone) {
|
|
1971
|
-
await fetch(`${AUTH_BASE}/verify/phone`, {
|
|
1972
|
-
method: "PUT",
|
|
1973
|
-
headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
|
|
1974
|
-
body: JSON.stringify({ phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" })
|
|
1975
|
-
});
|
|
1976
|
-
}
|
|
1977
1997
|
async function trustSession(session) {
|
|
1978
1998
|
const r = await fetch(`${AUTH_BASE}/2sv/trust`, { headers: buildAuthHeaders(session) });
|
|
1979
1999
|
Object.assign(session.cookies, parseCookies(r.headers));
|
|
@@ -2265,23 +2285,13 @@ var init_request = __esm({
|
|
|
2265
2285
|
};
|
|
2266
2286
|
}
|
|
2267
2287
|
});
|
|
2268
|
-
|
|
2269
|
-
// src/pilot/index.ts
|
|
2270
|
-
var pilot_exports = {};
|
|
2271
|
-
__export(pilot_exports, {
|
|
2272
|
-
distributeToGroups: () => distributeToGroups,
|
|
2273
|
-
expireBuild: () => expireBuild,
|
|
2274
|
-
setChangelog: () => setChangelog,
|
|
2275
|
-
uploadToTestFlight: () => uploadToTestFlight,
|
|
2276
|
-
waitForProcessing: () => waitForProcessing
|
|
2277
|
-
});
|
|
2278
|
-
async function uploadToTestFlight(options) {
|
|
2288
|
+
async function uploadWithAltool(options) {
|
|
2279
2289
|
if (!fs.existsSync(options.ipaPath)) {
|
|
2280
2290
|
throw new Error(`File not found: ${options.ipaPath}`);
|
|
2281
2291
|
}
|
|
2282
2292
|
const platform = options.platform ?? "ios";
|
|
2283
2293
|
const timeout = options.timeout ?? 18e5;
|
|
2284
|
-
const keyDir = await promises.mkdtemp(path.join(os.tmpdir(), "fastnode-
|
|
2294
|
+
const keyDir = await promises.mkdtemp(path.join(os.tmpdir(), "fastnode-altool-"));
|
|
2285
2295
|
const keyPath = path.join(keyDir, `AuthKey_${options.apiKeyId}.p8`);
|
|
2286
2296
|
await promises.writeFile(keyPath, options.apiPrivateKey);
|
|
2287
2297
|
return new Promise((resolve3) => {
|
|
@@ -2305,24 +2315,228 @@ async function uploadToTestFlight(options) {
|
|
|
2305
2315
|
const timer = setTimeout(() => {
|
|
2306
2316
|
child.kill("SIGTERM");
|
|
2307
2317
|
}, timeout);
|
|
2308
|
-
|
|
2318
|
+
const onData = (data) => {
|
|
2309
2319
|
const chunk = data.toString();
|
|
2310
2320
|
output += chunk;
|
|
2311
2321
|
options.onOutput?.(chunk);
|
|
2322
|
+
};
|
|
2323
|
+
child.stdout.on("data", onData);
|
|
2324
|
+
child.stderr.on("data", onData);
|
|
2325
|
+
child.on("close", async (code) => {
|
|
2326
|
+
clearTimeout(timer);
|
|
2327
|
+
await promises.unlink(keyPath).catch(() => {
|
|
2328
|
+
});
|
|
2329
|
+
await promises.rm(keyDir, { recursive: true, force: true }).catch(() => {
|
|
2330
|
+
});
|
|
2331
|
+
resolve3({ success: code === 0, output, backend: "altool" });
|
|
2312
2332
|
});
|
|
2313
|
-
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
var init_upload_altool = __esm({
|
|
2336
|
+
"src/pilot/upload-altool.ts"() {
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
2339
|
+
async function resolveTransporter() {
|
|
2340
|
+
const override = process.env.ITMSTRANSPORTER_BIN;
|
|
2341
|
+
if (override) {
|
|
2342
|
+
if (!fs.existsSync(override)) {
|
|
2343
|
+
throw new Error(
|
|
2344
|
+
`ITMSTRANSPORTER_BIN is set to "${override}" but that path does not exist.`
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
return override;
|
|
2348
|
+
}
|
|
2349
|
+
const candidates = process.platform === "darwin" ? [
|
|
2350
|
+
// Xcode 15+: Transporter.app bundled with Xcode
|
|
2351
|
+
"/Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/itms/bin/iTMSTransporter",
|
|
2352
|
+
// Standalone Transporter.app from the Mac App Store
|
|
2353
|
+
"/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter"
|
|
2354
|
+
] : [
|
|
2355
|
+
"/usr/local/itms/bin/iTMSTransporter",
|
|
2356
|
+
"/opt/itms/bin/iTMSTransporter",
|
|
2357
|
+
"/usr/local/bin/iTMSTransporter"
|
|
2358
|
+
];
|
|
2359
|
+
for (const path of candidates) {
|
|
2360
|
+
if (fs.existsSync(path)) return path;
|
|
2361
|
+
}
|
|
2362
|
+
throw new Error(
|
|
2363
|
+
[
|
|
2364
|
+
"iTMSTransporter not found.",
|
|
2365
|
+
"",
|
|
2366
|
+
"Install options:",
|
|
2367
|
+
process.platform === "darwin" ? " - Install Xcode (includes Transporter)" : " - Download Apple's standalone iTMSTransporter package for Linux",
|
|
2368
|
+
" - Or set ITMSTRANSPORTER_BIN to the absolute path of the binary",
|
|
2369
|
+
"",
|
|
2370
|
+
`Searched: ${candidates.join(", ")}`
|
|
2371
|
+
].join("\n")
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
async function verifyJava() {
|
|
2375
|
+
return new Promise((resolve3, reject) => {
|
|
2376
|
+
const child = child_process.spawn("java", ["-version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
2377
|
+
let stderr = "";
|
|
2378
|
+
child.stderr.on("data", (d) => {
|
|
2379
|
+
stderr += d.toString();
|
|
2380
|
+
});
|
|
2381
|
+
child.on("error", (err) => {
|
|
2382
|
+
reject(
|
|
2383
|
+
new Error(
|
|
2384
|
+
`java not found on PATH (${err.message}). Install Java 17 LTS and ensure it is on PATH.`
|
|
2385
|
+
)
|
|
2386
|
+
);
|
|
2387
|
+
});
|
|
2388
|
+
child.on("close", (code) => {
|
|
2389
|
+
if (code !== 0) {
|
|
2390
|
+
reject(new Error(`java -version exited ${code}: ${stderr}`));
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
const match = stderr.match(/version "([^"]+)"/);
|
|
2394
|
+
resolve3({ version: match?.[1] ?? "unknown" });
|
|
2395
|
+
});
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
var init_transporter_install = __esm({
|
|
2399
|
+
"src/pilot/transporter-install.ts"() {
|
|
2400
|
+
}
|
|
2401
|
+
});
|
|
2402
|
+
async function uploadWithTransporter(options) {
|
|
2403
|
+
if (!fs.existsSync(options.ipaPath)) {
|
|
2404
|
+
throw new Error(`File not found: ${options.ipaPath}`);
|
|
2405
|
+
}
|
|
2406
|
+
await verifyJava();
|
|
2407
|
+
const transporterBin = await resolveTransporter();
|
|
2408
|
+
const platform = options.platform ?? "ios";
|
|
2409
|
+
const timeout = options.timeout ?? 18e5;
|
|
2410
|
+
const keyDir = await promises.mkdtemp(path.join(os.tmpdir(), "fastnode-itms-"));
|
|
2411
|
+
const keyPath = path.join(keyDir, `AuthKey_${options.apiKeyId}.p8`);
|
|
2412
|
+
await promises.writeFile(keyPath, options.apiPrivateKey);
|
|
2413
|
+
return new Promise((resolve3) => {
|
|
2414
|
+
const args = [
|
|
2415
|
+
"-m",
|
|
2416
|
+
"upload",
|
|
2417
|
+
"-assetFile",
|
|
2418
|
+
options.ipaPath,
|
|
2419
|
+
"-apiKey",
|
|
2420
|
+
options.apiKeyId,
|
|
2421
|
+
"-apiIssuer",
|
|
2422
|
+
options.apiIssuerId,
|
|
2423
|
+
// WONoPause prevents the JVM from waiting on stdin in non-TTY
|
|
2424
|
+
// environments (CI, LaunchAgent) where it would hang forever.
|
|
2425
|
+
"-WONoPause",
|
|
2426
|
+
"true"
|
|
2427
|
+
];
|
|
2428
|
+
if (platform !== "ios") {
|
|
2429
|
+
args.push("-bundleType", platform);
|
|
2430
|
+
}
|
|
2431
|
+
const child = child_process.spawn(transporterBin, args, {
|
|
2432
|
+
env: {
|
|
2433
|
+
...process.env,
|
|
2434
|
+
API_PRIVATE_KEYS_DIR: keyDir
|
|
2435
|
+
},
|
|
2436
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2437
|
+
});
|
|
2438
|
+
let output = "";
|
|
2439
|
+
const timer = setTimeout(() => {
|
|
2440
|
+
child.kill("SIGTERM");
|
|
2441
|
+
}, timeout);
|
|
2442
|
+
const onData = (data) => {
|
|
2314
2443
|
const chunk = data.toString();
|
|
2315
2444
|
output += chunk;
|
|
2316
2445
|
options.onOutput?.(chunk);
|
|
2317
|
-
}
|
|
2446
|
+
};
|
|
2447
|
+
child.stdout.on("data", onData);
|
|
2448
|
+
child.stderr.on("data", onData);
|
|
2318
2449
|
child.on("close", async (code) => {
|
|
2319
2450
|
clearTimeout(timer);
|
|
2320
|
-
await promises.
|
|
2451
|
+
await promises.rm(keyDir, { recursive: true, force: true }).catch(() => {
|
|
2452
|
+
});
|
|
2453
|
+
resolve3({ success: code === 0, output, backend: "transporter" });
|
|
2454
|
+
});
|
|
2455
|
+
child.on("error", async (err) => {
|
|
2456
|
+
clearTimeout(timer);
|
|
2457
|
+
await promises.rm(keyDir, { recursive: true, force: true }).catch(() => {
|
|
2458
|
+
});
|
|
2459
|
+
resolve3({
|
|
2460
|
+
success: false,
|
|
2461
|
+
output: output + `
|
|
2462
|
+
Process error: ${err.message}`,
|
|
2463
|
+
backend: "transporter"
|
|
2321
2464
|
});
|
|
2322
|
-
resolve3({ success: code === 0, output });
|
|
2323
2465
|
});
|
|
2324
2466
|
});
|
|
2325
2467
|
}
|
|
2468
|
+
var init_upload_transporter = __esm({
|
|
2469
|
+
"src/pilot/upload-transporter.ts"() {
|
|
2470
|
+
init_transporter_install();
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
|
|
2474
|
+
// src/pilot/classify.ts
|
|
2475
|
+
function classifyUploadError(output) {
|
|
2476
|
+
for (const { pattern, kind } of RULES) {
|
|
2477
|
+
if (pattern.test(output)) return kind;
|
|
2478
|
+
}
|
|
2479
|
+
return null;
|
|
2480
|
+
}
|
|
2481
|
+
function isTransientUploadError(output) {
|
|
2482
|
+
return classifyUploadError(output) !== null;
|
|
2483
|
+
}
|
|
2484
|
+
var RULES;
|
|
2485
|
+
var init_classify = __esm({
|
|
2486
|
+
"src/pilot/classify.ts"() {
|
|
2487
|
+
RULES = [
|
|
2488
|
+
// Apple's finalize-upload call returning 5xx — the most common transient
|
|
2489
|
+
// failure we see in practice. Example:
|
|
2490
|
+
// "CHANGE UPLOAD STATE TO COMPLETE (UPLOADING ASSET DESCRIPTION):
|
|
2491
|
+
// received status code 500; internal server error."
|
|
2492
|
+
{ pattern: /CHANGE UPLOAD STATE TO COMPLETE/i, kind: "finalize_failed" },
|
|
2493
|
+
// Generic 5xx from ASC backend services
|
|
2494
|
+
{ pattern: /status code 5\d\d/i, kind: "apple_5xx" },
|
|
2495
|
+
{ pattern: /internal server error/i, kind: "apple_5xx" },
|
|
2496
|
+
{ pattern: /Service Unavailable/i, kind: "apple_5xx" },
|
|
2497
|
+
{ pattern: /Bad Gateway/i, kind: "apple_5xx" },
|
|
2498
|
+
// Network-level interruptions during the upload body transfer
|
|
2499
|
+
{ pattern: /Connection reset/i, kind: "connection_reset" },
|
|
2500
|
+
{ pattern: /SocketException/i, kind: "connection_reset" },
|
|
2501
|
+
{ pattern: /Broken pipe/i, kind: "connection_reset" },
|
|
2502
|
+
// Timeouts — distinct from resets because the other side may have
|
|
2503
|
+
// actually processed the request, so dedupe logic may matter.
|
|
2504
|
+
{ pattern: /Read timed out/i, kind: "network_timeout" },
|
|
2505
|
+
{ pattern: /SocketTimeoutException/i, kind: "network_timeout" },
|
|
2506
|
+
{ pattern: /Operation timed out/i, kind: "network_timeout" },
|
|
2507
|
+
// JVM-internal errors from iTMSTransporter (OOM, thread interruption, etc.)
|
|
2508
|
+
{ pattern: /java\.lang\.OutOfMemoryError/i, kind: "transporter_jvm_error" },
|
|
2509
|
+
{ pattern: /EOFException/i, kind: "transporter_jvm_error" }
|
|
2510
|
+
];
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
|
|
2514
|
+
// src/pilot/index.ts
|
|
2515
|
+
var pilot_exports = {};
|
|
2516
|
+
__export(pilot_exports, {
|
|
2517
|
+
classifyUploadError: () => classifyUploadError,
|
|
2518
|
+
distributeToGroups: () => distributeToGroups,
|
|
2519
|
+
expireBuild: () => expireBuild,
|
|
2520
|
+
isTransientUploadError: () => isTransientUploadError,
|
|
2521
|
+
resolveTransporter: () => resolveTransporter,
|
|
2522
|
+
setChangelog: () => setChangelog,
|
|
2523
|
+
uploadToTestFlight: () => uploadToTestFlight,
|
|
2524
|
+
uploadWithAltool: () => uploadWithAltool,
|
|
2525
|
+
uploadWithTransporter: () => uploadWithTransporter,
|
|
2526
|
+
verifyJava: () => verifyJava,
|
|
2527
|
+
waitForProcessing: () => waitForProcessing
|
|
2528
|
+
});
|
|
2529
|
+
async function uploadToTestFlight(options) {
|
|
2530
|
+
const backend = selectBackend(options.backend ?? "auto");
|
|
2531
|
+
if (backend === "altool") {
|
|
2532
|
+
return uploadWithAltool(options);
|
|
2533
|
+
}
|
|
2534
|
+
return uploadWithTransporter(options);
|
|
2535
|
+
}
|
|
2536
|
+
function selectBackend(pref) {
|
|
2537
|
+
if (pref !== "auto") return pref;
|
|
2538
|
+
return process.platform === "darwin" ? "altool" : "transporter";
|
|
2539
|
+
}
|
|
2326
2540
|
async function waitForProcessing(client, params) {
|
|
2327
2541
|
const timeout = params.timeout ?? 18e5;
|
|
2328
2542
|
const pollInterval = params.pollInterval ?? 3e4;
|
|
@@ -2443,6 +2657,12 @@ function sleep2(ms) {
|
|
|
2443
2657
|
var init_pilot = __esm({
|
|
2444
2658
|
"src/pilot/index.ts"() {
|
|
2445
2659
|
init_request();
|
|
2660
|
+
init_upload_altool();
|
|
2661
|
+
init_upload_transporter();
|
|
2662
|
+
init_upload_altool();
|
|
2663
|
+
init_upload_transporter();
|
|
2664
|
+
init_classify();
|
|
2665
|
+
init_transporter_install();
|
|
2446
2666
|
}
|
|
2447
2667
|
});
|
|
2448
2668
|
async function loadMetadata(metadataPath) {
|
|
@@ -2530,9 +2750,9 @@ var init_metadata = __esm({
|
|
|
2530
2750
|
// src/xcode/upload.ts
|
|
2531
2751
|
var upload_exports = {};
|
|
2532
2752
|
__export(upload_exports, {
|
|
2533
|
-
uploadWithAltool: () =>
|
|
2753
|
+
uploadWithAltool: () => uploadWithAltool2
|
|
2534
2754
|
});
|
|
2535
|
-
async function
|
|
2755
|
+
async function uploadWithAltool2(options) {
|
|
2536
2756
|
requireMacOS("altool upload");
|
|
2537
2757
|
let keyDir;
|
|
2538
2758
|
if (options.apiPrivateKey && !options.apiKeyPath) {
|
|
@@ -2641,8 +2861,8 @@ async function uploadToAppStore(options) {
|
|
|
2641
2861
|
if (!options.skipBinaryUpload) {
|
|
2642
2862
|
const binaryPath = options.ipaPath ?? options.pkgPath;
|
|
2643
2863
|
if (binaryPath && options.apiKeyId && options.apiIssuerId) {
|
|
2644
|
-
const { uploadWithAltool:
|
|
2645
|
-
await
|
|
2864
|
+
const { uploadWithAltool: uploadWithAltool3 } = await Promise.resolve().then(() => (init_upload(), upload_exports));
|
|
2865
|
+
await uploadWithAltool3({
|
|
2646
2866
|
filePath: binaryPath,
|
|
2647
2867
|
apiKeyId: options.apiKeyId,
|
|
2648
2868
|
apiIssuerId: options.apiIssuerId,
|
|
@@ -3428,8 +3648,8 @@ async function captureScreenshots(options) {
|
|
|
3428
3648
|
const outputDir = options.outputDirectory ?? path.join(process.cwd(), "screenshots");
|
|
3429
3649
|
const retries = options.numberOfRetries ?? 1;
|
|
3430
3650
|
if (options.clearPreviousScreenshots && fs.existsSync(outputDir)) {
|
|
3431
|
-
const { rm:
|
|
3432
|
-
await
|
|
3651
|
+
const { rm: rm4 } = await import('fs/promises');
|
|
3652
|
+
await rm4(outputDir, { recursive: true, force: true });
|
|
3433
3653
|
}
|
|
3434
3654
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
3435
3655
|
const screenshots = {};
|
|
@@ -6136,6 +6356,264 @@ var init_portal = __esm({
|
|
|
6136
6356
|
async init() {
|
|
6137
6357
|
await this.post("account/listTeams.action", {});
|
|
6138
6358
|
}
|
|
6359
|
+
/**
|
|
6360
|
+
* JSON POST — different content type than the standard form-encoded
|
|
6361
|
+
* `post()`. Used by the App Store Connect API key endpoints
|
|
6362
|
+
* (`account/auth/key/v2/create`), which Spaceship discovered take a
|
|
6363
|
+
* JSON body even though every other portal mutation is form-encoded.
|
|
6364
|
+
*
|
|
6365
|
+
* Like the form-encoded `post()`, this also runs the response through
|
|
6366
|
+
* `checkPortalResponse` — Apple returns HTTP 200 even for errors and
|
|
6367
|
+
* encodes the error in `resultCode`/`userString` fields, so a raw
|
|
6368
|
+
* status check would silently miss real failures.
|
|
6369
|
+
*/
|
|
6370
|
+
async postJson(path, body) {
|
|
6371
|
+
const headers = {
|
|
6372
|
+
Cookie: this.cookieStr,
|
|
6373
|
+
"Content-Type": "application/json",
|
|
6374
|
+
Accept: "application/json"
|
|
6375
|
+
};
|
|
6376
|
+
if (this.csrf) {
|
|
6377
|
+
headers["csrf"] = this.csrf;
|
|
6378
|
+
headers["csrf_ts"] = this.csrfTs;
|
|
6379
|
+
}
|
|
6380
|
+
console.log(`[portal] POST(json) ${path}`);
|
|
6381
|
+
const res = await fetch(`${PORTAL_BASE}/${path}`, {
|
|
6382
|
+
method: "POST",
|
|
6383
|
+
headers,
|
|
6384
|
+
body: JSON.stringify(body)
|
|
6385
|
+
});
|
|
6386
|
+
if (res.headers.get("csrf")) {
|
|
6387
|
+
this.csrf = res.headers.get("csrf");
|
|
6388
|
+
this.csrfTs = res.headers.get("csrf_ts") ?? "";
|
|
6389
|
+
}
|
|
6390
|
+
for (const raw of res.headers.getSetCookie?.() ?? []) {
|
|
6391
|
+
const m = raw.match(/^([^=]+)=([^;]*)/);
|
|
6392
|
+
if (m && !this.cookieStr.includes(m[1] + "=")) this.cookieStr += "; " + m[1] + "=" + m[2];
|
|
6393
|
+
}
|
|
6394
|
+
const text = await res.text();
|
|
6395
|
+
let data = null;
|
|
6396
|
+
try {
|
|
6397
|
+
data = text ? JSON.parse(text) : null;
|
|
6398
|
+
} catch {
|
|
6399
|
+
}
|
|
6400
|
+
console.log(`[portal] POST(json) ${path} \u2192 ${res.status} ${text.slice(0, 400)}`);
|
|
6401
|
+
checkPortalResponse(data, this.session);
|
|
6402
|
+
return { status: res.status, body: data, rawText: text };
|
|
6403
|
+
}
|
|
6404
|
+
/**
|
|
6405
|
+
* GET helper for the portal endpoints that take query parameters
|
|
6406
|
+
* (currently just `account/auth/key/download`). Returns the raw text
|
|
6407
|
+
* since download endpoints can return PEMs or JSON depending on Apple's
|
|
6408
|
+
* mood — caller decides how to parse.
|
|
6409
|
+
*/
|
|
6410
|
+
async getRaw(path, params = {}) {
|
|
6411
|
+
const url = new URL(`${PORTAL_BASE}/${path}`);
|
|
6412
|
+
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
6413
|
+
const headers = {
|
|
6414
|
+
Cookie: this.cookieStr,
|
|
6415
|
+
Accept: "application/json, text/plain, application/x-pem-file, */*"
|
|
6416
|
+
};
|
|
6417
|
+
if (this.csrf) {
|
|
6418
|
+
headers["csrf"] = this.csrf;
|
|
6419
|
+
headers["csrf_ts"] = this.csrfTs;
|
|
6420
|
+
}
|
|
6421
|
+
console.log(`[portal] GET ${url.pathname}${url.search}`);
|
|
6422
|
+
const res = await fetch(url.toString(), { headers });
|
|
6423
|
+
for (const raw of res.headers.getSetCookie?.() ?? []) {
|
|
6424
|
+
const m = raw.match(/^([^=]+)=([^;]*)/);
|
|
6425
|
+
if (m && !this.cookieStr.includes(m[1] + "=")) this.cookieStr += "; " + m[1] + "=" + m[2];
|
|
6426
|
+
}
|
|
6427
|
+
const text = await res.text();
|
|
6428
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
6429
|
+
console.log(`[portal] GET ${url.pathname} \u2192 ${res.status} ${contentType} ${text.length}b`);
|
|
6430
|
+
return { status: res.status, rawText: text, contentType };
|
|
6431
|
+
}
|
|
6432
|
+
// ── App Store Connect API Keys (.p8) ────────────────────────────────────
|
|
6433
|
+
/**
|
|
6434
|
+
* Create an App Store Connect API key via the developer portal's
|
|
6435
|
+
* `account/auth/key/v2/create` endpoint. Reverse-engineered from the
|
|
6436
|
+
* _current_ @expo/apple-utils npm package (v2.1.21), which is what
|
|
6437
|
+
* EAS CLI uses to manage Apple credentials in production.
|
|
6438
|
+
*
|
|
6439
|
+
* This differs from older published reverse-engineering guides:
|
|
6440
|
+
* • URL uses the `/v2/create` suffix (Apple kept it)
|
|
6441
|
+
* • Field is `serviceConfigurations`, shape is an object keyed by
|
|
6442
|
+
* service id (NOT `serviceConfigurationsRequests: [...]` array)
|
|
6443
|
+
* • Body REQUIRES `scope: "team"`
|
|
6444
|
+
* • Needs a scope-specific CSRF warmup via the `keys` endpoint
|
|
6445
|
+
* before the create call (EAS's `ensureCSRFAsync("keys", …)`)
|
|
6446
|
+
*
|
|
6447
|
+
* For an App Store Connect API key (PUBLIC_API, used by altool for
|
|
6448
|
+
* TestFlight uploads), `serviceConfigurations` is an empty object `{}`.
|
|
6449
|
+
* For an APNs auth key, it's `{ "U27F4V844T": [] }`. For MusicKit:
|
|
6450
|
+
* `{ "6A7HVUVQ3M": [musicId] }`, etc.
|
|
6451
|
+
*
|
|
6452
|
+
* Apple's name validator rejects parentheses, brackets, and most
|
|
6453
|
+
* punctuation — sanitize to alphanumeric + space + dash + underscore
|
|
6454
|
+
* + period and collapse whitespace.
|
|
6455
|
+
*/
|
|
6456
|
+
async createApiKey(rawName, serviceConfigurations = {}) {
|
|
6457
|
+
const name = rawName.replace(/[^A-Za-z0-9 _\-.]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
|
|
6458
|
+
if (!name) throw new Error("createApiKey: name is empty after sanitization");
|
|
6459
|
+
try {
|
|
6460
|
+
await this.listApiKeys();
|
|
6461
|
+
} catch {
|
|
6462
|
+
}
|
|
6463
|
+
const body = {
|
|
6464
|
+
teamId: this.teamId,
|
|
6465
|
+
name,
|
|
6466
|
+
serviceConfigurations,
|
|
6467
|
+
scope: "team"
|
|
6468
|
+
};
|
|
6469
|
+
const result = await this.postJson("account/auth/key/v2/create", body);
|
|
6470
|
+
const created = result.body?.keys?.[0] ?? result.body?.key ?? result.body?.data ?? result.body;
|
|
6471
|
+
const keyId = created?.keyId ?? created?.id ?? created?.kid ?? created?.attributes?.kid;
|
|
6472
|
+
if (!keyId) {
|
|
6473
|
+
throw new Error(`createApiKey: no key id in response. Body: ${result.rawText.slice(0, 400)}`);
|
|
6474
|
+
}
|
|
6475
|
+
return { keyId, raw: created };
|
|
6476
|
+
}
|
|
6477
|
+
/**
|
|
6478
|
+
* Fetch a single API key by id via `account/auth/key/get`. Reverse-
|
|
6479
|
+
* engineered from Spaceship's `get_key`. Returns the raw key record so
|
|
6480
|
+
* the caller can extract whatever fields it needs (e.g. issuerId).
|
|
6481
|
+
*
|
|
6482
|
+
* Form-encoded POST — same shape as every other PortalClient mutation
|
|
6483
|
+
* except the JSON `create` endpoint.
|
|
6484
|
+
*/
|
|
6485
|
+
async getApiKey(keyId) {
|
|
6486
|
+
const data = await this.post("account/auth/key/get", {
|
|
6487
|
+
teamId: this.teamId,
|
|
6488
|
+
keyId
|
|
6489
|
+
});
|
|
6490
|
+
return data?.keys?.[0] ?? data?.key ?? data?.data ?? data;
|
|
6491
|
+
}
|
|
6492
|
+
/**
|
|
6493
|
+
* Revoke an existing key. Form-encoded POST per Spaceship's
|
|
6494
|
+
* `revoke_key!`. Used for cleaning up orphan keys created during
|
|
6495
|
+
* failed wizard runs.
|
|
6496
|
+
*/
|
|
6497
|
+
async revokeApiKey(keyId) {
|
|
6498
|
+
await this.post("account/auth/key/revoke", {
|
|
6499
|
+
teamId: this.teamId,
|
|
6500
|
+
keyId
|
|
6501
|
+
});
|
|
6502
|
+
}
|
|
6503
|
+
// ── APNs Authentication Keys (.p8) ──────────────────────────────────────
|
|
6504
|
+
/** Apple's hardcoded service id for APNs auth keys (from Spaceship). */
|
|
6505
|
+
static APNS_SERVICE_ID = "U27F4V844T";
|
|
6506
|
+
/**
|
|
6507
|
+
* Create an APNs Authentication Key (.p8) via the developer portal.
|
|
6508
|
+
*
|
|
6509
|
+
* Apple requires BOTH `serviceConfigurations` (the map) AND
|
|
6510
|
+
* `serviceConfigurationsRequests` (the array of full config objects)
|
|
6511
|
+
* in the v2/create body. Just sending the map alone results in
|
|
6512
|
+
* "No value was provided for the parameter 'serviceConfigurations'".
|
|
6513
|
+
*
|
|
6514
|
+
* The exact body comes from @expo/apple-utils v2.1.21 — both fields
|
|
6515
|
+
* are produced by separate functions (`d` for the map, `h` for the
|
|
6516
|
+
* array) and merged into the create payload.
|
|
6517
|
+
*/
|
|
6518
|
+
async createApnsAuthKey(rawName) {
|
|
6519
|
+
const name = rawName.replace(/[^A-Za-z0-9 _\-.]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
|
|
6520
|
+
if (!name) throw new Error("createApnsAuthKey: name is empty after sanitization");
|
|
6521
|
+
try {
|
|
6522
|
+
await this.listApiKeys();
|
|
6523
|
+
} catch {
|
|
6524
|
+
}
|
|
6525
|
+
const body = {
|
|
6526
|
+
teamId: this.teamId,
|
|
6527
|
+
name,
|
|
6528
|
+
serviceConfigurations: {
|
|
6529
|
+
[_PortalClient.APNS_SERVICE_ID]: []
|
|
6530
|
+
},
|
|
6531
|
+
serviceConfigurationsRequests: [
|
|
6532
|
+
{
|
|
6533
|
+
isNew: true,
|
|
6534
|
+
serviceId: _PortalClient.APNS_SERVICE_ID,
|
|
6535
|
+
identifiers: {},
|
|
6536
|
+
environment: "all",
|
|
6537
|
+
scope: "team"
|
|
6538
|
+
}
|
|
6539
|
+
],
|
|
6540
|
+
scope: "team"
|
|
6541
|
+
};
|
|
6542
|
+
const result = await this.postJson("account/auth/key/v2/create", body);
|
|
6543
|
+
const created = result.body?.keys?.[0];
|
|
6544
|
+
if (!created?.keyId) {
|
|
6545
|
+
throw new Error(`createApnsAuthKey: no key id in response. Body: ${result.rawText.slice(0, 400)}`);
|
|
6546
|
+
}
|
|
6547
|
+
const pem = await this.downloadApiKey(created.keyId);
|
|
6548
|
+
return { keyId: created.keyId, pem, raw: created };
|
|
6549
|
+
}
|
|
6550
|
+
/**
|
|
6551
|
+
* Find existing APNs auth keys in the team. Used by the wizard to
|
|
6552
|
+
* detect "you already have one" cases and offer to reuse rather than
|
|
6553
|
+
* proliferate keys.
|
|
6554
|
+
*/
|
|
6555
|
+
async listApnsAuthKeys() {
|
|
6556
|
+
const list = await this.listApiKeys();
|
|
6557
|
+
return (list.keys ?? []).filter(
|
|
6558
|
+
(k) => (k.services ?? []).some((s) => s.id === _PortalClient.APNS_SERVICE_ID || /apns/i.test(s.name ?? ""))
|
|
6559
|
+
).map((k) => ({
|
|
6560
|
+
keyId: k.keyId,
|
|
6561
|
+
keyName: k.keyName,
|
|
6562
|
+
canDownload: Boolean(k.canDownload)
|
|
6563
|
+
}));
|
|
6564
|
+
}
|
|
6565
|
+
/**
|
|
6566
|
+
* Download an existing App Store Connect API key as PEM. Spaceship's
|
|
6567
|
+
* `download_key` endpoint — GET with teamId + keyId in query string.
|
|
6568
|
+
* Returns the raw .p8 PEM text. Apple shows the .p8 exactly once
|
|
6569
|
+
* (after creation), so call this immediately after createApiKey.
|
|
6570
|
+
*/
|
|
6571
|
+
async downloadApiKey(keyId) {
|
|
6572
|
+
const { status, rawText, contentType } = await this.getRaw("account/auth/key/download", {
|
|
6573
|
+
teamId: this.teamId,
|
|
6574
|
+
keyId
|
|
6575
|
+
});
|
|
6576
|
+
if (status !== 200) {
|
|
6577
|
+
throw new Error(`downloadApiKey failed (${status}): ${rawText.slice(0, 200)}`);
|
|
6578
|
+
}
|
|
6579
|
+
if (rawText.includes("BEGIN PRIVATE KEY")) return rawText;
|
|
6580
|
+
if (contentType.includes("json")) {
|
|
6581
|
+
try {
|
|
6582
|
+
const parsed = JSON.parse(rawText);
|
|
6583
|
+
const candidates = [
|
|
6584
|
+
parsed?.privateKey,
|
|
6585
|
+
parsed?.key?.privateKey,
|
|
6586
|
+
parsed?.keys?.[0]?.privateKey,
|
|
6587
|
+
parsed?.data?.attributes?.privateKey,
|
|
6588
|
+
parsed?.value
|
|
6589
|
+
];
|
|
6590
|
+
for (const c of candidates) {
|
|
6591
|
+
if (typeof c === "string" && c.includes("BEGIN PRIVATE KEY")) return c;
|
|
6592
|
+
}
|
|
6593
|
+
} catch {
|
|
6594
|
+
}
|
|
6595
|
+
}
|
|
6596
|
+
throw new Error(`downloadApiKey returned no PEM. Content-Type=${contentType} body=${rawText.slice(0, 300)}`);
|
|
6597
|
+
}
|
|
6598
|
+
/**
|
|
6599
|
+
* List existing API keys via the portal. Used to discover the team's
|
|
6600
|
+
* issuer id — every key in the team shares the same issuer id, and
|
|
6601
|
+
* altool needs it alongside the key id and .p8 file.
|
|
6602
|
+
*
|
|
6603
|
+
* Form-encoded POST (NOT JSON), with pagination params, per Spaceship's
|
|
6604
|
+
* `list_keys` method. JSON returned 415 from this endpoint.
|
|
6605
|
+
*/
|
|
6606
|
+
async listApiKeys() {
|
|
6607
|
+
const data = await this.post("account/auth/key/list", {
|
|
6608
|
+
teamId: this.teamId,
|
|
6609
|
+
pageNumber: "1",
|
|
6610
|
+
pageSize: "500",
|
|
6611
|
+
sort: "name=asc"
|
|
6612
|
+
});
|
|
6613
|
+
const keys = data?.keys ?? [];
|
|
6614
|
+
const issuerId = data?.issuerId ?? data?.issuer ?? data?.team?.issuerId ?? keys[0]?.issuerId ?? keys[0]?.issuer;
|
|
6615
|
+
return { keys, issuerId };
|
|
6616
|
+
}
|
|
6139
6617
|
async post(path, params) {
|
|
6140
6618
|
const body = new URLSearchParams();
|
|
6141
6619
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -6203,6 +6681,31 @@ var init_portal = __esm({
|
|
|
6203
6681
|
async deleteAppId(appIdId) {
|
|
6204
6682
|
await this.post("account/ios/identifiers/deleteAppId.action", { teamId: this.teamId, appIdId });
|
|
6205
6683
|
}
|
|
6684
|
+
/**
|
|
6685
|
+
* Toggle a capability (a.k.a. "service") on a bundle ID. Mirrors
|
|
6686
|
+
* Spaceship::Portal's `update_service` — the legacy portal endpoint
|
|
6687
|
+
* still works and is the only session-cookie-authenticated way to flip
|
|
6688
|
+
* capabilities like Push Notifications and Sign in with Apple from
|
|
6689
|
+
* outside the Apple Developer web UI.
|
|
6690
|
+
*
|
|
6691
|
+
* Common service codes (from Spaceship portal/portal_client.rb):
|
|
6692
|
+
* push → push notifications
|
|
6693
|
+
* APG3427HIY → Sign in with Apple
|
|
6694
|
+
* IAD53UNK2F → in-app purchase
|
|
6695
|
+
* homeKit → HomeKit
|
|
6696
|
+
* healthKit → HealthKit
|
|
6697
|
+
*
|
|
6698
|
+
* @param featureType Apple's internal capability key (string code or short name)
|
|
6699
|
+
* @param enabled true → turn on, false → turn off
|
|
6700
|
+
*/
|
|
6701
|
+
async setBundleCapability(params) {
|
|
6702
|
+
await this.post("account/ios/identifiers/updateAppService.action", {
|
|
6703
|
+
teamId: this.teamId,
|
|
6704
|
+
displayId: params.appIdId,
|
|
6705
|
+
featureType: params.featureType,
|
|
6706
|
+
featureValue: params.enabled ? "on" : "off"
|
|
6707
|
+
});
|
|
6708
|
+
}
|
|
6206
6709
|
// ── Certificates ───────────────────────────────────────────────────────
|
|
6207
6710
|
async listCertificates(types) {
|
|
6208
6711
|
const iosTypes = types ?? [
|
|
@@ -6220,12 +6723,21 @@ var init_portal = __esm({
|
|
|
6220
6723
|
return data.certRequests ?? [];
|
|
6221
6724
|
}
|
|
6222
6725
|
async listPushCertificates() {
|
|
6726
|
+
const types = Array.from(
|
|
6727
|
+
/* @__PURE__ */ new Set([
|
|
6728
|
+
_PortalClient.CERT_TYPES.IOS_PRODUCTION_PUSH,
|
|
6729
|
+
_PortalClient.CERT_TYPES.IOS_DEVELOPMENT_PUSH,
|
|
6730
|
+
// Legacy push codes from OLDER_IOS_CERTIFICATE_TYPES
|
|
6731
|
+
"3BQKVH9I2X",
|
|
6732
|
+
"BKLRAVXMGM"
|
|
6733
|
+
])
|
|
6734
|
+
);
|
|
6223
6735
|
const data = await this.post("account/ios/certificate/listCertRequests.action", {
|
|
6224
6736
|
teamId: this.teamId,
|
|
6225
6737
|
pageSize: "500",
|
|
6226
6738
|
pageNumber: "1",
|
|
6227
6739
|
sort: "certRequestStatusCode=asc",
|
|
6228
|
-
types:
|
|
6740
|
+
types: types.join(",")
|
|
6229
6741
|
});
|
|
6230
6742
|
return data.certRequests ?? [];
|
|
6231
6743
|
}
|
|
@@ -6367,18 +6879,332 @@ var init_portal = __esm({
|
|
|
6367
6879
|
}
|
|
6368
6880
|
});
|
|
6369
6881
|
|
|
6882
|
+
// src/client/iris.ts
|
|
6883
|
+
var IRIS_BASE, IrisClient, IrisAuthError;
|
|
6884
|
+
var init_iris = __esm({
|
|
6885
|
+
"src/client/iris.ts"() {
|
|
6886
|
+
IRIS_BASE = "https://appstoreconnect.apple.com/iris/v1";
|
|
6887
|
+
IrisClient = class {
|
|
6888
|
+
constructor(session) {
|
|
6889
|
+
this.session = session;
|
|
6890
|
+
this.cookieStr = Object.entries(session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
6891
|
+
}
|
|
6892
|
+
session;
|
|
6893
|
+
cookieStr;
|
|
6894
|
+
buildHeaders() {
|
|
6895
|
+
return {
|
|
6896
|
+
Cookie: this.cookieStr,
|
|
6897
|
+
Accept: "application/vnd.api+json, application/json, text/plain, */*",
|
|
6898
|
+
"Content-Type": "application/vnd.api+json",
|
|
6899
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
6900
|
+
// Mimic the App Store Connect web UI's user agent
|
|
6901
|
+
"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"
|
|
6902
|
+
};
|
|
6903
|
+
}
|
|
6904
|
+
/**
|
|
6905
|
+
* Issue a request and absorb any new cookies (Apple rotates session
|
|
6906
|
+
* cookies on a lot of iris calls).
|
|
6907
|
+
*/
|
|
6908
|
+
async request(method, path, body) {
|
|
6909
|
+
const headers = this.buildHeaders();
|
|
6910
|
+
if (process.env.FASTNODE_DEBUG) {
|
|
6911
|
+
console.log(`[iris] ${method} ${path}${body ? ` body=${JSON.stringify(body).slice(0, 200)}` : ""}`);
|
|
6912
|
+
}
|
|
6913
|
+
const res = await fetch(`${IRIS_BASE}${path}`, {
|
|
6914
|
+
method,
|
|
6915
|
+
headers,
|
|
6916
|
+
body: body ? JSON.stringify(body) : void 0
|
|
6917
|
+
});
|
|
6918
|
+
for (const raw of res.headers.getSetCookie?.() ?? []) {
|
|
6919
|
+
const m = raw.match(/^([^=]+)=([^;]*)/);
|
|
6920
|
+
if (m) {
|
|
6921
|
+
this.session.cookies[m[1]] = m[2];
|
|
6922
|
+
}
|
|
6923
|
+
}
|
|
6924
|
+
this.cookieStr = Object.entries(this.session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
6925
|
+
const text = await res.text();
|
|
6926
|
+
let parsed = null;
|
|
6927
|
+
try {
|
|
6928
|
+
parsed = text ? JSON.parse(text) : null;
|
|
6929
|
+
} catch {
|
|
6930
|
+
}
|
|
6931
|
+
if (process.env.FASTNODE_DEBUG) {
|
|
6932
|
+
console.log(`[iris] \u2192 ${res.status} ${text.slice(0, 300)}`);
|
|
6933
|
+
}
|
|
6934
|
+
return { status: res.status, body: parsed, rawText: text };
|
|
6935
|
+
}
|
|
6936
|
+
/**
|
|
6937
|
+
* List existing API keys. Used both as a sanity check (the user has
|
|
6938
|
+
* permission) and to discover the team's issuer id (which is the same
|
|
6939
|
+
* for every key in the team and is needed by altool).
|
|
6940
|
+
*
|
|
6941
|
+
* Important: Apple's iris endpoint rejects most query parameters with
|
|
6942
|
+
* `PARAMETER_ERROR.INVALID`. Call with no params and let the API return
|
|
6943
|
+
* its default page.
|
|
6944
|
+
*/
|
|
6945
|
+
async listApiKeys() {
|
|
6946
|
+
const { status, body, rawText } = await this.request("GET", "/apiKeys");
|
|
6947
|
+
if (status === 403) {
|
|
6948
|
+
throw new IrisAuthError(
|
|
6949
|
+
"You don't have permission to manage App Store Connect API keys (this requires the Account Holder or Admin role)."
|
|
6950
|
+
);
|
|
6951
|
+
}
|
|
6952
|
+
if (status >= 400 || !body) {
|
|
6953
|
+
throw new Error(`iris listApiKeys failed (${status}): ${rawText.slice(0, 200)}`);
|
|
6954
|
+
}
|
|
6955
|
+
const issuerId = body?.meta?.issuerId ?? body?.data?.[0]?.attributes?.issuerId;
|
|
6956
|
+
const keys = (body.data ?? []).map((d) => ({
|
|
6957
|
+
id: d.id,
|
|
6958
|
+
kid: d.attributes?.kid ?? d.id,
|
|
6959
|
+
nickname: d.attributes?.nickname ?? "",
|
|
6960
|
+
issuerId: d.attributes?.issuerId ?? issuerId,
|
|
6961
|
+
roles: d.attributes?.roles ?? [],
|
|
6962
|
+
allAppsVisible: d.attributes?.allAppsVisible ?? true
|
|
6963
|
+
}));
|
|
6964
|
+
return { keys, issuerId };
|
|
6965
|
+
}
|
|
6966
|
+
/**
|
|
6967
|
+
* Create a new API key. Apple returns the .p8 PEM exactly once in the
|
|
6968
|
+
* create response — capture it immediately or the key is dead weight.
|
|
6969
|
+
*
|
|
6970
|
+
* `keyType` is required by Apple's iris API:
|
|
6971
|
+
* • PUBLIC_API — the standard team-wide API key (what TestFlight uploads need)
|
|
6972
|
+
* • MARKETING_API — for App Analytics access only
|
|
6973
|
+
*/
|
|
6974
|
+
async createApiKey(input) {
|
|
6975
|
+
const requestBody = {
|
|
6976
|
+
data: {
|
|
6977
|
+
type: "apiKeys",
|
|
6978
|
+
attributes: {
|
|
6979
|
+
nickname: input.nickname,
|
|
6980
|
+
roles: input.roles ?? ["APP_MANAGER"],
|
|
6981
|
+
allAppsVisible: input.allAppsVisible ?? true,
|
|
6982
|
+
keyType: input.keyType ?? "PUBLIC_API"
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
};
|
|
6986
|
+
console.log("[iris] createApiKey request:", JSON.stringify(requestBody));
|
|
6987
|
+
const { status, body, rawText } = await this.request("POST", "/apiKeys", requestBody);
|
|
6988
|
+
console.log(`[iris] createApiKey response: ${status} ${rawText.slice(0, 800)}`);
|
|
6989
|
+
if (status === 403) {
|
|
6990
|
+
throw new IrisAuthError(
|
|
6991
|
+
"You don't have permission to create App Store Connect API keys (this requires the Account Holder or Admin role)."
|
|
6992
|
+
);
|
|
6993
|
+
}
|
|
6994
|
+
if (status >= 400 || !body) {
|
|
6995
|
+
const msg = body?.errors?.[0]?.detail ?? body?.errors?.[0]?.title ?? rawText.slice(0, 300) ?? `iris createApiKey failed (${status})`;
|
|
6996
|
+
throw new Error(msg);
|
|
6997
|
+
}
|
|
6998
|
+
const data = body.data ?? body;
|
|
6999
|
+
if (!data?.id) {
|
|
7000
|
+
throw new Error(`iris createApiKey returned no key id. Raw response: ${rawText.slice(0, 400)}`);
|
|
7001
|
+
}
|
|
7002
|
+
const key = {
|
|
7003
|
+
id: data.id,
|
|
7004
|
+
kid: data.attributes?.kid ?? data.id,
|
|
7005
|
+
nickname: data.attributes?.nickname ?? input.nickname,
|
|
7006
|
+
privateKey: data.attributes?.privateKey,
|
|
7007
|
+
issuerId: data.attributes?.issuerId,
|
|
7008
|
+
roles: data.attributes?.roles ?? input.roles ?? ["APP_MANAGER"],
|
|
7009
|
+
allAppsVisible: data.attributes?.allAppsVisible ?? true
|
|
7010
|
+
};
|
|
7011
|
+
return key;
|
|
7012
|
+
}
|
|
7013
|
+
/**
|
|
7014
|
+
* Fetch the .p8 PEM AND the team's issuer id for a previously-created
|
|
7015
|
+
* ASC API key. Both come back in a single request via:
|
|
7016
|
+
*
|
|
7017
|
+
* GET /iris/v1/apiKeys/{id}?fields[apiKeys]=privateKey&include=provider
|
|
7018
|
+
*
|
|
7019
|
+
* Sparse fieldset requesting ONLY `privateKey` is the magic that makes
|
|
7020
|
+
* Apple actually include the (base64-encoded) PEM in the response —
|
|
7021
|
+
* the default view returns `privateKey: null`. The `include=provider`
|
|
7022
|
+
* pulls the team's contentProvider record into `included[]`, and its
|
|
7023
|
+
* `id` field IS the issuer id altool needs (a UUID like
|
|
7024
|
+
* `cfadaf94-44ca-4c2c-8fa6-ecc28d3c7d0d`).
|
|
7025
|
+
*
|
|
7026
|
+
* Reverse-engineered from @expo/apple-utils v2.1.21 (the package that
|
|
7027
|
+
* powers EAS CLI), specifically the `ApiKey.downloadAsync` method
|
|
7028
|
+
* combined with `ApiKey.infoAsync` reading `provider?.id`.
|
|
7029
|
+
*
|
|
7030
|
+
* Returns `{ pem, issuerId }`, or `null` if Apple won't return the
|
|
7031
|
+
* private key (typically because the key was already downloaded once).
|
|
7032
|
+
*/
|
|
7033
|
+
async fetchApiKeyPrivateKey(keyId) {
|
|
7034
|
+
try {
|
|
7035
|
+
const url = `${IRIS_BASE}/apiKeys/${keyId}?fields%5BapiKeys%5D=privateKey&include=provider`;
|
|
7036
|
+
const res = await fetch(url, {
|
|
7037
|
+
headers: {
|
|
7038
|
+
...this.buildHeaders(),
|
|
7039
|
+
Accept: "application/vnd.api+json, application/json, text/plain, */*"
|
|
7040
|
+
}
|
|
7041
|
+
});
|
|
7042
|
+
const text = await res.text();
|
|
7043
|
+
console.log(`[iris] fetchApiKeyPrivateKey \u2192 ${res.status} (${text.length} bytes)`);
|
|
7044
|
+
if (res.status !== 200) {
|
|
7045
|
+
console.log(`[iris] err: ${text.slice(0, 300)}`);
|
|
7046
|
+
return null;
|
|
7047
|
+
}
|
|
7048
|
+
const body = JSON.parse(text);
|
|
7049
|
+
const encoded = body?.data?.attributes?.privateKey;
|
|
7050
|
+
if (typeof encoded !== "string" || encoded.length === 0) {
|
|
7051
|
+
console.log(`[iris] no privateKey in response`);
|
|
7052
|
+
return null;
|
|
7053
|
+
}
|
|
7054
|
+
const pem = Buffer.from(encoded, "base64").toString("utf-8");
|
|
7055
|
+
if (!pem.includes("BEGIN PRIVATE KEY")) {
|
|
7056
|
+
console.log(`[iris] decoded body didn't contain PEM marker`);
|
|
7057
|
+
return null;
|
|
7058
|
+
}
|
|
7059
|
+
const provider = (body?.included ?? []).find((i) => i.type === "contentProviders");
|
|
7060
|
+
const issuerId = provider?.id;
|
|
7061
|
+
console.log(`[iris] \u2713 PEM ${pem.length} bytes, issuerId=${issuerId ?? "(missing)"}`);
|
|
7062
|
+
return { pem, issuerId };
|
|
7063
|
+
} catch (err) {
|
|
7064
|
+
console.log(`[iris] fetchApiKeyPrivateKey error: ${err.message}`);
|
|
7065
|
+
return null;
|
|
7066
|
+
}
|
|
7067
|
+
}
|
|
7068
|
+
/**
|
|
7069
|
+
* @deprecated Kept as a fallback probe for debugging. The real path
|
|
7070
|
+
* is the sparse fieldset above — everything in this brute-force list
|
|
7071
|
+
* returned 404 during testing. Left here so we can re-verify if Apple
|
|
7072
|
+
* changes anything.
|
|
7073
|
+
*/
|
|
7074
|
+
async fetchApiKeyPrivateKeyBruteForce(keyId) {
|
|
7075
|
+
const attempts = [
|
|
7076
|
+
// Sparse fieldsets — JSON-API spec lets us request specific attributes.
|
|
7077
|
+
// Apple may exclude privateKey by default and return it only when asked.
|
|
7078
|
+
{ kind: "GET", path: `/apiKeys/${keyId}?fields[apiKeys]=privateKey,kid,issuerId,nickname,roles` },
|
|
7079
|
+
{ kind: "GET", path: `/apiKeys/${keyId}?include=privateKey` },
|
|
7080
|
+
{ kind: "GET", path: `/apiKeys/${keyId}?fields[apiKeys]=privateKey` },
|
|
7081
|
+
// Custom Accept headers — maybe Apple negotiates content type
|
|
7082
|
+
{ kind: "GET", path: `/apiKeys/${keyId}`, accept: "application/x-pem-file" },
|
|
7083
|
+
{ kind: "GET", path: `/apiKeys/${keyId}`, accept: "application/octet-stream" },
|
|
7084
|
+
{ kind: "GET", path: `/apiKeys/${keyId}/p8`, accept: "application/x-pem-file" },
|
|
7085
|
+
// POST variants — Apple sometimes uses POST to mint a download token
|
|
7086
|
+
{ kind: "POST", path: `/apiKeys/${keyId}/secrets`, body: {} },
|
|
7087
|
+
{ kind: "POST", path: `/apiKeys/${keyId}/regenerate`, body: {} },
|
|
7088
|
+
{ kind: "POST", path: `/apiKeys/${keyId}/p8`, body: {} },
|
|
7089
|
+
// Bare GET (last) — confirms canonical fields, in case my parser missed any
|
|
7090
|
+
{ kind: "GET", path: `/apiKeys/${keyId}` },
|
|
7091
|
+
// Different host — the legacy developer.apple.com user-and-access namespace
|
|
7092
|
+
{ kind: "ABSOLUTE", url: `https://appstoreconnect.apple.com/api/v1/apiKeys/${keyId}/p8` },
|
|
7093
|
+
{ kind: "ABSOLUTE", url: `https://appstoreconnect.apple.com/access/api/keys/${keyId}/download` }
|
|
7094
|
+
];
|
|
7095
|
+
for (const attempt of attempts) {
|
|
7096
|
+
try {
|
|
7097
|
+
let result;
|
|
7098
|
+
if (attempt.kind === "GET") {
|
|
7099
|
+
result = await this.requestWithHeaders("GET", attempt.path, void 0, {
|
|
7100
|
+
...attempt.accept ? { Accept: attempt.accept } : {}
|
|
7101
|
+
});
|
|
7102
|
+
console.log(`[iris] fetchApiKeyPrivateKey GET ${attempt.path}${attempt.accept ? ` accept=${attempt.accept}` : ""} \u2192 ${result.status} (${result.rawText.length} bytes)`);
|
|
7103
|
+
} else if (attempt.kind === "POST") {
|
|
7104
|
+
result = await this.request("POST", attempt.path, attempt.body);
|
|
7105
|
+
console.log(`[iris] fetchApiKeyPrivateKey POST ${attempt.path} \u2192 ${result.status} (${result.rawText.length} bytes)`);
|
|
7106
|
+
} else {
|
|
7107
|
+
result = await this.requestAbsolute("GET", attempt.url);
|
|
7108
|
+
console.log(`[iris] fetchApiKeyPrivateKey GET ${attempt.url} \u2192 ${result.status} (${result.rawText.length} bytes)`);
|
|
7109
|
+
}
|
|
7110
|
+
if (result.status === 200) {
|
|
7111
|
+
console.log(`[iris] body: ${result.rawText.slice(0, 1500)}`);
|
|
7112
|
+
}
|
|
7113
|
+
if (result.status !== 200) continue;
|
|
7114
|
+
if (result.rawText && result.rawText.includes("BEGIN PRIVATE KEY")) {
|
|
7115
|
+
console.log(`[iris] \u2713 found PEM (plain text response)`);
|
|
7116
|
+
return result.rawText;
|
|
7117
|
+
}
|
|
7118
|
+
if (result.body) {
|
|
7119
|
+
const candidates = [
|
|
7120
|
+
result.body?.data?.attributes?.privateKey,
|
|
7121
|
+
result.body?.data?.attributes?.p8,
|
|
7122
|
+
result.body?.data?.attributes?.privateKeyContent,
|
|
7123
|
+
result.body?.data?.attributes?.kpem,
|
|
7124
|
+
result.body?.data?.attributes?.secret,
|
|
7125
|
+
result.body?.data?.attributes?.keyContent,
|
|
7126
|
+
result.body?.privateKey,
|
|
7127
|
+
result.body?.p8,
|
|
7128
|
+
result.body?.value,
|
|
7129
|
+
result.body?.secret
|
|
7130
|
+
];
|
|
7131
|
+
for (const c of candidates) {
|
|
7132
|
+
if (typeof c === "string" && c.includes("BEGIN PRIVATE KEY")) {
|
|
7133
|
+
console.log(`[iris] \u2713 found PEM in JSON response`);
|
|
7134
|
+
return c;
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
} catch (err) {
|
|
7139
|
+
console.log(`[iris] fetchApiKeyPrivateKey attempt failed: ${err.message}`);
|
|
7140
|
+
}
|
|
7141
|
+
}
|
|
7142
|
+
console.log(`[iris] fetchApiKeyPrivateKey: no working download path for key ${keyId}`);
|
|
7143
|
+
return null;
|
|
7144
|
+
}
|
|
7145
|
+
/** Internal: like request() but lets us override headers. */
|
|
7146
|
+
async requestWithHeaders(method, path, body, extraHeaders = {}) {
|
|
7147
|
+
const headers = { ...this.buildHeaders(), ...extraHeaders };
|
|
7148
|
+
const res = await fetch(`${IRIS_BASE}${path}`, {
|
|
7149
|
+
method,
|
|
7150
|
+
headers,
|
|
7151
|
+
body: body ? JSON.stringify(body) : void 0
|
|
7152
|
+
});
|
|
7153
|
+
for (const raw of res.headers.getSetCookie?.() ?? []) {
|
|
7154
|
+
const m = raw.match(/^([^=]+)=([^;]*)/);
|
|
7155
|
+
if (m) this.session.cookies[m[1]] = m[2];
|
|
7156
|
+
}
|
|
7157
|
+
this.cookieStr = Object.entries(this.session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
7158
|
+
const text = await res.text();
|
|
7159
|
+
let parsed = null;
|
|
7160
|
+
try {
|
|
7161
|
+
parsed = text ? JSON.parse(text) : null;
|
|
7162
|
+
} catch {
|
|
7163
|
+
}
|
|
7164
|
+
return { status: res.status, body: parsed, rawText: text };
|
|
7165
|
+
}
|
|
7166
|
+
/** Internal: fetch an absolute URL with the session cookies. */
|
|
7167
|
+
async requestAbsolute(method, url) {
|
|
7168
|
+
const headers = this.buildHeaders();
|
|
7169
|
+
const res = await fetch(url, { method, headers });
|
|
7170
|
+
const text = await res.text();
|
|
7171
|
+
let parsed = null;
|
|
7172
|
+
try {
|
|
7173
|
+
parsed = text ? JSON.parse(text) : null;
|
|
7174
|
+
} catch {
|
|
7175
|
+
}
|
|
7176
|
+
return { status: res.status, body: parsed, rawText: text };
|
|
7177
|
+
}
|
|
7178
|
+
async deleteApiKey(keyId) {
|
|
7179
|
+
await this.request("DELETE", `/apiKeys/${keyId}`);
|
|
7180
|
+
}
|
|
7181
|
+
};
|
|
7182
|
+
IrisAuthError = class extends Error {
|
|
7183
|
+
constructor(message) {
|
|
7184
|
+
super(message);
|
|
7185
|
+
this.name = "IrisAuthError";
|
|
7186
|
+
}
|
|
7187
|
+
};
|
|
7188
|
+
}
|
|
7189
|
+
});
|
|
7190
|
+
|
|
6370
7191
|
// src/client/index.ts
|
|
6371
7192
|
var client_exports = {};
|
|
6372
7193
|
__export(client_exports, {
|
|
6373
7194
|
AppStoreConnectClient: () => AppStoreConnectClient,
|
|
6374
7195
|
AppStoreConnectError: () => AppStoreConnectError,
|
|
7196
|
+
IrisAuthError: () => IrisAuthError,
|
|
7197
|
+
IrisClient: () => IrisClient,
|
|
6375
7198
|
PortalClient: () => PortalClient,
|
|
6376
7199
|
ProgramLicenseAgreementError: () => ProgramLicenseAgreementError,
|
|
6377
7200
|
RateLimitError: () => RateLimitError,
|
|
6378
7201
|
RequestClient: () => RequestClient,
|
|
6379
7202
|
TokenManager: () => TokenManager,
|
|
7203
|
+
beginLogin: () => beginLogin,
|
|
6380
7204
|
checkPortalResponse: () => checkPortalResponse,
|
|
6381
|
-
loginWithAppleId: () => loginWithAppleId
|
|
7205
|
+
loginWithAppleId: () => loginWithAppleId,
|
|
7206
|
+
requestSmsCode: () => requestSmsCode,
|
|
7207
|
+
submitTwoFactorCode: () => submitTwoFactorCode
|
|
6382
7208
|
});
|
|
6383
7209
|
var AppStoreConnectClient;
|
|
6384
7210
|
var init_client = __esm({
|
|
@@ -6423,6 +7249,7 @@ var init_client = __esm({
|
|
|
6423
7249
|
init_request();
|
|
6424
7250
|
init_apple_session();
|
|
6425
7251
|
init_portal();
|
|
7252
|
+
init_iris();
|
|
6426
7253
|
AppStoreConnectClient = class {
|
|
6427
7254
|
http;
|
|
6428
7255
|
certificates;
|