@sitepong/fastnode 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +148 -0
  2. package/dist/actions/index.cjs +758 -110
  3. package/dist/actions/index.cjs.map +1 -1
  4. package/dist/actions/index.mjs +758 -110
  5. package/dist/actions/index.mjs.map +1 -1
  6. package/dist/cert/index.cjs +40 -0
  7. package/dist/cert/index.cjs.map +1 -1
  8. package/dist/cert/index.d.cts +3 -3
  9. package/dist/cert/index.d.ts +3 -3
  10. package/dist/cert/index.mjs +37 -1
  11. package/dist/cert/index.mjs.map +1 -1
  12. package/dist/{cert-owkOFNW8.d.ts → cert-e5GAgAPb.d.ts} +1 -1
  13. package/dist/{cert-BmJGtYtb.d.cts → cert-uLLdb5a7.d.cts} +1 -1
  14. package/dist/cli/index.cjs +758 -110
  15. package/dist/cli/index.cjs.map +1 -1
  16. package/dist/cli/index.d.cts +1 -1
  17. package/dist/cli/index.d.ts +1 -1
  18. package/dist/cli/index.mjs +758 -110
  19. package/dist/cli/index.mjs.map +1 -1
  20. package/dist/client/index.cjs +751 -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 +747 -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-BFdNOmPJ.d.cts → index-BN0iWhuw.d.cts} +1 -1
  29. package/dist/index-BNP7Bpu5.d.ts +61 -0
  30. package/dist/{index-1Vu2cRQ2.d.ts → index-BbY_H_f6.d.ts} +1 -1
  31. package/dist/{index-D6zjGlPL.d.cts → index-BgHagzj1.d.cts} +1 -1
  32. package/dist/{index-D5eihzwM.d.cts → index-BjkOUQcQ.d.cts} +1 -1
  33. package/dist/{index-D1Izm7qt.d.ts → index-BwwpAlk2.d.ts} +363 -2
  34. package/dist/{index-A5MimeQk.d.ts → index-C2YLoFVl.d.ts} +1 -1
  35. package/dist/{index-TfXqwuac.d.ts → index-CEldrbyz.d.ts} +1 -1
  36. package/dist/{index-BLubSlgK.d.cts → index-CdfgEZwh.d.cts} +363 -2
  37. package/dist/{index-DfCUVJX1.d.cts → index-Clyf4-lu.d.cts} +1 -1
  38. package/dist/index-CtydPNxA.d.cts +61 -0
  39. package/dist/{index-BQJIJlK7.d.cts → index-DHxi-QPC.d.cts} +3 -3
  40. package/dist/{index-BN0ubxjZ.d.ts → index-Di4Ic0EE.d.ts} +3 -3
  41. package/dist/{index-EcVvViW8.d.cts → index-DrCOh0Vh.d.cts} +1 -1
  42. package/dist/{index-B0yhu_LZ.d.ts → index-EKU_G1G5.d.ts} +1 -1
  43. package/dist/{index-CWNLnSOL.d.cts → index-XsMsVc3O.d.cts} +1 -1
  44. package/dist/{index-B9yXicrQ.d.ts → index-jeI44Q5G.d.ts} +1 -1
  45. package/dist/{index-DHw08dUc.d.ts → index-mddpvrRX.d.ts} +1 -1
  46. package/dist/index.cjs +1233 -110
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.cts +226 -10
  49. package/dist/index.d.ts +226 -10
  50. package/dist/index.mjs +1230 -111
  51. package/dist/index.mjs.map +1 -1
  52. package/dist/lane/index.cjs +758 -110
  53. package/dist/lane/index.cjs.map +1 -1
  54. package/dist/lane/index.mjs +758 -110
  55. package/dist/lane/index.mjs.map +1 -1
  56. package/dist/match/index.d.cts +4 -4
  57. package/dist/match/index.d.ts +4 -4
  58. package/dist/pem/index.d.cts +2 -2
  59. package/dist/pem/index.d.ts +2 -2
  60. package/dist/pilot/index.d.cts +2 -2
  61. package/dist/pilot/index.d.ts +2 -2
  62. package/dist/precheck/index.d.cts +2 -2
  63. package/dist/precheck/index.d.ts +2 -2
  64. package/dist/produce/index.d.cts +2 -2
  65. package/dist/produce/index.d.ts +2 -2
  66. package/dist/sigh/index.d.cts +2 -2
  67. package/dist/sigh/index.d.ts +2 -2
  68. package/package.json +3 -1
  69. package/dist/index-DfirmFbC.d.ts +0 -32
  70. package/dist/index-lt819Jgj.d.cts +0 -32
@@ -205,9 +205,9 @@ Please accept the agreement at https://developer.apple.com/account before contin
205
205
  throw new Error(msg);
206
206
  }
207
207
  }
208
- async function loginWithAppleId(config) {
209
- const log = config.onLog ?? console.log;
210
- const promptFn = config.onPrompt ?? defaultPrompt;
208
+ async function beginLogin(config) {
209
+ const log = config.onLog ?? (() => {
210
+ });
211
211
  log("Fetching Apple service key...");
212
212
  const serviceKey = await fetchServiceKey();
213
213
  const initRes = await fetch(`${AUTH_BASE}/signin?widgetKey=${serviceKey}`, {
@@ -224,27 +224,94 @@ async function loginWithAppleId(config) {
224
224
  const cookieStr = Object.entries(initCookies).map(([k, v]) => k.startsWith("DES") ? `${k}="${v}"` : `${k}=${v}`).join("; ");
225
225
  log("Starting SIRP Apple ID login...");
226
226
  const response = await doSirp(config.email, config.password, serviceKey, cookieStr, hashcash);
227
- const session = {
227
+ const baseState = {
228
228
  cookies: { ...initCookies, ...response.cookies },
229
229
  sessionId: response.sessionId,
230
230
  scnt: response.scnt,
231
231
  serviceKey,
232
- userEmail: config.email,
233
- pendingAgreements: []
232
+ userEmail: config.email
234
233
  };
235
234
  if (response.status === 403) {
236
235
  throw new Error(`Invalid username and password combination. Used '${config.email}' as the username.`);
237
- } else if (response.status === 200) {
238
- log("Login successful.");
239
- await fetchOlympusSession(session);
240
- } else if (response.status === 409) {
241
- await handleTwoStepOrFactor(session, log, promptFn);
242
- await fetchOlympusSession(session);
243
- } else if (response.status === 412) {
236
+ }
237
+ if (response.status === 412) {
244
238
  throw new Error("Need to acknowledge Apple's Apple ID and Privacy statement. Please manually log into https://appleid.apple.com.");
239
+ }
240
+ if (response.status === 200) {
241
+ log("Login successful (no 2FA).");
242
+ const session = { ...baseState, pendingAgreements: [] };
243
+ await finalizeSession(session, log);
244
+ return { kind: "logged_in", session };
245
+ }
246
+ if (response.status === 409) {
247
+ log("2FA required, fetching challenge...");
248
+ const challenge = await fetchTwoFactorChallenge(baseState);
249
+ return { kind: "needs_2fa", pending: baseState, challenge };
250
+ }
251
+ throw new Error(`Unexpected login response (${response.status}): ${JSON.stringify(response.body).slice(0, 500)}`);
252
+ }
253
+ async function fetchTwoFactorChallenge(pending) {
254
+ const r = await fetch(`${AUTH_BASE}`, { headers: buildAuthHeadersFromPending(pending) });
255
+ const body = await r.json();
256
+ const trustedPhoneNumbers = (body.trustedPhoneNumbers ?? []).map((p) => ({
257
+ id: p.id,
258
+ numberWithDialCode: p.numberWithDialCode,
259
+ pushMode: p.pushMode
260
+ }));
261
+ const phoneOnly = body.noTrustedDevices === true;
262
+ const codeLength = body.securityCode?.length ?? 6;
263
+ return {
264
+ kind: phoneOnly ? "phone_only" : "device",
265
+ codeLength,
266
+ trustedPhoneNumbers
267
+ };
268
+ }
269
+ async function requestSmsCode(input) {
270
+ await fetch(`${AUTH_BASE}/verify/phone`, {
271
+ method: "PUT",
272
+ headers: { ...buildAuthHeadersFromPending(input.pending), "Content-Type": "application/json" },
273
+ body: JSON.stringify({ phoneNumber: { id: input.phoneId }, mode: input.mode ?? "sms" })
274
+ });
275
+ }
276
+ async function submitTwoFactorCode(input) {
277
+ const log = input.onLog ?? (() => {
278
+ });
279
+ let path;
280
+ let body;
281
+ if (input.source === "device") {
282
+ path = `${AUTH_BASE}/verify/trusteddevice/securitycode`;
283
+ body = JSON.stringify({ securityCode: { code: input.code } });
245
284
  } else {
246
- throw new Error(`Unexpected login response (${response.status}): ${JSON.stringify(response.body).slice(0, 500)}`);
285
+ if (input.phoneId == null) throw new Error("phoneId is required when source='phone'");
286
+ path = `${AUTH_BASE}/verify/phone/securitycode`;
287
+ body = JSON.stringify({
288
+ securityCode: { code: input.code },
289
+ phoneNumber: { id: input.phoneId },
290
+ mode: input.mode ?? "sms"
291
+ });
247
292
  }
293
+ log("Submitting 2FA code...");
294
+ const verifyRes = await fetch(path, {
295
+ method: "POST",
296
+ headers: { ...buildAuthHeadersFromPending(input.pending), "Content-Type": "application/json" },
297
+ body
298
+ });
299
+ if (!verifyRes.ok) {
300
+ const err = await verifyRes.json().catch(() => ({}));
301
+ const msgs = (err?.service_errors ?? err?.serviceErrors ?? []).map((e) => e.message).filter(Boolean);
302
+ throw new Error(msgs.join("; ") || `Verification failed (${verifyRes.status})`);
303
+ }
304
+ const session = {
305
+ ...input.pending,
306
+ cookies: { ...input.pending.cookies, ...parseCookies(verifyRes.headers) },
307
+ pendingAgreements: []
308
+ };
309
+ await trustSession(session);
310
+ await finalizeSession(session, log);
311
+ return session;
312
+ }
313
+ async function finalizeSession(session, log) {
314
+ await fetchOlympusSession(session);
248
315
  const agreements = await fetchContractMessages(session);
249
316
  if (agreements.length > 0) {
250
317
  for (const msg of agreements) {
@@ -253,7 +320,53 @@ async function loginWithAppleId(config) {
253
320
  }
254
321
  session.pendingAgreements = agreements;
255
322
  log(`Logged in as ${session.userEmail}`);
256
- return session;
323
+ }
324
+ async function loginWithAppleId(config) {
325
+ const log = config.onLog ?? console.log;
326
+ const promptFn = config.onPrompt ?? defaultPrompt;
327
+ const result = await beginLogin({ email: config.email, password: config.password, onLog: log });
328
+ if (result.kind === "logged_in") return result.session;
329
+ const { pending, challenge } = result;
330
+ const phones = challenge.trustedPhoneNumbers;
331
+ if (challenge.kind === "device") {
332
+ log("Two-factor Authentication (6 digit code) is enabled for this account");
333
+ log("(Input 'sms' to escape this prompt and select a trusted phone number to send the code as a text message)");
334
+ const input = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
335
+ if (input.toLowerCase() === "sms") {
336
+ log("Please select a trusted phone number:");
337
+ for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
338
+ const choice2 = await promptFn("Enter number: ");
339
+ const phone2 = phones[Math.max(0, parseInt(choice2, 10) - 1)] ?? phones[0];
340
+ await requestSmsCode({ pending, phoneId: phone2.id, mode: phone2.pushMode });
341
+ log(`Code sent to ${phone2.numberWithDialCode}`);
342
+ const code2 = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
343
+ return submitTwoFactorCode({ pending, code: code2, source: "phone", phoneId: phone2.id, mode: phone2.pushMode ?? "sms", onLog: log });
344
+ }
345
+ return submitTwoFactorCode({ pending, code: input, source: "device", onLog: log });
346
+ }
347
+ log("Two-factor Authentication (6 digit code) is enabled for this account");
348
+ if (phones.length === 1) {
349
+ const phone2 = phones[0];
350
+ log(`Code automatically sent to ${phone2.numberWithDialCode}`);
351
+ const code2 = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
352
+ return submitTwoFactorCode({ pending, code: code2, source: "phone", phoneId: phone2.id, mode: phone2.pushMode ?? "sms", onLog: log });
353
+ }
354
+ log("Please select a trusted phone number:");
355
+ for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
356
+ const choice = await promptFn("Enter number: ");
357
+ const phone = phones[Math.max(0, parseInt(choice, 10) - 1)] ?? phones[0];
358
+ await requestSmsCode({ pending, phoneId: phone.id, mode: phone.pushMode });
359
+ log(`Code sent to ${phone.numberWithDialCode}`);
360
+ const code = await promptFn(`Please enter the ${challenge.codeLength} digit code: `);
361
+ return submitTwoFactorCode({ pending, code, source: "phone", phoneId: phone.id, mode: phone.pushMode ?? "sms", onLog: log });
362
+ }
363
+ function buildAuthHeadersFromPending(pending) {
364
+ const h = { "X-Apple-Widget-Key": pending.serviceKey, Accept: "application/json" };
365
+ if (pending.sessionId) h["X-Apple-Id-Session-Id"] = pending.sessionId;
366
+ if (pending.scnt) h["scnt"] = pending.scnt;
367
+ const c = Object.entries(pending.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
368
+ if (c) h["Cookie"] = c;
369
+ return h;
257
370
  }
258
371
  async function doSirp(user, password, serviceKey, cookieStr, hashcash) {
259
372
  const client = new SRPClient(2048);
@@ -309,99 +422,6 @@ async function doSirp(user, password, serviceKey, cookieStr, hashcash) {
309
422
  scnt: completeRes.headers.get("scnt") ?? ""
310
423
  };
311
424
  }
312
- async function handleTwoStepOrFactor(session, log, promptFn) {
313
- const r = await fetch(`${AUTH_BASE}`, { headers: buildAuthHeaders(session) });
314
- const body = await r.json();
315
- if (body.trustedDevices?.length > 0) {
316
- await handleTwoStep(session, body, log, promptFn);
317
- } else if (body.trustedPhoneNumbers?.length > 0) {
318
- await handleTwoFactor(session, body, log, promptFn);
319
- } else {
320
- throw new Error("2FA required but no trusted devices or phone numbers found.");
321
- }
322
- }
323
- async function handleTwoFactor(session, authBody, log, promptFn) {
324
- log("Two-factor Authentication (6 digit code) is enabled for this account");
325
- const codeLength = authBody.securityCode?.length ?? 6;
326
- const phones = authBody.trustedPhoneNumbers ?? [];
327
- const noTrusted = authBody.noTrustedDevices === true;
328
- let codeType;
329
- let reqBody;
330
- if (noTrusted && phones.length === 1) {
331
- const phone = phones[0];
332
- log(`Code automatically sent to ${phone.numberWithDialCode}`);
333
- const code = await promptFn(`Please enter the ${codeLength} digit code you received at ${phone.numberWithDialCode}: `);
334
- codeType = "phone";
335
- reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
336
- } else if (noTrusted) {
337
- const phone = await choosePhone(phones, log, promptFn);
338
- await requestPhoneCode(session, phone);
339
- log(`Successfully requested text message to ${phone.numberWithDialCode}`);
340
- const code = await promptFn(`Please enter the ${codeLength} digit code: `);
341
- codeType = "phone";
342
- reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
343
- } else {
344
- log("(Input 'sms' to escape this prompt and select a trusted phone number to send the code as a text message)");
345
- const input = await promptFn(`Please enter the ${codeLength} digit code: `);
346
- if (input.toLowerCase() === "sms") {
347
- const phone = await choosePhone(phones, log, promptFn);
348
- await requestPhoneCode(session, phone);
349
- const code = await promptFn(`Please enter the ${codeLength} digit code sent to ${phone.numberWithDialCode}: `);
350
- codeType = "phone";
351
- reqBody = JSON.stringify({ securityCode: { code }, phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" });
352
- } else {
353
- codeType = "trusteddevice";
354
- reqBody = JSON.stringify({ securityCode: { code: input } });
355
- }
356
- }
357
- log("Requesting session...");
358
- const verifyRes = await fetch(`${AUTH_BASE}/verify/${codeType}/securitycode`, {
359
- method: "POST",
360
- headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
361
- body: reqBody
362
- });
363
- if (!verifyRes.ok) {
364
- const err = await verifyRes.json().catch(() => ({}));
365
- const msgs = (err?.service_errors ?? err?.serviceErrors ?? []).map((e) => e.message).filter(Boolean);
366
- throw new Error(msgs.join("; ") || `Verification failed (${verifyRes.status})`);
367
- }
368
- Object.assign(session.cookies, parseCookies(verifyRes.headers));
369
- await trustSession(session);
370
- }
371
- async function handleTwoStep(session, authBody, log, promptFn) {
372
- log("Two-step Verification (4 digit code) is enabled for this account");
373
- const devices = authBody.trustedDevices ?? [];
374
- log("Please select a trusted device:");
375
- for (let i = 0; i < devices.length; i++) {
376
- log(` ${i + 1}. ${devices[i].name} ${devices[i].modelName ?? "SMS"} (${devices[i].id})`);
377
- }
378
- const choice = await promptFn("Enter device number: ");
379
- const device = devices[Math.max(0, parseInt(choice, 10) - 1)] ?? devices[0];
380
- await fetch(`${AUTH_BASE}/verify/device/${device.id}/securitycode`, { method: "PUT", headers: buildAuthHeaders(session) });
381
- log("Successfully requested notification");
382
- const code = await promptFn("Please enter the 4 digit code: ");
383
- const verifyRes = await fetch(`${AUTH_BASE}/verify/phone/securitycode`, {
384
- method: "POST",
385
- headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
386
- body: JSON.stringify({ phoneNumber: { id: device.id }, securityCode: { code }, mode: "sms" })
387
- });
388
- if (!verifyRes.ok) throw new Error("Incorrect verification code.");
389
- Object.assign(session.cookies, parseCookies(verifyRes.headers));
390
- await trustSession(session);
391
- }
392
- async function choosePhone(phones, log, promptFn) {
393
- log("Please select a trusted phone number:");
394
- for (let i = 0; i < phones.length; i++) log(` ${i + 1}. ${phones[i].numberWithDialCode}`);
395
- const choice = await promptFn("Enter number: ");
396
- return phones[Math.max(0, parseInt(choice, 10) - 1)] ?? phones[0];
397
- }
398
- async function requestPhoneCode(session, phone) {
399
- await fetch(`${AUTH_BASE}/verify/phone`, {
400
- method: "PUT",
401
- headers: { ...buildAuthHeaders(session), "Content-Type": "application/json" },
402
- body: JSON.stringify({ phoneNumber: { id: phone.id }, mode: phone.pushMode ?? "sms" })
403
- });
404
- }
405
425
  async function trustSession(session) {
406
426
  const r = await fetch(`${AUTH_BASE}/2sv/trust`, { headers: buildAuthHeaders(session) });
407
427
  Object.assign(session.cookies, parseCookies(r.headers));
@@ -2249,6 +2269,12 @@ var XcodeMetricsResource = class {
2249
2269
 
2250
2270
  // src/client/portal.ts
2251
2271
  var PORTAL_BASE = "https://developer.apple.com/services-account/QH65B2";
2272
+ function pickUniqueKeyName(base, taken) {
2273
+ if (!taken.has(base)) return base;
2274
+ let n = 1;
2275
+ while (taken.has(`${base} - ${n}`)) n++;
2276
+ return `${base} - ${n}`;
2277
+ }
2252
2278
  var PortalClient = class _PortalClient {
2253
2279
  session;
2254
2280
  teamId;
@@ -2264,6 +2290,279 @@ var PortalClient = class _PortalClient {
2264
2290
  async init() {
2265
2291
  await this.post("account/listTeams.action", {});
2266
2292
  }
2293
+ /**
2294
+ * JSON POST — different content type than the standard form-encoded
2295
+ * `post()`. Used by the App Store Connect API key endpoints
2296
+ * (`account/auth/key/v2/create`), which Spaceship discovered take a
2297
+ * JSON body even though every other portal mutation is form-encoded.
2298
+ *
2299
+ * Like the form-encoded `post()`, this also runs the response through
2300
+ * `checkPortalResponse` — Apple returns HTTP 200 even for errors and
2301
+ * encodes the error in `resultCode`/`userString` fields, so a raw
2302
+ * status check would silently miss real failures.
2303
+ */
2304
+ async postJson(path, body) {
2305
+ const headers = {
2306
+ Cookie: this.cookieStr,
2307
+ "Content-Type": "application/json",
2308
+ Accept: "application/json"
2309
+ };
2310
+ if (this.csrf) {
2311
+ headers["csrf"] = this.csrf;
2312
+ headers["csrf_ts"] = this.csrfTs;
2313
+ }
2314
+ console.log(`[portal] POST(json) ${path}`);
2315
+ const res = await fetch(`${PORTAL_BASE}/${path}`, {
2316
+ method: "POST",
2317
+ headers,
2318
+ body: JSON.stringify(body)
2319
+ });
2320
+ if (res.headers.get("csrf")) {
2321
+ this.csrf = res.headers.get("csrf");
2322
+ this.csrfTs = res.headers.get("csrf_ts") ?? "";
2323
+ }
2324
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
2325
+ const m = raw.match(/^([^=]+)=([^;]*)/);
2326
+ if (m && !this.cookieStr.includes(m[1] + "=")) this.cookieStr += "; " + m[1] + "=" + m[2];
2327
+ }
2328
+ const text = await res.text();
2329
+ let data = null;
2330
+ try {
2331
+ data = text ? JSON.parse(text) : null;
2332
+ } catch {
2333
+ }
2334
+ console.log(`[portal] POST(json) ${path} \u2192 ${res.status} ${text.slice(0, 400)}`);
2335
+ checkPortalResponse(data, this.session);
2336
+ return { status: res.status, body: data, rawText: text };
2337
+ }
2338
+ /**
2339
+ * GET helper for the portal endpoints that take query parameters
2340
+ * (currently just `account/auth/key/download`). Returns the raw text
2341
+ * since download endpoints can return PEMs or JSON depending on Apple's
2342
+ * mood — caller decides how to parse.
2343
+ */
2344
+ async getRaw(path, params = {}) {
2345
+ const url = new URL(`${PORTAL_BASE}/${path}`);
2346
+ for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
2347
+ const headers = {
2348
+ Cookie: this.cookieStr,
2349
+ Accept: "application/json, text/plain, application/x-pem-file, */*"
2350
+ };
2351
+ if (this.csrf) {
2352
+ headers["csrf"] = this.csrf;
2353
+ headers["csrf_ts"] = this.csrfTs;
2354
+ }
2355
+ console.log(`[portal] GET ${url.pathname}${url.search}`);
2356
+ const res = await fetch(url.toString(), { headers });
2357
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
2358
+ const m = raw.match(/^([^=]+)=([^;]*)/);
2359
+ if (m && !this.cookieStr.includes(m[1] + "=")) this.cookieStr += "; " + m[1] + "=" + m[2];
2360
+ }
2361
+ const text = await res.text();
2362
+ const contentType = res.headers.get("content-type") ?? "";
2363
+ console.log(`[portal] GET ${url.pathname} \u2192 ${res.status} ${contentType} ${text.length}b`);
2364
+ return { status: res.status, rawText: text, contentType };
2365
+ }
2366
+ // ── App Store Connect API Keys (.p8) ────────────────────────────────────
2367
+ /**
2368
+ * Create an App Store Connect API key via the developer portal's
2369
+ * `account/auth/key/v2/create` endpoint. Reverse-engineered from the
2370
+ * _current_ @expo/apple-utils npm package (v2.1.21), which is what
2371
+ * EAS CLI uses to manage Apple credentials in production.
2372
+ *
2373
+ * This differs from older published reverse-engineering guides:
2374
+ * • URL uses the `/v2/create` suffix (Apple kept it)
2375
+ * • Field is `serviceConfigurations`, shape is an object keyed by
2376
+ * service id (NOT `serviceConfigurationsRequests: [...]` array)
2377
+ * • Body REQUIRES `scope: "team"`
2378
+ * • Needs a scope-specific CSRF warmup via the `keys` endpoint
2379
+ * before the create call (EAS's `ensureCSRFAsync("keys", …)`)
2380
+ *
2381
+ * For an App Store Connect API key (PUBLIC_API, used by altool for
2382
+ * TestFlight uploads), `serviceConfigurations` is an empty object `{}`.
2383
+ * For an APNs auth key, it's `{ "U27F4V844T": [] }`. For MusicKit:
2384
+ * `{ "6A7HVUVQ3M": [musicId] }`, etc.
2385
+ *
2386
+ * Apple's name validator rejects parentheses, brackets, and most
2387
+ * punctuation — sanitize to alphanumeric + space + dash + underscore
2388
+ * + period and collapse whitespace.
2389
+ */
2390
+ async createApiKey(rawName, serviceConfigurations = {}) {
2391
+ const baseName = rawName.replace(/[^A-Za-z0-9 _\-.]/g, " ").replace(/\s+/g, " ").trim().slice(0, 45);
2392
+ if (!baseName) throw new Error("createApiKey: name is empty after sanitization");
2393
+ let existingNames = /* @__PURE__ */ new Set();
2394
+ try {
2395
+ const list = await this.listApiKeys();
2396
+ existingNames = new Set(
2397
+ (list.keys ?? []).map((k) => String(k.keyName ?? "").trim())
2398
+ );
2399
+ } catch {
2400
+ }
2401
+ const name = pickUniqueKeyName(baseName, existingNames);
2402
+ const body = {
2403
+ teamId: this.teamId,
2404
+ name,
2405
+ serviceConfigurations,
2406
+ scope: "team"
2407
+ };
2408
+ const result = await this.postJson("account/auth/key/v2/create", body);
2409
+ const created = result.body?.keys?.[0] ?? result.body?.key ?? result.body?.data ?? result.body;
2410
+ const keyId = created?.keyId ?? created?.id ?? created?.kid ?? created?.attributes?.kid;
2411
+ if (!keyId) {
2412
+ throw new Error(`createApiKey: no key id in response. Body: ${result.rawText.slice(0, 400)}`);
2413
+ }
2414
+ return { keyId, raw: created };
2415
+ }
2416
+ /**
2417
+ * Fetch a single API key by id via `account/auth/key/get`. Reverse-
2418
+ * engineered from Spaceship's `get_key`. Returns the raw key record so
2419
+ * the caller can extract whatever fields it needs (e.g. issuerId).
2420
+ *
2421
+ * Form-encoded POST — same shape as every other PortalClient mutation
2422
+ * except the JSON `create` endpoint.
2423
+ */
2424
+ async getApiKey(keyId) {
2425
+ const data = await this.post("account/auth/key/get", {
2426
+ teamId: this.teamId,
2427
+ keyId
2428
+ });
2429
+ return data?.keys?.[0] ?? data?.key ?? data?.data ?? data;
2430
+ }
2431
+ /**
2432
+ * Revoke an existing key. Form-encoded POST per Spaceship's
2433
+ * `revoke_key!`. Used for cleaning up orphan keys created during
2434
+ * failed wizard runs.
2435
+ */
2436
+ async revokeApiKey(keyId) {
2437
+ await this.post("account/auth/key/revoke", {
2438
+ teamId: this.teamId,
2439
+ keyId
2440
+ });
2441
+ }
2442
+ // ── APNs Authentication Keys (.p8) ──────────────────────────────────────
2443
+ /** Apple's hardcoded service id for APNs auth keys (from Spaceship). */
2444
+ static APNS_SERVICE_ID = "U27F4V844T";
2445
+ /**
2446
+ * Create an APNs Authentication Key (.p8) via the developer portal.
2447
+ *
2448
+ * Apple requires BOTH `serviceConfigurations` (the map) AND
2449
+ * `serviceConfigurationsRequests` (the array of full config objects)
2450
+ * in the v2/create body. Just sending the map alone results in
2451
+ * "No value was provided for the parameter 'serviceConfigurations'".
2452
+ *
2453
+ * The exact body comes from @expo/apple-utils v2.1.21 — both fields
2454
+ * are produced by separate functions (`d` for the map, `h` for the
2455
+ * array) and merged into the create payload.
2456
+ *
2457
+ * Apple also rejects duplicate key names ("Another key with same
2458
+ * name exists") — we dedupe by listing existing keys first and
2459
+ * appending " (N)" until we find a free slot, mirroring the
2460
+ * provisioning profile dedup.
2461
+ */
2462
+ async createApnsAuthKey(rawName) {
2463
+ const baseName = rawName.replace(/[^A-Za-z0-9 _\-.]/g, " ").replace(/\s+/g, " ").trim().slice(0, 45);
2464
+ if (!baseName) throw new Error("createApnsAuthKey: name is empty after sanitization");
2465
+ let existingNames = /* @__PURE__ */ new Set();
2466
+ try {
2467
+ const list = await this.listApiKeys();
2468
+ existingNames = new Set(
2469
+ (list.keys ?? []).map((k) => String(k.keyName ?? "").trim())
2470
+ );
2471
+ } catch {
2472
+ }
2473
+ const name = pickUniqueKeyName(baseName, existingNames);
2474
+ const body = {
2475
+ teamId: this.teamId,
2476
+ name,
2477
+ serviceConfigurations: {
2478
+ [_PortalClient.APNS_SERVICE_ID]: []
2479
+ },
2480
+ serviceConfigurationsRequests: [
2481
+ {
2482
+ isNew: true,
2483
+ serviceId: _PortalClient.APNS_SERVICE_ID,
2484
+ identifiers: {},
2485
+ environment: "all",
2486
+ scope: "team"
2487
+ }
2488
+ ],
2489
+ scope: "team"
2490
+ };
2491
+ const result = await this.postJson("account/auth/key/v2/create", body);
2492
+ const created = result.body?.keys?.[0];
2493
+ if (!created?.keyId) {
2494
+ throw new Error(`createApnsAuthKey: no key id in response. Body: ${result.rawText.slice(0, 400)}`);
2495
+ }
2496
+ const pem = await this.downloadApiKey(created.keyId);
2497
+ return { keyId: created.keyId, pem, raw: created };
2498
+ }
2499
+ /**
2500
+ * Find existing APNs auth keys in the team. Used by the wizard to
2501
+ * detect "you already have one" cases and offer to reuse rather than
2502
+ * proliferate keys.
2503
+ */
2504
+ async listApnsAuthKeys() {
2505
+ const list = await this.listApiKeys();
2506
+ return (list.keys ?? []).filter(
2507
+ (k) => (k.services ?? []).some((s) => s.id === _PortalClient.APNS_SERVICE_ID || /apns/i.test(s.name ?? ""))
2508
+ ).map((k) => ({
2509
+ keyId: k.keyId,
2510
+ keyName: k.keyName,
2511
+ canDownload: Boolean(k.canDownload)
2512
+ }));
2513
+ }
2514
+ /**
2515
+ * Download an existing App Store Connect API key as PEM. Spaceship's
2516
+ * `download_key` endpoint — GET with teamId + keyId in query string.
2517
+ * Returns the raw .p8 PEM text. Apple shows the .p8 exactly once
2518
+ * (after creation), so call this immediately after createApiKey.
2519
+ */
2520
+ async downloadApiKey(keyId) {
2521
+ const { status, rawText, contentType } = await this.getRaw("account/auth/key/download", {
2522
+ teamId: this.teamId,
2523
+ keyId
2524
+ });
2525
+ if (status !== 200) {
2526
+ throw new Error(`downloadApiKey failed (${status}): ${rawText.slice(0, 200)}`);
2527
+ }
2528
+ if (rawText.includes("BEGIN PRIVATE KEY")) return rawText;
2529
+ if (contentType.includes("json")) {
2530
+ try {
2531
+ const parsed = JSON.parse(rawText);
2532
+ const candidates = [
2533
+ parsed?.privateKey,
2534
+ parsed?.key?.privateKey,
2535
+ parsed?.keys?.[0]?.privateKey,
2536
+ parsed?.data?.attributes?.privateKey,
2537
+ parsed?.value
2538
+ ];
2539
+ for (const c of candidates) {
2540
+ if (typeof c === "string" && c.includes("BEGIN PRIVATE KEY")) return c;
2541
+ }
2542
+ } catch {
2543
+ }
2544
+ }
2545
+ throw new Error(`downloadApiKey returned no PEM. Content-Type=${contentType} body=${rawText.slice(0, 300)}`);
2546
+ }
2547
+ /**
2548
+ * List existing API keys via the portal. Used to discover the team's
2549
+ * issuer id — every key in the team shares the same issuer id, and
2550
+ * altool needs it alongside the key id and .p8 file.
2551
+ *
2552
+ * Form-encoded POST (NOT JSON), with pagination params, per Spaceship's
2553
+ * `list_keys` method. JSON returned 415 from this endpoint.
2554
+ */
2555
+ async listApiKeys() {
2556
+ const data = await this.post("account/auth/key/list", {
2557
+ teamId: this.teamId,
2558
+ pageNumber: "1",
2559
+ pageSize: "500",
2560
+ sort: "name=asc"
2561
+ });
2562
+ const keys = data?.keys ?? [];
2563
+ const issuerId = data?.issuerId ?? data?.issuer ?? data?.team?.issuerId ?? keys[0]?.issuerId ?? keys[0]?.issuer;
2564
+ return { keys, issuerId };
2565
+ }
2267
2566
  async post(path, params) {
2268
2567
  const body = new URLSearchParams();
2269
2568
  for (const [k, v] of Object.entries(params)) {
@@ -2331,6 +2630,31 @@ var PortalClient = class _PortalClient {
2331
2630
  async deleteAppId(appIdId) {
2332
2631
  await this.post("account/ios/identifiers/deleteAppId.action", { teamId: this.teamId, appIdId });
2333
2632
  }
2633
+ /**
2634
+ * Toggle a capability (a.k.a. "service") on a bundle ID. Mirrors
2635
+ * Spaceship::Portal's `update_service` — the legacy portal endpoint
2636
+ * still works and is the only session-cookie-authenticated way to flip
2637
+ * capabilities like Push Notifications and Sign in with Apple from
2638
+ * outside the Apple Developer web UI.
2639
+ *
2640
+ * Common service codes (from Spaceship portal/portal_client.rb):
2641
+ * push → push notifications
2642
+ * APG3427HIY → Sign in with Apple
2643
+ * IAD53UNK2F → in-app purchase
2644
+ * homeKit → HomeKit
2645
+ * healthKit → HealthKit
2646
+ *
2647
+ * @param featureType Apple's internal capability key (string code or short name)
2648
+ * @param enabled true → turn on, false → turn off
2649
+ */
2650
+ async setBundleCapability(params) {
2651
+ await this.post("account/ios/identifiers/updateAppService.action", {
2652
+ teamId: this.teamId,
2653
+ displayId: params.appIdId,
2654
+ featureType: params.featureType,
2655
+ featureValue: params.enabled ? "on" : "off"
2656
+ });
2657
+ }
2334
2658
  // ── Certificates ───────────────────────────────────────────────────────
2335
2659
  async listCertificates(types) {
2336
2660
  const iosTypes = types ?? [
@@ -2348,12 +2672,21 @@ var PortalClient = class _PortalClient {
2348
2672
  return data.certRequests ?? [];
2349
2673
  }
2350
2674
  async listPushCertificates() {
2675
+ const types = Array.from(
2676
+ /* @__PURE__ */ new Set([
2677
+ _PortalClient.CERT_TYPES.IOS_PRODUCTION_PUSH,
2678
+ _PortalClient.CERT_TYPES.IOS_DEVELOPMENT_PUSH,
2679
+ // Legacy push codes from OLDER_IOS_CERTIFICATE_TYPES
2680
+ "3BQKVH9I2X",
2681
+ "BKLRAVXMGM"
2682
+ ])
2683
+ );
2351
2684
  const data = await this.post("account/ios/certificate/listCertRequests.action", {
2352
2685
  teamId: this.teamId,
2353
2686
  pageSize: "500",
2354
2687
  pageNumber: "1",
2355
2688
  sort: "certRequestStatusCode=asc",
2356
- types: [_PortalClient.CERT_TYPES.IOS_PRODUCTION_PUSH, _PortalClient.CERT_TYPES.IOS_DEVELOPMENT_PUSH].join(",")
2689
+ types: types.join(",")
2357
2690
  });
2358
2691
  return data.certRequests ?? [];
2359
2692
  }
@@ -2493,6 +2826,310 @@ var PortalClient = class _PortalClient {
2493
2826
  ];
2494
2827
  };
2495
2828
 
2829
+ // src/client/iris.ts
2830
+ var IRIS_BASE = "https://appstoreconnect.apple.com/iris/v1";
2831
+ var IrisClient = class {
2832
+ constructor(session) {
2833
+ this.session = session;
2834
+ this.cookieStr = Object.entries(session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
2835
+ }
2836
+ session;
2837
+ cookieStr;
2838
+ buildHeaders() {
2839
+ return {
2840
+ Cookie: this.cookieStr,
2841
+ Accept: "application/vnd.api+json, application/json, text/plain, */*",
2842
+ "Content-Type": "application/vnd.api+json",
2843
+ "X-Requested-With": "XMLHttpRequest",
2844
+ // Mimic the App Store Connect web UI's user agent
2845
+ "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"
2846
+ };
2847
+ }
2848
+ /**
2849
+ * Issue a request and absorb any new cookies (Apple rotates session
2850
+ * cookies on a lot of iris calls).
2851
+ */
2852
+ async request(method, path, body) {
2853
+ const headers = this.buildHeaders();
2854
+ if (process.env.FASTNODE_DEBUG) {
2855
+ console.log(`[iris] ${method} ${path}${body ? ` body=${JSON.stringify(body).slice(0, 200)}` : ""}`);
2856
+ }
2857
+ const res = await fetch(`${IRIS_BASE}${path}`, {
2858
+ method,
2859
+ headers,
2860
+ body: body ? JSON.stringify(body) : void 0
2861
+ });
2862
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
2863
+ const m = raw.match(/^([^=]+)=([^;]*)/);
2864
+ if (m) {
2865
+ this.session.cookies[m[1]] = m[2];
2866
+ }
2867
+ }
2868
+ this.cookieStr = Object.entries(this.session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
2869
+ const text = await res.text();
2870
+ let parsed = null;
2871
+ try {
2872
+ parsed = text ? JSON.parse(text) : null;
2873
+ } catch {
2874
+ }
2875
+ if (process.env.FASTNODE_DEBUG) {
2876
+ console.log(`[iris] \u2192 ${res.status} ${text.slice(0, 300)}`);
2877
+ }
2878
+ return { status: res.status, body: parsed, rawText: text };
2879
+ }
2880
+ /**
2881
+ * List existing API keys. Used both as a sanity check (the user has
2882
+ * permission) and to discover the team's issuer id (which is the same
2883
+ * for every key in the team and is needed by altool).
2884
+ *
2885
+ * Important: Apple's iris endpoint rejects most query parameters with
2886
+ * `PARAMETER_ERROR.INVALID`. Call with no params and let the API return
2887
+ * its default page.
2888
+ */
2889
+ async listApiKeys() {
2890
+ const { status, body, rawText } = await this.request("GET", "/apiKeys");
2891
+ if (status === 403) {
2892
+ throw new IrisAuthError(
2893
+ "You don't have permission to manage App Store Connect API keys (this requires the Account Holder or Admin role)."
2894
+ );
2895
+ }
2896
+ if (status >= 400 || !body) {
2897
+ throw new Error(`iris listApiKeys failed (${status}): ${rawText.slice(0, 200)}`);
2898
+ }
2899
+ const issuerId = body?.meta?.issuerId ?? body?.data?.[0]?.attributes?.issuerId;
2900
+ const keys = (body.data ?? []).map((d) => ({
2901
+ id: d.id,
2902
+ kid: d.attributes?.kid ?? d.id,
2903
+ nickname: d.attributes?.nickname ?? "",
2904
+ issuerId: d.attributes?.issuerId ?? issuerId,
2905
+ roles: d.attributes?.roles ?? [],
2906
+ allAppsVisible: d.attributes?.allAppsVisible ?? true
2907
+ }));
2908
+ return { keys, issuerId };
2909
+ }
2910
+ /**
2911
+ * Create a new API key. Apple returns the .p8 PEM exactly once in the
2912
+ * create response — capture it immediately or the key is dead weight.
2913
+ *
2914
+ * `keyType` is required by Apple's iris API:
2915
+ * • PUBLIC_API — the standard team-wide API key (what TestFlight uploads need)
2916
+ * • MARKETING_API — for App Analytics access only
2917
+ */
2918
+ async createApiKey(input) {
2919
+ const requestBody = {
2920
+ data: {
2921
+ type: "apiKeys",
2922
+ attributes: {
2923
+ nickname: input.nickname,
2924
+ roles: input.roles ?? ["APP_MANAGER"],
2925
+ allAppsVisible: input.allAppsVisible ?? true,
2926
+ keyType: input.keyType ?? "PUBLIC_API"
2927
+ }
2928
+ }
2929
+ };
2930
+ console.log("[iris] createApiKey request:", JSON.stringify(requestBody));
2931
+ const { status, body, rawText } = await this.request("POST", "/apiKeys", requestBody);
2932
+ console.log(`[iris] createApiKey response: ${status} ${rawText.slice(0, 800)}`);
2933
+ if (status === 403) {
2934
+ throw new IrisAuthError(
2935
+ "You don't have permission to create App Store Connect API keys (this requires the Account Holder or Admin role)."
2936
+ );
2937
+ }
2938
+ if (status >= 400 || !body) {
2939
+ const msg = body?.errors?.[0]?.detail ?? body?.errors?.[0]?.title ?? rawText.slice(0, 300) ?? `iris createApiKey failed (${status})`;
2940
+ throw new Error(msg);
2941
+ }
2942
+ const data = body.data ?? body;
2943
+ if (!data?.id) {
2944
+ throw new Error(`iris createApiKey returned no key id. Raw response: ${rawText.slice(0, 400)}`);
2945
+ }
2946
+ const key = {
2947
+ id: data.id,
2948
+ kid: data.attributes?.kid ?? data.id,
2949
+ nickname: data.attributes?.nickname ?? input.nickname,
2950
+ privateKey: data.attributes?.privateKey,
2951
+ issuerId: data.attributes?.issuerId,
2952
+ roles: data.attributes?.roles ?? input.roles ?? ["APP_MANAGER"],
2953
+ allAppsVisible: data.attributes?.allAppsVisible ?? true
2954
+ };
2955
+ return key;
2956
+ }
2957
+ /**
2958
+ * Fetch the .p8 PEM AND the team's issuer id for a previously-created
2959
+ * ASC API key. Both come back in a single request via:
2960
+ *
2961
+ * GET /iris/v1/apiKeys/{id}?fields[apiKeys]=privateKey&include=provider
2962
+ *
2963
+ * Sparse fieldset requesting ONLY `privateKey` is the magic that makes
2964
+ * Apple actually include the (base64-encoded) PEM in the response —
2965
+ * the default view returns `privateKey: null`. The `include=provider`
2966
+ * pulls the team's contentProvider record into `included[]`, and its
2967
+ * `id` field IS the issuer id altool needs (a UUID like
2968
+ * `cfadaf94-44ca-4c2c-8fa6-ecc28d3c7d0d`).
2969
+ *
2970
+ * Reverse-engineered from @expo/apple-utils v2.1.21 (the package that
2971
+ * powers EAS CLI), specifically the `ApiKey.downloadAsync` method
2972
+ * combined with `ApiKey.infoAsync` reading `provider?.id`.
2973
+ *
2974
+ * Returns `{ pem, issuerId }`, or `null` if Apple won't return the
2975
+ * private key (typically because the key was already downloaded once).
2976
+ */
2977
+ async fetchApiKeyPrivateKey(keyId) {
2978
+ try {
2979
+ const url = `${IRIS_BASE}/apiKeys/${keyId}?fields%5BapiKeys%5D=privateKey&include=provider`;
2980
+ const res = await fetch(url, {
2981
+ headers: {
2982
+ ...this.buildHeaders(),
2983
+ Accept: "application/vnd.api+json, application/json, text/plain, */*"
2984
+ }
2985
+ });
2986
+ const text = await res.text();
2987
+ console.log(`[iris] fetchApiKeyPrivateKey \u2192 ${res.status} (${text.length} bytes)`);
2988
+ if (res.status !== 200) {
2989
+ console.log(`[iris] err: ${text.slice(0, 300)}`);
2990
+ return null;
2991
+ }
2992
+ const body = JSON.parse(text);
2993
+ const encoded = body?.data?.attributes?.privateKey;
2994
+ if (typeof encoded !== "string" || encoded.length === 0) {
2995
+ console.log(`[iris] no privateKey in response`);
2996
+ return null;
2997
+ }
2998
+ const pem = Buffer.from(encoded, "base64").toString("utf-8");
2999
+ if (!pem.includes("BEGIN PRIVATE KEY")) {
3000
+ console.log(`[iris] decoded body didn't contain PEM marker`);
3001
+ return null;
3002
+ }
3003
+ const provider = (body?.included ?? []).find((i) => i.type === "contentProviders");
3004
+ const issuerId = provider?.id;
3005
+ console.log(`[iris] \u2713 PEM ${pem.length} bytes, issuerId=${issuerId ?? "(missing)"}`);
3006
+ return { pem, issuerId };
3007
+ } catch (err) {
3008
+ console.log(`[iris] fetchApiKeyPrivateKey error: ${err.message}`);
3009
+ return null;
3010
+ }
3011
+ }
3012
+ /**
3013
+ * @deprecated Kept as a fallback probe for debugging. The real path
3014
+ * is the sparse fieldset above — everything in this brute-force list
3015
+ * returned 404 during testing. Left here so we can re-verify if Apple
3016
+ * changes anything.
3017
+ */
3018
+ async fetchApiKeyPrivateKeyBruteForce(keyId) {
3019
+ const attempts = [
3020
+ // Sparse fieldsets — JSON-API spec lets us request specific attributes.
3021
+ // Apple may exclude privateKey by default and return it only when asked.
3022
+ { kind: "GET", path: `/apiKeys/${keyId}?fields[apiKeys]=privateKey,kid,issuerId,nickname,roles` },
3023
+ { kind: "GET", path: `/apiKeys/${keyId}?include=privateKey` },
3024
+ { kind: "GET", path: `/apiKeys/${keyId}?fields[apiKeys]=privateKey` },
3025
+ // Custom Accept headers — maybe Apple negotiates content type
3026
+ { kind: "GET", path: `/apiKeys/${keyId}`, accept: "application/x-pem-file" },
3027
+ { kind: "GET", path: `/apiKeys/${keyId}`, accept: "application/octet-stream" },
3028
+ { kind: "GET", path: `/apiKeys/${keyId}/p8`, accept: "application/x-pem-file" },
3029
+ // POST variants — Apple sometimes uses POST to mint a download token
3030
+ { kind: "POST", path: `/apiKeys/${keyId}/secrets`, body: {} },
3031
+ { kind: "POST", path: `/apiKeys/${keyId}/regenerate`, body: {} },
3032
+ { kind: "POST", path: `/apiKeys/${keyId}/p8`, body: {} },
3033
+ // Bare GET (last) — confirms canonical fields, in case my parser missed any
3034
+ { kind: "GET", path: `/apiKeys/${keyId}` },
3035
+ // Different host — the legacy developer.apple.com user-and-access namespace
3036
+ { kind: "ABSOLUTE", url: `https://appstoreconnect.apple.com/api/v1/apiKeys/${keyId}/p8` },
3037
+ { kind: "ABSOLUTE", url: `https://appstoreconnect.apple.com/access/api/keys/${keyId}/download` }
3038
+ ];
3039
+ for (const attempt of attempts) {
3040
+ try {
3041
+ let result;
3042
+ if (attempt.kind === "GET") {
3043
+ result = await this.requestWithHeaders("GET", attempt.path, void 0, {
3044
+ ...attempt.accept ? { Accept: attempt.accept } : {}
3045
+ });
3046
+ console.log(`[iris] fetchApiKeyPrivateKey GET ${attempt.path}${attempt.accept ? ` accept=${attempt.accept}` : ""} \u2192 ${result.status} (${result.rawText.length} bytes)`);
3047
+ } else if (attempt.kind === "POST") {
3048
+ result = await this.request("POST", attempt.path, attempt.body);
3049
+ console.log(`[iris] fetchApiKeyPrivateKey POST ${attempt.path} \u2192 ${result.status} (${result.rawText.length} bytes)`);
3050
+ } else {
3051
+ result = await this.requestAbsolute("GET", attempt.url);
3052
+ console.log(`[iris] fetchApiKeyPrivateKey GET ${attempt.url} \u2192 ${result.status} (${result.rawText.length} bytes)`);
3053
+ }
3054
+ if (result.status === 200) {
3055
+ console.log(`[iris] body: ${result.rawText.slice(0, 1500)}`);
3056
+ }
3057
+ if (result.status !== 200) continue;
3058
+ if (result.rawText && result.rawText.includes("BEGIN PRIVATE KEY")) {
3059
+ console.log(`[iris] \u2713 found PEM (plain text response)`);
3060
+ return result.rawText;
3061
+ }
3062
+ if (result.body) {
3063
+ const candidates = [
3064
+ result.body?.data?.attributes?.privateKey,
3065
+ result.body?.data?.attributes?.p8,
3066
+ result.body?.data?.attributes?.privateKeyContent,
3067
+ result.body?.data?.attributes?.kpem,
3068
+ result.body?.data?.attributes?.secret,
3069
+ result.body?.data?.attributes?.keyContent,
3070
+ result.body?.privateKey,
3071
+ result.body?.p8,
3072
+ result.body?.value,
3073
+ result.body?.secret
3074
+ ];
3075
+ for (const c of candidates) {
3076
+ if (typeof c === "string" && c.includes("BEGIN PRIVATE KEY")) {
3077
+ console.log(`[iris] \u2713 found PEM in JSON response`);
3078
+ return c;
3079
+ }
3080
+ }
3081
+ }
3082
+ } catch (err) {
3083
+ console.log(`[iris] fetchApiKeyPrivateKey attempt failed: ${err.message}`);
3084
+ }
3085
+ }
3086
+ console.log(`[iris] fetchApiKeyPrivateKey: no working download path for key ${keyId}`);
3087
+ return null;
3088
+ }
3089
+ /** Internal: like request() but lets us override headers. */
3090
+ async requestWithHeaders(method, path, body, extraHeaders = {}) {
3091
+ const headers = { ...this.buildHeaders(), ...extraHeaders };
3092
+ const res = await fetch(`${IRIS_BASE}${path}`, {
3093
+ method,
3094
+ headers,
3095
+ body: body ? JSON.stringify(body) : void 0
3096
+ });
3097
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
3098
+ const m = raw.match(/^([^=]+)=([^;]*)/);
3099
+ if (m) this.session.cookies[m[1]] = m[2];
3100
+ }
3101
+ this.cookieStr = Object.entries(this.session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
3102
+ const text = await res.text();
3103
+ let parsed = null;
3104
+ try {
3105
+ parsed = text ? JSON.parse(text) : null;
3106
+ } catch {
3107
+ }
3108
+ return { status: res.status, body: parsed, rawText: text };
3109
+ }
3110
+ /** Internal: fetch an absolute URL with the session cookies. */
3111
+ async requestAbsolute(method, url) {
3112
+ const headers = this.buildHeaders();
3113
+ const res = await fetch(url, { method, headers });
3114
+ const text = await res.text();
3115
+ let parsed = null;
3116
+ try {
3117
+ parsed = text ? JSON.parse(text) : null;
3118
+ } catch {
3119
+ }
3120
+ return { status: res.status, body: parsed, rawText: text };
3121
+ }
3122
+ async deleteApiKey(keyId) {
3123
+ await this.request("DELETE", `/apiKeys/${keyId}`);
3124
+ }
3125
+ };
3126
+ var IrisAuthError = class extends Error {
3127
+ constructor(message) {
3128
+ super(message);
3129
+ this.name = "IrisAuthError";
3130
+ }
3131
+ };
3132
+
2496
3133
  // src/client/index.ts
2497
3134
  var AppStoreConnectClient = class {
2498
3135
  http;
@@ -2581,12 +3218,17 @@ var AppStoreConnectClient = class {
2581
3218
 
2582
3219
  exports.AppStoreConnectClient = AppStoreConnectClient;
2583
3220
  exports.AppStoreConnectError = AppStoreConnectError;
3221
+ exports.IrisAuthError = IrisAuthError;
3222
+ exports.IrisClient = IrisClient;
2584
3223
  exports.PortalClient = PortalClient;
2585
3224
  exports.ProgramLicenseAgreementError = ProgramLicenseAgreementError;
2586
3225
  exports.RateLimitError = RateLimitError;
2587
3226
  exports.RequestClient = RequestClient;
2588
3227
  exports.TokenManager = TokenManager;
3228
+ exports.beginLogin = beginLogin;
2589
3229
  exports.checkPortalResponse = checkPortalResponse;
2590
3230
  exports.loginWithAppleId = loginWithAppleId;
3231
+ exports.requestSmsCode = requestSmsCode;
3232
+ exports.submitTwoFactorCode = submitTwoFactorCode;
2591
3233
  //# sourceMappingURL=index.cjs.map
2592
3234
  //# sourceMappingURL=index.cjs.map