@sitepong/fastnode 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +58 -1
  2. package/dist/actions/index.cjs +848 -54
  3. package/dist/actions/index.cjs.map +1 -1
  4. package/dist/actions/index.mjs +849 -55
  5. package/dist/actions/index.mjs.map +1 -1
  6. package/dist/apple-session-jUzBrql5.d.cts +122 -0
  7. package/dist/apple-session-jUzBrql5.d.ts +122 -0
  8. package/dist/cert/index.cjs +782 -736
  9. package/dist/cert/index.cjs.map +1 -1
  10. package/dist/cert/index.d.cts +5 -4
  11. package/dist/cert/index.d.ts +5 -4
  12. package/dist/cert/index.mjs +782 -737
  13. package/dist/cert/index.mjs.map +1 -1
  14. package/dist/{cert-Ddl7FpFT.d.ts → cert-RvXmdMke.d.ts} +2 -2
  15. package/dist/{cert-FehieKeb.d.cts → cert-eOoxGMN1.d.cts} +2 -2
  16. package/dist/cli/index.cjs +1321 -60
  17. package/dist/cli/index.cjs.map +1 -1
  18. package/dist/cli/index.d.cts +3 -2
  19. package/dist/cli/index.d.ts +3 -2
  20. package/dist/cli/index.mjs +1322 -61
  21. package/dist/cli/index.mjs.map +1 -1
  22. package/dist/client/index.cjs +443 -31
  23. package/dist/client/index.cjs.map +1 -1
  24. package/dist/client/index.d.cts +3 -2
  25. package/dist/client/index.d.ts +3 -2
  26. package/dist/client/index.mjs +443 -31
  27. package/dist/client/index.mjs.map +1 -1
  28. package/dist/deliver/index.cjs +334 -13
  29. package/dist/deliver/index.cjs.map +1 -1
  30. package/dist/deliver/index.d.cts +4 -3
  31. package/dist/deliver/index.d.ts +4 -3
  32. package/dist/deliver/index.mjs +330 -15
  33. package/dist/deliver/index.mjs.map +1 -1
  34. package/dist/{enums-B609j2rb.d.cts → enums-yXNVh3UI.d.cts} +8 -1
  35. package/dist/{enums-B609j2rb.d.ts → enums-yXNVh3UI.d.ts} +8 -1
  36. package/dist/gradle/index.cjs +222 -0
  37. package/dist/gradle/index.cjs.map +1 -0
  38. package/dist/gradle/index.d.cts +1 -0
  39. package/dist/gradle/index.d.ts +1 -0
  40. package/dist/gradle/index.mjs +217 -0
  41. package/dist/gradle/index.mjs.map +1 -0
  42. package/dist/gym/index.d.cts +2 -2
  43. package/dist/gym/index.d.ts +2 -2
  44. package/dist/index-8djTPW8O.d.ts +104 -0
  45. package/dist/{index-DrUpRXRG.d.cts → index-B6-MJpO4.d.cts} +1 -1
  46. package/dist/{index-D13sn8DO.d.cts → index-BE7bwNff.d.cts} +1 -1
  47. package/dist/index-BGhV0gS-.d.ts +65 -0
  48. package/dist/index-BaNcf7jt.d.cts +201 -0
  49. package/dist/{index-DZoPGCbE.d.ts → index-BvyZ4uiw.d.ts} +3 -3
  50. package/dist/{index-B8FOfxOw.d.ts → index-C01Rkrir.d.ts} +2 -2
  51. package/dist/{index-YcKz4aRS.d.ts → index-C9mHBJED.d.ts} +1 -1
  52. package/dist/{index-BICa8odo.d.cts → index-CAEiXVWd.d.ts} +9 -1
  53. package/dist/index-CBf0-NUQ.d.ts +201 -0
  54. package/dist/index-CXJlvmB4.d.cts +63 -0
  55. package/dist/index-CXJlvmB4.d.ts +63 -0
  56. package/dist/{index-CNHf7I4v.d.cts → index-D1GHEM3c.d.ts} +32 -3
  57. package/dist/index-DFstwgGK.d.cts +65 -0
  58. package/dist/{index-Bk1Gd-qE.d.cts → index-DGkgRnZp.d.cts} +2 -2
  59. package/dist/{index-CplRWSUc.d.ts → index-DLJso7Vs.d.cts} +32 -3
  60. package/dist/{index-BZWmOTWE.d.cts → index-DWOLZNlz.d.cts} +2 -2
  61. package/dist/{index-OelCoOth.d.cts → index-DfkIz7i0.d.cts} +18 -4
  62. package/dist/{index-CtPyeMLH.d.ts → index-DjC_gmnL.d.ts} +18 -4
  63. package/dist/index-DnHQ2uT2.d.cts +104 -0
  64. package/dist/{index-CCtYhPN9.d.ts → index-DrmS802s.d.cts} +331 -130
  65. package/dist/index-Dv-Mek07.d.cts +84 -0
  66. package/dist/index-Dv-Mek07.d.ts +84 -0
  67. package/dist/{index-oVyLLIEK.d.cts → index-icIH5e5V.d.cts} +3 -3
  68. package/dist/{index-DI1ZHr-3.d.ts → index-laE3_wm9.d.cts} +9 -1
  69. package/dist/{index-BKUapQP2.d.cts → index-lsuS2R1T.d.ts} +331 -130
  70. package/dist/{index-DImN9PWc.d.ts → index-mEEB7An6.d.ts} +1 -1
  71. package/dist/{index-DFni8oZt.d.ts → index-rTZOdMRE.d.ts} +2 -2
  72. package/dist/index.cjs +3342 -959
  73. package/dist/index.cjs.map +1 -1
  74. package/dist/index.d.cts +237 -14
  75. package/dist/index.d.ts +237 -14
  76. package/dist/index.mjs +3335 -963
  77. package/dist/index.mjs.map +1 -1
  78. package/dist/lane/index.cjs +848 -54
  79. package/dist/lane/index.cjs.map +1 -1
  80. package/dist/lane/index.mjs +849 -55
  81. package/dist/lane/index.mjs.map +1 -1
  82. package/dist/match/index.d.cts +6 -5
  83. package/dist/match/index.d.ts +6 -5
  84. package/dist/pem/index.d.cts +4 -3
  85. package/dist/pem/index.d.ts +4 -3
  86. package/dist/pilot/index.cjs +8 -4
  87. package/dist/pilot/index.cjs.map +1 -1
  88. package/dist/pilot/index.d.cts +4 -3
  89. package/dist/pilot/index.d.ts +4 -3
  90. package/dist/pilot/index.mjs +10 -6
  91. package/dist/pilot/index.mjs.map +1 -1
  92. package/dist/playstore/index.cjs +207 -0
  93. package/dist/playstore/index.cjs.map +1 -0
  94. package/dist/playstore/index.d.cts +1 -0
  95. package/dist/playstore/index.d.ts +1 -0
  96. package/dist/playstore/index.mjs +203 -0
  97. package/dist/playstore/index.mjs.map +1 -0
  98. package/dist/precheck/index.d.cts +4 -3
  99. package/dist/precheck/index.d.ts +4 -3
  100. package/dist/privacy/index.cjs +207 -0
  101. package/dist/privacy/index.cjs.map +1 -0
  102. package/dist/privacy/index.d.cts +2 -0
  103. package/dist/privacy/index.d.ts +2 -0
  104. package/dist/privacy/index.mjs +195 -0
  105. package/dist/privacy/index.mjs.map +1 -0
  106. package/dist/produce/index.cjs +32 -0
  107. package/dist/produce/index.cjs.map +1 -1
  108. package/dist/produce/index.d.cts +4 -3
  109. package/dist/produce/index.d.ts +4 -3
  110. package/dist/produce/index.mjs +32 -1
  111. package/dist/produce/index.mjs.map +1 -1
  112. package/dist/review/index.cjs +141 -0
  113. package/dist/review/index.cjs.map +1 -0
  114. package/dist/review/index.d.cts +2 -0
  115. package/dist/review/index.d.ts +2 -0
  116. package/dist/review/index.mjs +137 -0
  117. package/dist/review/index.mjs.map +1 -0
  118. package/dist/sigh/index.d.cts +4 -3
  119. package/dist/sigh/index.d.ts +4 -3
  120. package/dist/xcode/index.cjs +1 -1
  121. package/dist/xcode/index.cjs.map +1 -1
  122. package/dist/xcode/index.mjs +1 -1
  123. package/dist/xcode/index.mjs.map +1 -1
  124. package/package.json +27 -1
  125. package/dist/index-B9QeTpQK.d.cts +0 -99
  126. package/dist/index-xykpq6Lz.d.ts +0 -99
@@ -2,7 +2,7 @@ import * as x509 from '@peculiar/x509';
2
2
  import { randomUUID, createHash, webcrypto, randomBytes, pbkdf2Sync, createCipheriv, createDecipheriv } from 'crypto';
3
3
  import { spawn, execFile, execFileSync } from 'child_process';
4
4
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, copyFileSync, mkdtempSync, rmSync, chmodSync } from 'fs';
5
- import { join, extname, resolve, basename } from 'path';
5
+ import { join, extname, resolve, basename, relative } from 'path';
6
6
  import { homedir, tmpdir } from 'os';
7
7
  import { promisify } from 'util';
8
8
  import { mkdir, writeFile, readFile, readdir, unlink, stat, rm, mkdtemp } from 'fs/promises';
@@ -12,6 +12,7 @@ import { createInterface } from 'readline';
12
12
  import emojiRegex from 'emoji-regex';
13
13
  import { importPKCS8, SignJWT } from 'jose';
14
14
  import { Command } from 'commander';
15
+ import { Writable } from 'stream';
15
16
 
16
17
  var __defProp = Object.defineProperty;
17
18
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -184,8 +185,8 @@ async function revokeExpiredCertificates(options) {
184
185
  return count;
185
186
  }
186
187
  async function importToKeychain(certData, keychainPath, keychainPassword) {
187
- const { tmpdir: tmpdir8 } = await import('os');
188
- const tempCertPath = join(tmpdir8(), `fastnode-cert-${Date.now()}.cer`);
188
+ const { tmpdir: tmpdir7 } = await import('os');
189
+ const tempCertPath = join(tmpdir7(), `fastnode-cert-${Date.now()}.cer`);
189
190
  await writeFile(tempCertPath, certData);
190
191
  try {
191
192
  const args = [
@@ -1754,6 +1755,34 @@ var init_srp = __esm({
1754
1755
  };
1755
1756
  }
1756
1757
  });
1758
+
1759
+ // src/client/apple-session.ts
1760
+ var apple_session_exports = {};
1761
+ __export(apple_session_exports, {
1762
+ ProgramLicenseAgreementError: () => ProgramLicenseAgreementError,
1763
+ appleJson: () => appleJson,
1764
+ beginLogin: () => beginLogin,
1765
+ checkPortalResponse: () => checkPortalResponse,
1766
+ fetchServiceKey: () => fetchServiceKey,
1767
+ loginWithAppleId: () => loginWithAppleId,
1768
+ requestSmsCode: () => requestSmsCode,
1769
+ serviceKeyFromLocation: () => serviceKeyFromLocation,
1770
+ submitTwoFactorCode: () => submitTwoFactorCode
1771
+ });
1772
+ async function appleJson(res, what) {
1773
+ const text = await res.text();
1774
+ try {
1775
+ return JSON.parse(text);
1776
+ } catch {
1777
+ const contentType = (res.headers.get("content-type") ?? "").split(";")[0].trim();
1778
+ const title = /<title>([^<]{1,120})<\/title>/i.exec(text)?.[1]?.trim();
1779
+ const looksHtml = /^\s*<(!doctype|html)/i.test(text) || contentType === "text/html";
1780
+ const page = looksHtml ? "an HTML page" : contentType ? `a ${contentType} response` : "a non-JSON response";
1781
+ throw new Error(
1782
+ `Apple answered the ${what} with ${page} (HTTP ${res.status}${title ? `, "${title}"` : ""}) instead of JSON. Apple is refusing or rate-limiting sign-ins from this server right now; wait a few minutes and try again. If it keeps happening, sign in once at appleid.apple.com to clear any hold on the account.`
1783
+ );
1784
+ }
1785
+ }
1757
1786
  function checkPortalResponse(body, session) {
1758
1787
  if (!body || typeof body !== "object") return;
1759
1788
  if (body.resultCode === 4100) {
@@ -1830,7 +1859,7 @@ async function beginLogin(config) {
1830
1859
  }
1831
1860
  async function fetchTwoFactorChallenge(pending) {
1832
1861
  const r = await fetch(`${AUTH_BASE}`, { headers: buildAuthHeadersFromPending(pending) });
1833
- const body = await r.json();
1862
+ const body = await appleJson(r, "two-factor challenge");
1834
1863
  const trustedPhoneNumbers = (body.trustedPhoneNumbers ?? []).map((p) => ({
1835
1864
  id: p.id,
1836
1865
  numberWithDialCode: p.numberWithDialCode,
@@ -1851,7 +1880,8 @@ async function requestSmsCode(input) {
1851
1880
  body: JSON.stringify({ phoneNumber: { id: input.phoneId }, mode: input.mode ?? "sms" })
1852
1881
  });
1853
1882
  }
1854
- async function submitTwoFactorCode(input) {
1883
+ async function submitTwoFactorCode(rawInput) {
1884
+ const input = { ...rawInput, code: rawInput.code.replace(/[\s\u00a0]+/g, "") };
1855
1885
  const log = input.onLog ?? (() => {
1856
1886
  });
1857
1887
  let path;
@@ -1963,9 +1993,12 @@ async function doSirp(user, password, serviceKey, cookieStr, hashcash) {
1963
1993
  body: JSON.stringify({ a: aBase64, accountName: user, protocols: ["s2k", "s2k_fo"] }),
1964
1994
  redirect: "manual"
1965
1995
  });
1966
- const initBody = await initRes.json();
1996
+ const initBody = await appleJson(initRes, "sign-in init");
1967
1997
  if (initBody.serviceErrors) throw new Error(`Apple auth error: ${JSON.stringify(initBody.serviceErrors)}`);
1968
1998
  const { iteration: iterations, salt: saltB64, b: bB64, c, protocol } = initBody;
1999
+ if (!iterations || !saltB64 || !bB64 || !c || !protocol) {
2000
+ throw new Error(`Apple's sign-in init answered without a challenge (HTTP ${initRes.status}): ${JSON.stringify(initBody).slice(0, 300)}`);
2001
+ }
1969
2002
  const salt = Buffer.from(saltB64, "base64");
1970
2003
  const b = Buffer.from(bB64, "base64");
1971
2004
  const encryptedPassword = applePbkdf2(password, salt, iterations, 32, protocol);
@@ -2008,7 +2041,7 @@ async function fetchOlympusSession(session) {
2008
2041
  const cookieStr = Object.entries(session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
2009
2042
  const res = await fetch(OLYMPUS_SESSION, { headers: { Cookie: cookieStr, Accept: "application/json" } });
2010
2043
  if (res.ok) {
2011
- const body = await res.json();
2044
+ const body = await appleJson(res, "session lookup");
2012
2045
  if (body?.user?.emailAddress) session.userEmail = body.user.emailAddress;
2013
2046
  if (body?.provider?.providerId) session.teamId = String(body.provider.providerId);
2014
2047
  }
@@ -2020,7 +2053,7 @@ async function fetchContractMessages(session) {
2020
2053
  headers: { Cookie: cookieStr, Accept: "application/json" }
2021
2054
  });
2022
2055
  if (!res.ok) return [];
2023
- const body = await res.json();
2056
+ const body = await appleJson(res, "agreements lookup");
2024
2057
  return (body ?? []).map((m) => ({
2025
2058
  id: m.id ?? "",
2026
2059
  group: m.group ?? "",
@@ -2031,10 +2064,22 @@ async function fetchContractMessages(session) {
2031
2064
  return [];
2032
2065
  }
2033
2066
  }
2034
- async function fetchServiceKey() {
2035
- const res = await fetch(WIDGET_KEY_URL);
2036
- const body = await res.json();
2037
- if (!body?.authServiceKey) throw new Error("Could not fetch Apple service key");
2067
+ function serviceKeyFromLocation(location) {
2068
+ if (!location) return null;
2069
+ try {
2070
+ const key = new URL(location).searchParams.get("widgetKey")?.trim();
2071
+ return key && /^[0-9a-f]{16,}$/i.test(key) ? key : null;
2072
+ } catch {
2073
+ return null;
2074
+ }
2075
+ }
2076
+ async function fetchServiceKey(fetchImpl = fetch) {
2077
+ const head = await fetchImpl(LOGOUT_URL, { method: "HEAD", redirect: "manual" });
2078
+ const fromRedirect = serviceKeyFromLocation(head.headers.get("location"));
2079
+ if (fromRedirect) return fromRedirect;
2080
+ const res = await fetchImpl(WIDGET_KEY_URL);
2081
+ const body = await appleJson(res, "service key request");
2082
+ if (!body?.authServiceKey) throw new Error("Could not fetch Apple service key: the sign-out redirect carried no widgetKey and the config endpoint had no authServiceKey.");
2038
2083
  return body.authServiceKey;
2039
2084
  }
2040
2085
  function buildAuthHeaders(session) {
@@ -2082,7 +2127,7 @@ async function defaultPrompt(message) {
2082
2127
  });
2083
2128
  });
2084
2129
  }
2085
- var AUTH_BASE, OLYMPUS_SESSION, WIDGET_KEY_URL, ProgramLicenseAgreementError;
2130
+ var AUTH_BASE, OLYMPUS_SESSION, WIDGET_KEY_URL, ProgramLicenseAgreementError, LOGOUT_URL;
2086
2131
  var init_apple_session = __esm({
2087
2132
  "src/client/apple-session.ts"() {
2088
2133
  init_srp();
@@ -2097,6 +2142,7 @@ var init_apple_session = __esm({
2097
2142
  }
2098
2143
  agreements;
2099
2144
  };
2145
+ LOGOUT_URL = "https://appstoreconnect.apple.com/logout";
2100
2146
  }
2101
2147
  });
2102
2148
 
@@ -2413,9 +2459,10 @@ async function uploadWithTransporter(options) {
2413
2459
  const transporterBin = await resolveTransporter();
2414
2460
  const platform = options.platform ?? "ios";
2415
2461
  const timeout = options.timeout ?? 18e5;
2416
- const keyDir = await mkdtemp(join(tmpdir(), "fastnode-itms-"));
2462
+ const keyDir = join(homedir(), ".appstoreconnect", "private_keys");
2463
+ await mkdir(keyDir, { recursive: true });
2417
2464
  const keyPath = join(keyDir, `AuthKey_${options.apiKeyId}.p8`);
2418
- await writeFile(keyPath, options.apiPrivateKey);
2465
+ await writeFile(keyPath, options.apiPrivateKey, { mode: 384 });
2419
2466
  return new Promise((resolve4) => {
2420
2467
  const args = [
2421
2468
  "-m",
@@ -2431,6 +2478,9 @@ async function uploadWithTransporter(options) {
2431
2478
  "-WONoPause",
2432
2479
  "true"
2433
2480
  ];
2481
+ if (options.assetDescriptionPath) {
2482
+ args.push("-assetDescription", options.assetDescriptionPath);
2483
+ }
2434
2484
  if (platform !== "ios") {
2435
2485
  args.push("-bundleType", platform);
2436
2486
  }
@@ -2454,13 +2504,13 @@ async function uploadWithTransporter(options) {
2454
2504
  child.stderr.on("data", onData);
2455
2505
  child.on("close", async (code) => {
2456
2506
  clearTimeout(timer);
2457
- await rm(keyDir, { recursive: true, force: true }).catch(() => {
2507
+ await rm(keyPath, { force: true }).catch(() => {
2458
2508
  });
2459
2509
  resolve4({ success: code === 0, output, backend: "transporter" });
2460
2510
  });
2461
2511
  child.on("error", async (err) => {
2462
2512
  clearTimeout(timer);
2463
- await rm(keyDir, { recursive: true, force: true }).catch(() => {
2513
+ await rm(keyPath, { force: true }).catch(() => {
2464
2514
  });
2465
2515
  resolve4({
2466
2516
  success: false,
@@ -2752,6 +2802,311 @@ var init_metadata = __esm({
2752
2802
  };
2753
2803
  }
2754
2804
  });
2805
+ function categoryFor(placementType) {
2806
+ return placementType === "APP_SCREENSHOT" || placementType === "IMESSAGE_APP_SCREENSHOT" || placementType === "APP_PREVIEW" ? "APP_SCREENSHOTS_AND_PREVIEWS" : "CREATIVE_ASSETS";
2807
+ }
2808
+ function readImageSize(data) {
2809
+ if (data.length >= 26 && data.readUInt32BE(0) === 2303741511) {
2810
+ const colorType = data[25];
2811
+ return {
2812
+ width: data.readUInt32BE(16),
2813
+ height: data.readUInt32BE(20),
2814
+ alpha: colorType === 4 || colorType === 6
2815
+ };
2816
+ }
2817
+ if (data.length >= 4 && data[0] === 255 && data[1] === 216) {
2818
+ let offset = 2;
2819
+ while (offset + 9 < data.length) {
2820
+ if (data[offset] !== 255) return null;
2821
+ const marker = data[offset + 1];
2822
+ const length = data.readUInt16BE(offset + 2);
2823
+ const isSof = marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204;
2824
+ if (isSof) {
2825
+ return { height: data.readUInt16BE(offset + 5), width: data.readUInt16BE(offset + 7), alpha: false };
2826
+ }
2827
+ offset += 2 + length;
2828
+ }
2829
+ }
2830
+ return null;
2831
+ }
2832
+ function fits(spec, width, height) {
2833
+ const d = spec.dimensions;
2834
+ return width >= d.minWidth && width <= d.maxWidth && height >= d.minHeight && height <= d.maxHeight;
2835
+ }
2836
+ function groupsForFeature(refData, feature, placementType) {
2837
+ const mappings = refData.placementTypes?.find((p) => p.placementTypeId === placementType)?.specMappings ?? [];
2838
+ const all = mappings.map((m) => m.placementGroupId);
2839
+ const policy = refData.features?.find((f) => f.featureId === feature)?.placementPolicies?.find((p) => p.placementType === placementType);
2840
+ const limited = policy?.groupLimits?.flatMap((l) => l.groupIds);
2841
+ return limited?.length ? all.filter((g) => limited.includes(g)) : all;
2842
+ }
2843
+ function maxCountFor(refData, feature, placementType, placementGroup) {
2844
+ return refData.features?.find((f) => f.featureId === feature)?.placementPolicies?.find((p) => p.placementType === placementType)?.groupLimits?.find((l) => l.groupIds.includes(placementGroup))?.maxCount;
2845
+ }
2846
+ function resolveImageGroups(refData, feature, placementType, size) {
2847
+ const specs = (refData.imageSpecs ?? []).filter(
2848
+ (s) => s.compatiblePlacementTypes.includes(placementType) && fits(s, size.width, size.height)
2849
+ );
2850
+ const specIds = new Set(specs.map((s) => s.specId));
2851
+ const allowed = new Set(groupsForFeature(refData, feature, placementType));
2852
+ const mappings = refData.placementTypes?.find((p) => p.placementTypeId === placementType)?.specMappings ?? [];
2853
+ const groups = mappings.filter((m) => allowed.has(m.placementGroupId) && m.specs.some((id) => specIds.has(id))).map((m) => m.placementGroupId);
2854
+ return { groups: [...new Set(groups)], spec: specs[0] };
2855
+ }
2856
+ async function runUploadOperations(ops, data) {
2857
+ for (const op of ops) {
2858
+ const headers = {};
2859
+ for (const h of op.requestHeaders) headers[h.name] = h.value;
2860
+ const res = await fetch(op.url, {
2861
+ method: op.method,
2862
+ headers,
2863
+ body: data.subarray(op.offset, op.offset + op.length)
2864
+ });
2865
+ if (!res.ok) {
2866
+ throw new Error(`Upload operation failed: ${op.method} ${new URL(op.url).host} \u2192 HTTP ${res.status}`);
2867
+ }
2868
+ }
2869
+ }
2870
+ async function uploadLibraryAsset(options) {
2871
+ const { client, libraryId, filePath, category } = options;
2872
+ const data = await readFile(filePath);
2873
+ const fileName = basename(filePath);
2874
+ const kind = VIDEO_EXTENSIONS.includes(extname(filePath).toLowerCase()) ? "video" : "image";
2875
+ const hash = createHash("sha256").update(data).digest("hex").slice(0, 12);
2876
+ const referenceName = `${options.referenceName ?? fileName} #${hash}`;
2877
+ const media = kind === "image" ? client.appAssetLibraryImages : client.appAssetLibraryVideos;
2878
+ const filter = { filter: { referenceName } };
2879
+ const existing = kind === "image" ? await client.appAssetLibraries.listImages(libraryId, filter) : await client.appAssetLibraries.listVideos(libraryId, filter);
2880
+ const reusable = existing.find(
2881
+ (a) => a.attributes.category === category && a.attributes.state && READY_STATES.includes(a.attributes.state)
2882
+ );
2883
+ if (reusable) {
2884
+ return { kind, id: reusable.id, state: reusable.attributes.state, reused: true };
2885
+ }
2886
+ const reserved = kind === "image" ? await client.appAssetLibraryImages.create({ libraryId, fileName, fileSize: data.length, category, referenceName }) : await client.appAssetLibraryVideos.create({
2887
+ libraryId,
2888
+ fileName,
2889
+ fileSize: data.length,
2890
+ category,
2891
+ referenceName,
2892
+ previewFrameTimeCode: options.previewFrameTimeCode
2893
+ });
2894
+ await runUploadOperations(reserved.attributes.uploadOperations ?? [], data);
2895
+ await media.update(reserved.id, { uploaded: true });
2896
+ options.log?.(`uploaded ${fileName} (${kind}, ${data.length} bytes)`);
2897
+ const state = await waitForAssetReady(
2898
+ () => media.get(reserved.id),
2899
+ options.pollIntervalMs ?? 3e3,
2900
+ options.timeoutMs ?? (kind === "video" ? 30 * 6e4 : 5 * 6e4)
2901
+ );
2902
+ return { kind, id: reserved.id, state, reused: false };
2903
+ }
2904
+ async function waitForAssetReady(get, intervalMs, timeoutMs) {
2905
+ const deadline = Date.now() + timeoutMs;
2906
+ for (; ; ) {
2907
+ const asset = await get();
2908
+ const state = asset.attributes.state;
2909
+ if (state === "FAILED") {
2910
+ const why = (asset.attributes.stateDetails ?? []).map((d) => d.message ?? d.code).filter(Boolean).join("; ");
2911
+ throw new Error(`Asset processing failed${why ? `: ${why}` : ""}`);
2912
+ }
2913
+ if (state && READY_STATES.includes(state)) return state;
2914
+ if (Date.now() > deadline) throw new Error(`Asset still ${state ?? "unknown"} after ${timeoutMs / 1e3}s`);
2915
+ await new Promise((r) => setTimeout(r, intervalMs));
2916
+ }
2917
+ }
2918
+ async function listFiles(dir) {
2919
+ return (await readdir(dir)).filter((f) => !f.startsWith(".")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
2920
+ }
2921
+ async function isDir(path) {
2922
+ return (await stat(path)).isDirectory();
2923
+ }
2924
+ async function planLocale(localeDir, refData, feature, folders, warn) {
2925
+ const planned = [];
2926
+ for (const [folder, placementType] of Object.entries(folders)) {
2927
+ const typeDir = join(localeDir, folder);
2928
+ if (!existsSync(typeDir)) continue;
2929
+ const known = new Set(groupsForFeature(refData, feature, placementType));
2930
+ const entries = [];
2931
+ for (const entry of await listFiles(typeDir)) {
2932
+ const full = join(typeDir, entry);
2933
+ if (await isDir(full)) {
2934
+ if (!known.has(entry)) {
2935
+ warn(`${relative(localeDir, full)}: "${entry}" is not a ${placementType} placement group for ${feature} \u2014 skipped`);
2936
+ continue;
2937
+ }
2938
+ for (const f of await listFiles(full)) entries.push({ filePath: join(full, f), group: entry });
2939
+ } else {
2940
+ entries.push({ filePath: full });
2941
+ }
2942
+ }
2943
+ for (const { filePath, group } of entries) {
2944
+ const ext = extname(filePath).toLowerCase();
2945
+ const isVideo = VIDEO_EXTENSIONS.includes(ext);
2946
+ if (!isVideo && !IMAGE_EXTENSIONS.includes(ext)) continue;
2947
+ const needsVideo = placementType === "APP_PREVIEW";
2948
+ const needsImage = placementType === "APP_SCREENSHOT" || placementType === "IMESSAGE_APP_SCREENSHOT";
2949
+ if (needsVideo && !isVideo || needsImage && isVideo) {
2950
+ warn(`${relative(localeDir, filePath)}: ${isVideo ? "video" : "image"} cannot be a ${placementType} \u2014 skipped`);
2951
+ continue;
2952
+ }
2953
+ if (group) {
2954
+ planned.push({ filePath, placementType, groups: [group] });
2955
+ continue;
2956
+ }
2957
+ if (isVideo) {
2958
+ if (known.size === 1) planned.push({ filePath, placementType, groups: [...known] });
2959
+ else warn(`${relative(localeDir, filePath)}: put videos in a placement-group folder (one of ${[...known].join(", ")}) \u2014 skipped`);
2960
+ continue;
2961
+ }
2962
+ const size = readImageSize(await readFile(filePath));
2963
+ if (!size) {
2964
+ warn(`${relative(localeDir, filePath)}: could not read image size \u2014 skipped`);
2965
+ continue;
2966
+ }
2967
+ const { groups, spec } = resolveImageGroups(refData, feature, placementType, size);
2968
+ if (!groups.length) {
2969
+ warn(`${relative(localeDir, filePath)}: ${size.width}\xD7${size.height} matches no ${placementType} spec \u2014 skipped`);
2970
+ continue;
2971
+ }
2972
+ if (size.alpha && spec && spec.alphaAllowed === false) {
2973
+ warn(`${relative(localeDir, filePath)}: has an alpha channel, which Apple rejects for this spec`);
2974
+ }
2975
+ planned.push({ filePath, placementType, groups: groups.slice(0, 1) });
2976
+ }
2977
+ }
2978
+ return planned;
2979
+ }
2980
+ async function uploadAssetLibrary(options) {
2981
+ const { client } = options;
2982
+ const log = options.log ?? (() => {
2983
+ });
2984
+ const feature = "APP_STORE_VERSIONS";
2985
+ const result = { uploaded: 0, reused: 0, placed: 0, removed: 0, warnings: [] };
2986
+ const warn = (line) => {
2987
+ result.warnings.push(line);
2988
+ log(`warning: ${line}`);
2989
+ };
2990
+ const [library, refData, localizations] = await Promise.all([
2991
+ client.appAssetLibraries.getForApp(options.appId),
2992
+ client.appAssetLibraries.refData({ features: [feature] }),
2993
+ client.appStoreVersions.getLocalizations(options.versionId)
2994
+ ]);
2995
+ const folders = { ...options.folders ?? ASSET_FOLDERS };
2996
+ if (options.rootPlacementType) folders["."] = options.rootPlacementType;
2997
+ const uploads = /* @__PURE__ */ new Map();
2998
+ const uploadOnce = (filePath, placementType) => {
2999
+ const key = `${filePath}|${categoryFor(placementType)}`;
3000
+ if (!uploads.has(key)) {
3001
+ uploads.set(
3002
+ key,
3003
+ uploadLibraryAsset({
3004
+ client,
3005
+ libraryId: library.id,
3006
+ filePath,
3007
+ category: categoryFor(placementType),
3008
+ referenceName: relative(options.assetsPath, filePath),
3009
+ log
3010
+ }).then((a) => {
3011
+ if (a.reused) result.reused++;
3012
+ else result.uploaded++;
3013
+ return a;
3014
+ })
3015
+ );
3016
+ }
3017
+ return uploads.get(key);
3018
+ };
3019
+ for (const loc of localizations) {
3020
+ const localeDir = join(options.assetsPath, loc.attributes.locale);
3021
+ if (!existsSync(localeDir)) continue;
3022
+ const target = { kind: "appStoreVersionLocalization", id: loc.id };
3023
+ const plan = await planLocale(localeDir, refData, feature, folders, warn);
3024
+ const slots = /* @__PURE__ */ new Map();
3025
+ for (const p of plan) {
3026
+ for (const group of p.groups) {
3027
+ const key = `${p.placementType}|${group}`;
3028
+ if (!slots.has(key)) slots.set(key, { placementType: p.placementType, group, files: [] });
3029
+ slots.get(key).files.push(p.filePath);
3030
+ }
3031
+ }
3032
+ for (const slot of slots.values()) {
3033
+ const where = `${loc.attributes.locale} ${slot.placementType} ${slot.group}`;
3034
+ const max = maxCountFor(refData, feature, slot.placementType, slot.group);
3035
+ if (max !== void 0 && slot.files.length > max) {
3036
+ warn(`${where}: ${slot.files.length} files but Apple allows ${max} \u2014 keeping the first ${max}`);
3037
+ slot.files = slot.files.slice(0, max);
3038
+ }
3039
+ const existing = await client.appAssetLibraryPlacements.listForTarget(target, {
3040
+ placementType: slot.placementType,
3041
+ placementGroup: slot.group,
3042
+ include: "image,video"
3043
+ });
3044
+ if (options.overwrite) {
3045
+ for (const p of existing) {
3046
+ await client.appAssetLibraryPlacements.delete(p.id);
3047
+ result.removed++;
3048
+ }
3049
+ }
3050
+ const kept = options.overwrite ? [] : existing;
3051
+ const keptAssetIds = new Set(kept.map((p) => placementAssetId(p)).filter(Boolean));
3052
+ const ours = [];
3053
+ for (const filePath of slot.files) {
3054
+ const asset = await uploadOnce(filePath, slot.placementType);
3055
+ if (keptAssetIds.has(asset.id)) continue;
3056
+ const placement = await client.appAssetLibraryPlacements.create({
3057
+ placementType: slot.placementType,
3058
+ placementGroup: slot.group,
3059
+ ...asset.kind === "image" ? { imageId: asset.id } : { videoId: asset.id },
3060
+ target
3061
+ });
3062
+ ours.push(placement.id);
3063
+ result.placed++;
3064
+ }
3065
+ log(`${where}: placed ${ours.length}${kept.length ? `, kept ${kept.length}` : ""}`);
3066
+ const orderable = slot.placementType === "APP_SCREENSHOT" || slot.placementType === "APP_PREVIEW" || slot.placementType === "IMESSAGE_APP_SCREENSHOT";
3067
+ if (orderable && ours.length + kept.length > 1) {
3068
+ await client.appAssetLibraryPlacements.order({
3069
+ placementGroup: slot.group,
3070
+ placementIds: [...ours, ...kept.map((p) => p.id)],
3071
+ target
3072
+ });
3073
+ }
3074
+ }
3075
+ }
3076
+ return result;
3077
+ }
3078
+ function placementAssetId(p) {
3079
+ for (const rel of ["image", "video"]) {
3080
+ const data = p.relationships?.[rel]?.data;
3081
+ if (data?.id) return data.id;
3082
+ }
3083
+ return void 0;
3084
+ }
3085
+ var ASSET_FOLDERS, IMAGE_EXTENSIONS, VIDEO_EXTENSIONS, READY_STATES;
3086
+ var init_assets = __esm({
3087
+ "src/deliver/assets.ts"() {
3088
+ ASSET_FOLDERS = {
3089
+ screenshots: "APP_SCREENSHOT",
3090
+ imessage_screenshots: "IMESSAGE_APP_SCREENSHOT",
3091
+ previews: "APP_PREVIEW",
3092
+ header: "PRODUCT_PAGE_HEADER_ASSET",
3093
+ search_results: "APP_STORE_SEARCH_RESULTS_ASSET",
3094
+ event_card: "EVENT_CARD_ASSET",
3095
+ event_details: "EVENT_DETAILS_PAGE_ASSET"
3096
+ };
3097
+ IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg"];
3098
+ VIDEO_EXTENSIONS = [".mp4", ".mov", ".m4v"];
3099
+ READY_STATES = [
3100
+ "COMPLETE",
3101
+ "PREPARE_FOR_SUBMISSION",
3102
+ "READY_FOR_REVIEW",
3103
+ "WAITING_FOR_REVIEW",
3104
+ "IN_REVIEW",
3105
+ "ACCEPTED",
3106
+ "APPROVED"
3107
+ ];
3108
+ }
3109
+ });
2755
3110
 
2756
3111
  // src/xcode/upload.ts
2757
3112
  var upload_exports = {};
@@ -2817,8 +3172,14 @@ var init_upload = __esm({
2817
3172
  // src/deliver/index.ts
2818
3173
  var deliver_exports = {};
2819
3174
  __export(deliver_exports, {
3175
+ ASSET_FOLDERS: () => ASSET_FOLDERS,
3176
+ categoryFor: () => categoryFor,
2820
3177
  loadMetadata: () => loadMetadata,
3178
+ readImageSize: () => readImageSize,
3179
+ resolveImageGroups: () => resolveImageGroups,
2821
3180
  submitForReview: () => submitForReview,
3181
+ uploadAssetLibrary: () => uploadAssetLibrary,
3182
+ uploadLibraryAsset: () => uploadLibraryAsset,
2822
3183
  uploadMetadata: () => uploadMetadata,
2823
3184
  uploadScreenshots: () => uploadScreenshots,
2824
3185
  uploadToAppStore: () => uploadToAppStore
@@ -2857,11 +3218,36 @@ async function uploadToAppStore(options) {
2857
3218
  copyright: options.copyright
2858
3219
  });
2859
3220
  }
3221
+ const log = (line) => options.onOutput?.(`${line}
3222
+ `);
2860
3223
  if (!options.skipScreenshots && options.screenshotsPath && existsSync(options.screenshotsPath)) {
2861
- await uploadScreenshots({
3224
+ if (options.useAssetLibrary) {
3225
+ await uploadAssetLibrary({
3226
+ client: options.client,
3227
+ appId: app.id,
3228
+ versionId: version.id,
3229
+ assetsPath: options.screenshotsPath,
3230
+ folders: {},
3231
+ rootPlacementType: "APP_SCREENSHOT",
3232
+ overwrite: options.overwriteAssets,
3233
+ log
3234
+ });
3235
+ } else {
3236
+ await uploadScreenshots({
3237
+ client: options.client,
3238
+ versionId: version.id,
3239
+ screenshotsPath: options.screenshotsPath
3240
+ });
3241
+ }
3242
+ }
3243
+ if (!options.skipAssets && options.assetsPath && existsSync(options.assetsPath)) {
3244
+ await uploadAssetLibrary({
2862
3245
  client: options.client,
3246
+ appId: app.id,
2863
3247
  versionId: version.id,
2864
- screenshotsPath: options.screenshotsPath
3248
+ assetsPath: options.assetsPath,
3249
+ overwrite: options.overwriteAssets,
3250
+ log
2865
3251
  });
2866
3252
  }
2867
3253
  if (!options.skipBinaryUpload) {
@@ -2946,17 +3332,7 @@ async function uploadScreenshots(options) {
2946
3332
  fileName: file,
2947
3333
  fileSize: fileData.length
2948
3334
  });
2949
- const ops = reserved.attributes.uploadOperations;
2950
- if (ops) {
2951
- for (const op of ops) {
2952
- const chunk = fileData.subarray(op.offset, op.offset + op.length);
2953
- const headers = {};
2954
- for (const h of op.requestHeaders) {
2955
- headers[h.name] = h.value;
2956
- }
2957
- await fetch(op.url, { method: op.method, headers, body: chunk });
2958
- }
2959
- }
3335
+ await runUploadOperations(reserved.attributes.uploadOperations ?? [], fileData);
2960
3336
  await options.client.appScreenshots.commitScreenshot(reserved.id, {
2961
3337
  sourceFileChecksum: checksum,
2962
3338
  uploaded: true
@@ -2976,7 +3352,9 @@ function detectDisplayType(filename) {
2976
3352
  var init_deliver = __esm({
2977
3353
  "src/deliver/index.ts"() {
2978
3354
  init_metadata();
3355
+ init_assets();
2979
3356
  init_metadata();
3357
+ init_assets();
2980
3358
  }
2981
3359
  });
2982
3360
 
@@ -2985,6 +3363,7 @@ var produce_exports = {};
2985
3363
  __export(produce_exports, {
2986
3364
  SERVICE_NAME_TO_CAPABILITY: () => SERVICE_NAME_TO_CAPABILITY,
2987
3365
  addCapability: () => addCapability,
3366
+ associateAppGroups: () => associateAppGroups,
2988
3367
  buildCapabilitySettings: () => buildCapabilitySettings,
2989
3368
  createApp: () => createApp,
2990
3369
  enableServices: () => enableServices,
@@ -3079,6 +3458,30 @@ async function enableServices(options) {
3079
3458
  });
3080
3459
  }
3081
3460
  }
3461
+ async function associateAppGroups(options) {
3462
+ if (!options.appGroups?.length) return;
3463
+ await options.portalClient.enableAppGroups(options.bundleIdId).catch(() => {
3464
+ });
3465
+ const existing = await options.portalClient.listAppGroups();
3466
+ const appGroupIds = [];
3467
+ for (const group of options.appGroups) {
3468
+ let record = existing.find((ag) => ag.identifier === group.identifier);
3469
+ if (!record) {
3470
+ record = await options.portalClient.createAppGroup({
3471
+ name: group.name,
3472
+ identifier: group.identifier
3473
+ });
3474
+ }
3475
+ const id = record?.applicationGroup ?? record?.applicationGroupId ?? record?.appGroupId ?? record?.id;
3476
+ if (id) appGroupIds.push(id);
3477
+ }
3478
+ if (appGroupIds.length) {
3479
+ await options.portalClient.assignAppGroups({
3480
+ appIdId: options.bundleIdId,
3481
+ appGroupIds
3482
+ });
3483
+ }
3484
+ }
3082
3485
  async function createApp(options) {
3083
3486
  const existing = await options.client.apps.findByBundleId(options.bundleIdentifier);
3084
3487
  if (existing) return existing;
@@ -3097,6 +3500,13 @@ async function createApp(options) {
3097
3500
  });
3098
3501
  }
3099
3502
  }
3503
+ if (options.appGroups?.length && options.portalClient) {
3504
+ await associateAppGroups({
3505
+ portalClient: options.portalClient,
3506
+ bundleIdId: bundleId.id,
3507
+ appGroups: options.appGroups
3508
+ });
3509
+ }
3100
3510
  return options.client.apps.create({
3101
3511
  bundleIdentifier: options.bundleIdentifier,
3102
3512
  bundleIdId: bundleId.id,
@@ -3906,10 +4316,10 @@ async function parseTestResults(options) {
3906
4316
  } else if (inputPath.endsWith(".plist")) {
3907
4317
  testSuites = parsePlistResults(inputPath);
3908
4318
  } else {
3909
- const stat6 = await import('fs').then(
4319
+ const stat7 = await import('fs').then(
3910
4320
  (fs) => fs.statSync(inputPath)
3911
4321
  );
3912
- if (stat6.isDirectory()) {
4322
+ if (stat7.isDirectory()) {
3913
4323
  requireMacOS("xcresult parsing");
3914
4324
  testSuites = await parseXCResult(inputPath);
3915
4325
  } else {
@@ -6086,9 +6496,22 @@ var init_pre_release_versions = __esm({
6086
6496
  });
6087
6497
 
6088
6498
  // src/resources/review-submission-items.ts
6089
- var ReviewSubmissionItemsResource;
6499
+ var ITEM_RELATIONSHIPS, ReviewSubmissionItemsResource;
6090
6500
  var init_review_submission_items = __esm({
6091
6501
  "src/resources/review-submission-items.ts"() {
6502
+ ITEM_RELATIONSHIPS = {
6503
+ appStoreVersionId: ["appStoreVersion", "appStoreVersions"],
6504
+ appCustomProductPageVersionId: ["appCustomProductPageVersion", "appCustomProductPageVersions"],
6505
+ appStoreVersionExperimentId: ["appStoreVersionExperiment", "appStoreVersionExperiments"],
6506
+ appStoreVersionExperimentV2Id: ["appStoreVersionExperimentV2", "appStoreVersionExperiments"],
6507
+ appEventId: ["appEvent", "appEvents"],
6508
+ appAssetLibraryImageId: ["appAssetLibraryImage", "appAssetLibraryImages"],
6509
+ appAssetLibraryVideoId: ["appAssetLibraryVideo", "appAssetLibraryVideos"],
6510
+ backgroundAssetVersionId: ["backgroundAssetVersion", "backgroundAssetVersions"],
6511
+ inAppPurchaseVersionId: ["inAppPurchaseVersion", "inAppPurchaseVersions"],
6512
+ subscriptionVersionId: ["subscriptionVersion", "subscriptionVersions"],
6513
+ subscriptionGroupVersionId: ["subscriptionGroupVersion", "subscriptionGroupVersions"]
6514
+ };
6092
6515
  ReviewSubmissionItemsResource = class {
6093
6516
  constructor(http) {
6094
6517
  this.http = http;
@@ -6110,32 +6533,20 @@ var init_review_submission_items = __esm({
6110
6533
  );
6111
6534
  return response.data;
6112
6535
  }
6536
+ /**
6537
+ * Add one item to a review submission. Besides versions, asset library
6538
+ * images and videos can be submitted on their own (ASC API 4.5.1) so
6539
+ * creative assets are approved ahead of a campaign.
6540
+ */
6113
6541
  async create(params) {
6114
6542
  const relationships = {
6115
6543
  reviewSubmission: {
6116
6544
  data: { type: "reviewSubmissions", id: params.reviewSubmissionId }
6117
6545
  }
6118
6546
  };
6119
- if (params.appStoreVersionId) {
6120
- relationships.appStoreVersion = {
6121
- data: { type: "appStoreVersions", id: params.appStoreVersionId }
6122
- };
6123
- }
6124
- if (params.appCustomProductPageVersionId) {
6125
- relationships.appCustomProductPageVersion = {
6126
- data: {
6127
- type: "appCustomProductPageVersions",
6128
- id: params.appCustomProductPageVersionId
6129
- }
6130
- };
6131
- }
6132
- if (params.appStoreVersionExperimentId) {
6133
- relationships.appStoreVersionExperiment = {
6134
- data: {
6135
- type: "appStoreVersionExperiments",
6136
- id: params.appStoreVersionExperimentId
6137
- }
6138
- };
6547
+ for (const [param, [relationship, type]] of Object.entries(ITEM_RELATIONSHIPS)) {
6548
+ const id = params[param];
6549
+ if (id) relationships[relationship] = { data: { type, id } };
6139
6550
  }
6140
6551
  const response = await this.http.post(
6141
6552
  "/v1/reviewSubmissionItems",
@@ -6366,6 +6777,204 @@ var init_xcode_metrics = __esm({
6366
6777
  }
6367
6778
  });
6368
6779
 
6780
+ // src/resources/app-asset-libraries.ts
6781
+ function mergeRefData(parts) {
6782
+ const merged = {
6783
+ features: [],
6784
+ placementProfileGroups: [],
6785
+ imageSpecs: [],
6786
+ videoSpecs: [],
6787
+ placementTypes: [],
6788
+ displayClasses: []
6789
+ };
6790
+ for (const part of parts) {
6791
+ for (const key of Object.keys(merged)) {
6792
+ merged[key].push(...part[key] ?? []);
6793
+ }
6794
+ }
6795
+ return merged;
6796
+ }
6797
+ var AppAssetLibrariesResource;
6798
+ var init_app_asset_libraries = __esm({
6799
+ "src/resources/app-asset-libraries.ts"() {
6800
+ AppAssetLibrariesResource = class {
6801
+ constructor(http) {
6802
+ this.http = http;
6803
+ }
6804
+ http;
6805
+ async getForApp(appId) {
6806
+ const response = await this.http.get(
6807
+ `/v1/apps/${appId}/assetLibrary`
6808
+ );
6809
+ return response.data;
6810
+ }
6811
+ async listImages(libraryId, params = {}) {
6812
+ return this.http.all(`/v1/appAssetLibraries/${libraryId}/images`, params);
6813
+ }
6814
+ async listVideos(libraryId, params = {}) {
6815
+ return this.http.all(`/v1/appAssetLibraries/${libraryId}/videos`, params);
6816
+ }
6817
+ /**
6818
+ * Specs, placement groups and limits. Apple adds placement groups (new
6819
+ * devices) without an API version bump, so read them from here rather than
6820
+ * hard-coding display types.
6821
+ */
6822
+ async refData(params = {}) {
6823
+ const filter = {};
6824
+ if (params.features?.length) filter.features = params.features;
6825
+ if (params.placementTypes?.length) filter.placementTypes = params.placementTypes;
6826
+ if (params.placementProfileGroups?.length) filter.placementProfileGroups = params.placementProfileGroups;
6827
+ const data = await this.http.all("/v1/appAssetLibraryRefData", { filter });
6828
+ return mergeRefData(data.map((d) => d.attributes));
6829
+ }
6830
+ };
6831
+ }
6832
+ });
6833
+
6834
+ // src/resources/app-asset-library-assets.ts
6835
+ var AssetLibraryMediaResource, AppAssetLibraryImagesResource, AppAssetLibraryVideosResource;
6836
+ var init_app_asset_library_assets = __esm({
6837
+ "src/resources/app-asset-library-assets.ts"() {
6838
+ AssetLibraryMediaResource = class {
6839
+ constructor(http) {
6840
+ this.http = http;
6841
+ }
6842
+ http;
6843
+ async get(id) {
6844
+ const response = await this.http.get(`/v1/${this.type}/${id}`);
6845
+ return response.data;
6846
+ }
6847
+ async reserve(params, extra = {}) {
6848
+ const { libraryId, ...attributes } = params;
6849
+ const response = await this.http.post(`/v1/${this.type}`, {
6850
+ data: {
6851
+ type: this.type,
6852
+ attributes: { ...attributes, ...extra },
6853
+ relationships: {
6854
+ assetLibrary: { data: { type: "appAssetLibraries", id: libraryId } }
6855
+ }
6856
+ }
6857
+ });
6858
+ return response.data;
6859
+ }
6860
+ async patch(id, attributes) {
6861
+ const response = await this.http.patch(`/v1/${this.type}/${id}`, {
6862
+ data: { type: this.type, id, attributes }
6863
+ });
6864
+ return response.data;
6865
+ }
6866
+ /** Fails with 409 STATE_ERROR.ASSET_HAS_PLACEMENTS while the asset is still placed. */
6867
+ async delete(id) {
6868
+ await this.http.delete(`/v1/${this.type}/${id}`);
6869
+ }
6870
+ async listPlacements(id, params = {}) {
6871
+ return this.http.all(`/v1/${this.type}/${id}/placements`, params);
6872
+ }
6873
+ };
6874
+ AppAssetLibraryImagesResource = class extends AssetLibraryMediaResource {
6875
+ type = "appAssetLibraryImages";
6876
+ async create(params) {
6877
+ return this.reserve(params);
6878
+ }
6879
+ async update(id, attributes) {
6880
+ return this.patch(id, { ...attributes });
6881
+ }
6882
+ };
6883
+ AppAssetLibraryVideosResource = class extends AssetLibraryMediaResource {
6884
+ type = "appAssetLibraryVideos";
6885
+ async create(params) {
6886
+ const { previewFrameTimeCode, ...rest } = params;
6887
+ return this.reserve(rest, previewFrameTimeCode ? { previewFrameTimeCode } : {});
6888
+ }
6889
+ async update(id, attributes) {
6890
+ return this.patch(id, { ...attributes });
6891
+ }
6892
+ };
6893
+ }
6894
+ });
6895
+
6896
+ // src/resources/app-asset-library-placements.ts
6897
+ function targetRelationship(target) {
6898
+ return { [target.kind]: { data: { type: TARGET_TYPES[target.kind], id: target.id } } };
6899
+ }
6900
+ var TARGET_TYPES, AppAssetLibraryPlacementsResource;
6901
+ var init_app_asset_library_placements = __esm({
6902
+ "src/resources/app-asset-library-placements.ts"() {
6903
+ TARGET_TYPES = {
6904
+ appStoreVersionLocalization: "appStoreVersionLocalizations",
6905
+ appCustomProductPageLocalization: "appCustomProductPageLocalizations",
6906
+ appEventLocalization: "appEventLocalizations",
6907
+ appStoreVersionExperimentTreatmentLocalization: "appStoreVersionExperimentTreatmentLocalizations"
6908
+ };
6909
+ AppAssetLibraryPlacementsResource = class {
6910
+ constructor(http) {
6911
+ this.http = http;
6912
+ }
6913
+ http;
6914
+ /**
6915
+ * Place an image or video on one localization. Placements cannot be edited —
6916
+ * delete and recreate. The parent version/page must be editable.
6917
+ */
6918
+ async create(params) {
6919
+ if (!params.imageId === !params.videoId) {
6920
+ throw new Error("A placement needs exactly one of imageId or videoId");
6921
+ }
6922
+ const media = params.imageId ? { image: { data: { type: "appAssetLibraryImages", id: params.imageId } } } : { video: { data: { type: "appAssetLibraryVideos", id: params.videoId } } };
6923
+ const response = await this.http.post(
6924
+ "/v1/appAssetLibraryPlacements",
6925
+ {
6926
+ data: {
6927
+ type: "appAssetLibraryPlacements",
6928
+ attributes: { placementType: params.placementType, placementGroup: params.placementGroup },
6929
+ relationships: { ...media, ...targetRelationship(params.target) }
6930
+ }
6931
+ }
6932
+ );
6933
+ return response.data;
6934
+ }
6935
+ async get(id, params = {}) {
6936
+ const response = await this.http.get(
6937
+ `/v1/appAssetLibraryPlacements/${id}`,
6938
+ params.include ? { include: params.include } : void 0
6939
+ );
6940
+ return response.data;
6941
+ }
6942
+ async delete(id) {
6943
+ await this.http.delete(`/v1/appAssetLibraryPlacements/${id}`);
6944
+ }
6945
+ /** Placements on one localization, in display order by default. */
6946
+ async listForTarget(target, params = {}) {
6947
+ const filter = {};
6948
+ if (params.placementType) filter.placementType = params.placementType;
6949
+ if (params.placementGroup) filter.placementGroup = params.placementGroup;
6950
+ const listParams = { filter, include: params.include, sort: "placementGroupPosition" };
6951
+ return this.http.all(
6952
+ `/v1/${TARGET_TYPES[target.kind]}/${target.id}/placements`,
6953
+ listParams
6954
+ );
6955
+ }
6956
+ /** Set the display order of a placement group. In-app event localizations cannot be ordered. */
6957
+ async order(params) {
6958
+ if (params.target.kind === "appEventLocalization") {
6959
+ throw new Error("In-app event placements cannot be ordered");
6960
+ }
6961
+ await this.http.post("/v1/appAssetLibraryPlacementOrderingRequests", {
6962
+ data: {
6963
+ type: "appAssetLibraryPlacementOrderingRequests",
6964
+ attributes: { placementGroup: params.placementGroup },
6965
+ relationships: {
6966
+ orderedPlacements: {
6967
+ data: params.placementIds.map((id) => ({ type: "appAssetLibraryPlacements", id }))
6968
+ },
6969
+ ...targetRelationship(params.target)
6970
+ }
6971
+ }
6972
+ });
6973
+ }
6974
+ };
6975
+ }
6976
+ });
6977
+
6369
6978
  // src/client/portal.ts
6370
6979
  function pickUniqueKeyName(base, taken) {
6371
6980
  if (!taken.has(base)) return base;
@@ -6705,7 +7314,10 @@ var init_portal = __esm({
6705
7314
  }
6706
7315
  // ── Teams ──────────────────────────────────────────────────────────────
6707
7316
  async listTeams() {
6708
- const data = await this.post("account/listTeams.action", {});
7317
+ const data = await this.post("account/listTeams.action", {
7318
+ pageSize: "500",
7319
+ pageNumber: "1"
7320
+ });
6709
7321
  return data.teams ?? [];
6710
7322
  }
6711
7323
  // ── App IDs / Bundle IDs ───────────────────────────────────────────────
@@ -6719,11 +7331,12 @@ var init_portal = __esm({
6719
7331
  return data.appIds ?? [];
6720
7332
  }
6721
7333
  async createAppId(params) {
7334
+ const safeName = params.name.replace(/[^0-9A-Za-z\s]/g, " ").replace(/\s+/g, " ").trim() || "App";
6722
7335
  const reqParams = {
6723
7336
  teamId: this.teamId,
6724
7337
  type: "explicit",
6725
7338
  identifier: params.identifier,
6726
- name: params.name
7339
+ name: safeName
6727
7340
  };
6728
7341
  if (params.enablePush) reqParams.push = "on";
6729
7342
  if (params.enableIAP !== false) reqParams.inAppPurchase = "on";
@@ -6758,6 +7371,191 @@ var init_portal = __esm({
6758
7371
  featureValue: params.enabled ? "on" : "off"
6759
7372
  });
6760
7373
  }
7374
+ // ── Application Groups ──────────────────────────────────────────────────
7375
+ //
7376
+ // App Groups (e.g. `group.com.playspina.com`) are the only clean way to
7377
+ // share data — an auth token, in our case — between a host app and an
7378
+ // embedded extension (Messages / Notification Service / etc.). App Store
7379
+ // Connect API keys (.p8) genuinely cannot manage them; Apple only exposes
7380
+ // group create/associate through this legacy session-cookie portal API,
7381
+ // which is why they live on PortalClient (Apple ID session) and not on the
7382
+ // ASC REST client. Endpoint shapes ported 1:1 from Spaceship's
7383
+ // portal_client.rb (listApplicationGroups / addApplicationGroup /
7384
+ // assignApplicationGroupToAppId).
7385
+ /** Apple's internal feature code for the App Groups capability. */
7386
+ static APP_GROUPS_FEATURE = "APG3427HIY";
7387
+ /** List all app groups in the team. */
7388
+ async listAppGroups() {
7389
+ const data = await this.post("account/ios/identifiers/listApplicationGroups.action", {
7390
+ teamId: this.teamId,
7391
+ pageNumber: "1",
7392
+ pageSize: "500",
7393
+ sort: "name=asc"
7394
+ });
7395
+ return data.applicationGroupList ?? [];
7396
+ }
7397
+ /**
7398
+ * Create an app group, returning the raw group record. Idempotent —
7399
+ * Apple rejects a duplicate group identifier ("Multiple App Groups may
7400
+ * not have the same identifier"), so we list first and reuse an existing
7401
+ * group when the identifier already exists in the team.
7402
+ *
7403
+ * Note the two ids on the returned record: `identifier` is the
7404
+ * `group.com.…` string you type, while `applicationGroup` is Apple's
7405
+ * opaque internal handle that association actually requires — do NOT pass
7406
+ * the human identifier to assignAppGroups.
7407
+ */
7408
+ async createAppGroup(params) {
7409
+ const existing = await this.listAppGroups();
7410
+ const hit = existing.find((g) => g.identifier === params.identifier);
7411
+ if (hit) return hit;
7412
+ const safeName = params.name.replace(/[^0-9A-Za-z\s]/g, " ").replace(/\s+/g, " ").trim() || "App Group";
7413
+ const data = await this.post("account/ios/identifiers/addApplicationGroup.action", {
7414
+ teamId: this.teamId,
7415
+ identifier: params.identifier,
7416
+ name: safeName
7417
+ });
7418
+ return data.applicationGroup;
7419
+ }
7420
+ /**
7421
+ * Enable the App Groups capability on an App ID. Required before an
7422
+ * App ID can have a group associated with it — mirrors Spaceship
7423
+ * enabling `AppService::AppGroup` before `associate_groups`.
7424
+ */
7425
+ async enableAppGroups(appIdId) {
7426
+ await this.post("account/ios/identifiers/updateService.action", {
7427
+ teamId: this.teamId,
7428
+ displayId: appIdId,
7429
+ featureType: _PortalClient.APP_GROUPS_FEATURE,
7430
+ featureValue: "true"
7431
+ });
7432
+ }
7433
+ /**
7434
+ * Associate one or more app groups with an App ID. `appGroupIds` are the
7435
+ * internal ids (Spaceship's `app_group_id`), NOT the `group.com.…`
7436
+ * identifiers. Apple wants both `appIdId` and `displayId` set to the
7437
+ * App ID's internal id, per Spaceship's associate_groups_with_app.
7438
+ */
7439
+ async assignAppGroups(params) {
7440
+ await this.post("account/ios/identifiers/assignApplicationGroupToAppId.action", {
7441
+ teamId: this.teamId,
7442
+ appIdId: params.appIdId,
7443
+ displayId: params.appIdId,
7444
+ applicationGroups: params.appGroupIds
7445
+ });
7446
+ }
7447
+ // ── Apple Pay merchants (OMCs) ─────────────────────────────────────────
7448
+ //
7449
+ // Merchant IDs are team-level identifiers (merchant.com.…) that an App ID's
7450
+ // Apple Pay capability must be LINKED to — enabling the capability alone
7451
+ // produces profiles that fail with "doesn't support the … Merchant ID".
7452
+ // Like App Groups, merchants are only manageable through the legacy
7453
+ // session-cookie portal API. Endpoint shapes ported 1:1 from Spaceship's
7454
+ // portal_client.rb (merchants / create_merchant! /
7455
+ // associate_merchants_with_app).
7456
+ /** Apple's internal feature code for the Apple Pay capability. */
7457
+ static APPLE_PAY_FEATURE = "OM633U5T5G";
7458
+ /**
7459
+ * List all Apple Pay merchant IDs in the team. Each record maps
7460
+ * (per Spaceship's merchant.rb attr_mapping): `omcId` = internal id,
7461
+ * `identifier` = the human `merchant.com.…` string, `name` = display name.
7462
+ */
7463
+ async listMerchants() {
7464
+ const data = await this.post("account/ios/identifiers/listOMCs.action", {
7465
+ teamId: this.teamId,
7466
+ pageNumber: "1",
7467
+ pageSize: "500",
7468
+ sort: "name=asc"
7469
+ });
7470
+ return data.identifierList ?? [];
7471
+ }
7472
+ /**
7473
+ * Register (or reuse) a merchant ID, returning its INTERNAL id (`omcId`) —
7474
+ * Apple's opaque handle that association requires, NOT the human
7475
+ * `merchant.com.…` identifier. Idempotent: lists first and reuses an
7476
+ * existing merchant with the same identifier. Endpoint + params ported 1:1
7477
+ * from Spaceship's create_merchant! (addOMC.action).
7478
+ */
7479
+ async createMerchant(params) {
7480
+ const existing = await this.listMerchants();
7481
+ const hit = existing.find((m) => m.identifier === params.identifier);
7482
+ if (hit?.omcId) return String(hit.omcId);
7483
+ const safeName = params.name.replace(/[^0-9A-Za-z\s]/g, " ").replace(/\s+/g, " ").trim() || "Merchant";
7484
+ const data = await this.post("account/ios/identifiers/addOMC.action", {
7485
+ name: safeName,
7486
+ identifier: params.identifier,
7487
+ teamId: this.teamId
7488
+ });
7489
+ const rec = data.omcId ?? data;
7490
+ const id = typeof rec === "string" ? rec : rec?.omcId ?? rec?.merchantId;
7491
+ if (!id) {
7492
+ throw new Error("Apple returned no internal merchant id (omcId) for the registered merchant.");
7493
+ }
7494
+ return String(id);
7495
+ }
7496
+ /**
7497
+ * Enable the Apple Pay capability on an App ID. Apple Pay is a standard
7498
+ * AppService (service_id OM633U5T5G) whose service_uri is the default
7499
+ * `updateService.action` — confirmed in Spaceship's app_service.rb.
7500
+ */
7501
+ async enableApplePay(appIdId) {
7502
+ await this.post("account/ios/identifiers/updateService.action", {
7503
+ teamId: this.teamId,
7504
+ displayId: appIdId,
7505
+ featureType: _PortalClient.APPLE_PAY_FEATURE,
7506
+ featureValue: "true"
7507
+ });
7508
+ }
7509
+ /**
7510
+ * Associate merchant IDs with an App ID's Apple Pay capability. `omcIds`
7511
+ * are the internal ids (each merchant record's `omcId`), NOT the
7512
+ * `merchant.com.…` identifiers. Endpoint + params ported 1:1 from
7513
+ * Spaceship's associate_merchants_with_app (assignOMCToAppId.action — note
7514
+ * the singular "OMC", and NO displayId param).
7515
+ */
7516
+ async assignMerchants(params) {
7517
+ await this.post("account/ios/identifiers/assignOMCToAppId.action", {
7518
+ teamId: this.teamId,
7519
+ appIdId: params.appIdId,
7520
+ omcIds: params.omcIds
7521
+ });
7522
+ }
7523
+ /**
7524
+ * Create an Apple Pay **Payment Processing Certificate** on a merchant ID
7525
+ * from a CSR — the certificate a PSP (e.g. Stripe) hands you as a CSR, which
7526
+ * must be uploaded to the Merchant ID in Apple Developer, then the resulting
7527
+ * `.cer` downloaded and given back to the PSP so it can decrypt payment
7528
+ * tokens. This is the one manual Apple-portal step in wiring up Apple Pay.
7529
+ *
7530
+ * Ported 1:1 from Spaceship's create_certificate! (submitCertificateRequest
7531
+ * .action): the merchant's INTERNAL id (`omcId`) is passed as BOTH `appIdId`
7532
+ * and `specialIdentifierDisplayId` — the latter is how the portal scopes a
7533
+ * cert to a merchant/special identifier rather than an App ID. The cert type
7534
+ * is APPLE_PAY (`4APLUP237T`). Returns the created certRequest (its
7535
+ * `certificateId` feeds `downloadApplePayCertificate`).
7536
+ *
7537
+ * @param omcId the merchant's internal id (from createMerchant/listMerchants)
7538
+ * @param csrContent the PEM CSR string (Stripe's "Apple Pay CSR")
7539
+ */
7540
+ async createApplePayCertificate(params) {
7541
+ const data = await this.post("account/ios/certificate/submitCertificateRequest.action", {
7542
+ teamId: this.teamId,
7543
+ type: _PortalClient.CERT_TYPES.APPLE_PAY,
7544
+ csrContent: params.csrContent,
7545
+ appIdId: params.omcId,
7546
+ specialIdentifierDisplayId: params.omcId
7547
+ });
7548
+ return data.certRequest;
7549
+ }
7550
+ /**
7551
+ * Download an Apple Pay Payment Processing Certificate's raw bytes by its
7552
+ * certificate id (from `createApplePayCertificate`). Thin wrapper over
7553
+ * `downloadCertificate` pinned to the APPLE_PAY type — the DER `.cer` you
7554
+ * upload back into Stripe.
7555
+ */
7556
+ async downloadApplePayCertificate(certificateId) {
7557
+ return this.downloadCertificate(certificateId, _PortalClient.CERT_TYPES.APPLE_PAY);
7558
+ }
6761
7559
  // ── Certificates ───────────────────────────────────────────────────────
6762
7560
  async listCertificates(types) {
6763
7561
  const iosTypes = types ?? [
@@ -7297,6 +8095,9 @@ var init_client = __esm({
7297
8095
  init_in_app_purchases();
7298
8096
  init_game_center_detail();
7299
8097
  init_xcode_metrics();
8098
+ init_app_asset_libraries();
8099
+ init_app_asset_library_assets();
8100
+ init_app_asset_library_placements();
7300
8101
  init_auth();
7301
8102
  init_request();
7302
8103
  init_apple_session();
@@ -7338,6 +8139,10 @@ var init_client = __esm({
7338
8139
  inAppPurchases;
7339
8140
  gameCenterDetail;
7340
8141
  xcodeMetrics;
8142
+ appAssetLibraries;
8143
+ appAssetLibraryImages;
8144
+ appAssetLibraryVideos;
8145
+ appAssetLibraryPlacements;
7341
8146
  constructor(config) {
7342
8147
  const tokenManager = new TokenManager({
7343
8148
  keyId: config.keyId,
@@ -7381,6 +8186,10 @@ var init_client = __esm({
7381
8186
  this.inAppPurchases = new InAppPurchasesResource(this.http);
7382
8187
  this.gameCenterDetail = new GameCenterDetailResource(this.http);
7383
8188
  this.xcodeMetrics = new XcodeMetricsResource(this.http);
8189
+ this.appAssetLibraries = new AppAssetLibrariesResource(this.http);
8190
+ this.appAssetLibraryImages = new AppAssetLibraryImagesResource(this.http);
8191
+ this.appAssetLibraryVideos = new AppAssetLibraryVideosResource(this.http);
8192
+ this.appAssetLibraryPlacements = new AppAssetLibraryPlacementsResource(this.http);
7384
8193
  }
7385
8194
  async request(path, options) {
7386
8195
  return this.http.request(path, options);
@@ -8148,8 +8957,8 @@ var init_utilities = __esm({
8148
8957
  description: "Ask the user for input",
8149
8958
  async run(params) {
8150
8959
  const text = params.text ?? "Enter a value:";
8151
- const { createInterface: createInterface2 } = await import('readline');
8152
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
8960
+ const { createInterface: createInterface4 } = await import('readline');
8961
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
8153
8962
  return new Promise((resolve4) => {
8154
8963
  rl.question(text + " ", (answer) => {
8155
8964
  rl.close();
@@ -8541,6 +9350,308 @@ var init_options = __esm({
8541
9350
  init_client();
8542
9351
  }
8543
9352
  });
9353
+
9354
+ // src/privacy/index.ts
9355
+ var privacy_exports = {};
9356
+ __export(privacy_exports, {
9357
+ DATA_PROTECTIONS: () => DATA_PROTECTIONS,
9358
+ DATA_USAGE_CATEGORIES: () => DATA_USAGE_CATEGORIES,
9359
+ DATA_USAGE_PURPOSES: () => DATA_USAGE_PURPOSES,
9360
+ createAppDataUsage: () => createAppDataUsage,
9361
+ deleteAppDataUsage: () => deleteAppDataUsage,
9362
+ getAppDataUsagesPublishState: () => getAppDataUsagesPublishState,
9363
+ listAppDataUsages: () => listAppDataUsages,
9364
+ publishAppDataUsages: () => publishAppDataUsages,
9365
+ selectProviderForApp: () => selectProviderForApp,
9366
+ uploadAppPrivacyDetails: () => uploadAppPrivacyDetails,
9367
+ validateUsages: () => validateUsages
9368
+ });
9369
+ async function iris(session, method, path, body) {
9370
+ const res = await fetch(path.startsWith("http") ? path : `${IRIS_BASE2}${path}`, {
9371
+ method,
9372
+ headers: {
9373
+ Cookie: cookieHeader(session),
9374
+ Accept: "application/vnd.api+json, application/json",
9375
+ "Content-Type": "application/vnd.api+json",
9376
+ "X-Requested-With": "XMLHttpRequest"
9377
+ },
9378
+ body: body ? JSON.stringify(body) : void 0
9379
+ });
9380
+ absorbCookies(session, res);
9381
+ const text = await res.text();
9382
+ let parsed = null;
9383
+ try {
9384
+ parsed = text ? JSON.parse(text) : null;
9385
+ } catch {
9386
+ }
9387
+ return { status: res.status, body: parsed, text };
9388
+ }
9389
+ function cookieHeader(session) {
9390
+ return Object.entries(session.cookies).map(([k, v]) => `${k}=${v}`).join("; ");
9391
+ }
9392
+ function absorbCookies(session, res) {
9393
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
9394
+ const m = raw.match(/^([^=]+)=([^;]*)/);
9395
+ if (m) session.cookies[m[1]] = m[2];
9396
+ }
9397
+ }
9398
+ function fail(what, r) {
9399
+ const detail = r.body?.errors?.map((e) => e.detail ?? e.title).join("; ") || r.text.slice(0, 300);
9400
+ throw new Error(`${what} failed (HTTP ${r.status}): ${detail}`);
9401
+ }
9402
+ async function selectProviderForApp(session, appId, onLog) {
9403
+ const owns = async () => (await iris(session, "GET", `/apps/${appId}?fields[apps]=name`)).status === 200;
9404
+ if (await owns()) return;
9405
+ const res = await fetch(OLYMPUS_SESSION2, { headers: { Cookie: cookieHeader(session), Accept: "application/json" } });
9406
+ absorbCookies(session, res);
9407
+ const info = await res.json().catch(() => null);
9408
+ const providers = info?.availableProviders ?? [];
9409
+ for (const p of providers) {
9410
+ const sw = await fetch(OLYMPUS_SESSION2, {
9411
+ method: "POST",
9412
+ headers: { Cookie: cookieHeader(session), "Content-Type": "application/json", Accept: "application/json" },
9413
+ body: JSON.stringify({ provider: { providerId: p.providerId } })
9414
+ });
9415
+ absorbCookies(session, sw);
9416
+ if (sw.ok && await owns()) {
9417
+ onLog?.(`Switched to team "${p.name}"`);
9418
+ session.teamId = String(p.providerId);
9419
+ return;
9420
+ }
9421
+ }
9422
+ throw new Error(`None of this Apple ID's ${providers.length} teams can see app ${appId}.`);
9423
+ }
9424
+ async function listAppDataUsages(session, appId) {
9425
+ const all = [];
9426
+ let next = `/apps/${appId}/dataUsages?include=category,grouping,purpose,dataProtection&limit=500`;
9427
+ while (next) {
9428
+ const r = await iris(session, "GET", next);
9429
+ if (r.status !== 200) fail("List app data usages", r);
9430
+ all.push(...(r.body?.data ?? []).map((d) => ({ id: d.id })));
9431
+ next = r.body?.links?.next ?? null;
9432
+ }
9433
+ return all;
9434
+ }
9435
+ async function createAppDataUsage(session, input) {
9436
+ const relationships = { app: { data: { type: "apps", id: input.appId } } };
9437
+ if (input.category) relationships.category = { data: { type: "appDataUsageCategories", id: input.category } };
9438
+ if (input.purpose) relationships.purpose = { data: { type: "appDataUsagePurposes", id: input.purpose } };
9439
+ relationships.dataProtection = { data: { type: "appDataUsageDataProtections", id: input.dataProtection } };
9440
+ const r = await iris(session, "POST", "/appDataUsages", { data: { type: "appDataUsages", relationships } });
9441
+ if (r.status !== 201 && r.status !== 200) fail(`Create ${input.category ?? ""} ${input.purpose ?? ""} ${input.dataProtection}`.replace(/\s+/g, " "), r);
9442
+ return r.body?.data?.id;
9443
+ }
9444
+ async function deleteAppDataUsage(session, id) {
9445
+ const r = await iris(session, "DELETE", `/appDataUsages/${id}`);
9446
+ if (r.status !== 204 && r.status !== 200) fail(`Delete app data usage ${id}`, r);
9447
+ }
9448
+ async function getAppDataUsagesPublishState(session, appId) {
9449
+ const r = await iris(session, "GET", `/apps/${appId}/dataUsagePublishState`);
9450
+ if (r.status !== 200) fail("Read privacy publish state", r);
9451
+ return { id: r.body.data.id, published: Boolean(r.body.data.attributes?.published) };
9452
+ }
9453
+ async function publishAppDataUsages(session, publishStateId) {
9454
+ const r = await iris(session, "PATCH", `/appDataUsagesPublishState/${publishStateId}`, {
9455
+ data: { type: "appDataUsagesPublishState", id: publishStateId, attributes: { published: true } }
9456
+ });
9457
+ if (r.status !== 200) fail("Publish privacy details", r);
9458
+ }
9459
+ async function uploadAppPrivacyDetails(opts) {
9460
+ const { session, appId, usages, skipPublish, onLog = () => {
9461
+ } } = opts;
9462
+ validateUsages(usages);
9463
+ await selectProviderForApp(session, appId, onLog);
9464
+ const existing = await listAppDataUsages(session, appId);
9465
+ onLog(`Deleting ${existing.length} existing answer${existing.length === 1 ? "" : "s"}`);
9466
+ for (const u of existing) await deleteAppDataUsage(session, u.id);
9467
+ let created = 0;
9468
+ for (const usage of usages) {
9469
+ const purposes = usage.purposes?.length ? usage.purposes : [null];
9470
+ for (const purpose of purposes) {
9471
+ for (const protection of usage.data_protections) {
9472
+ onLog(protection === "DATA_NOT_COLLECTED" ? `Setting ${protection}` : `Setting ${usage.category} and ${purpose} to ${protection}`);
9473
+ await createAppDataUsage(session, { appId, category: usage.category ?? null, purpose, dataProtection: protection });
9474
+ created++;
9475
+ }
9476
+ }
9477
+ }
9478
+ if (skipPublish) {
9479
+ onLog("Skipping publish");
9480
+ return { deleted: existing.length, created, published: false };
9481
+ }
9482
+ const state = await getAppDataUsagesPublishState(session, appId);
9483
+ await publishAppDataUsages(session, state.id);
9484
+ onLog("Published");
9485
+ return { deleted: existing.length, created, published: true };
9486
+ }
9487
+ function validateUsages(usages) {
9488
+ if (!Array.isArray(usages) || usages.length === 0) throw new Error("No privacy usages given.");
9489
+ const notCollected = usages.some((u) => u.data_protections?.includes("DATA_NOT_COLLECTED"));
9490
+ if (notCollected && usages.length > 1) {
9491
+ throw new Error("DATA_NOT_COLLECTED must be the only entry: an app cannot both collect nothing and collect something.");
9492
+ }
9493
+ const known = (list, v, what) => {
9494
+ if (!list.includes(v)) throw new Error(`"${v}" is not an App Store Connect ${what}. Use one of: ${list.join(", ")}.`);
9495
+ };
9496
+ for (const u of usages) {
9497
+ if (!u.data_protections?.length) throw new Error(`${u.category ?? "An entry"} has no data_protections.`);
9498
+ if (u.category) known(DATA_USAGE_CATEGORIES, u.category, "data category");
9499
+ for (const p of u.purposes ?? []) known(DATA_USAGE_PURPOSES, p, "purpose");
9500
+ for (const d of u.data_protections) known(DATA_PROTECTIONS, d, "data protection");
9501
+ if (u.data_protections.includes("DATA_NOT_COLLECTED")) continue;
9502
+ if (!u.category) throw new Error("Every collected entry needs a category.");
9503
+ if (!u.purposes?.length) throw new Error(`${u.category} needs at least one purpose.`);
9504
+ const linked = u.data_protections.includes("DATA_LINKED_TO_YOU");
9505
+ const unlinked = u.data_protections.includes("DATA_NOT_LINKED_TO_YOU");
9506
+ if (linked && unlinked) throw new Error(`${u.category} cannot be both linked and not linked to the user.`);
9507
+ }
9508
+ }
9509
+ var IRIS_BASE2, OLYMPUS_SESSION2, DATA_USAGE_CATEGORIES, DATA_USAGE_PURPOSES, DATA_PROTECTIONS;
9510
+ var init_privacy = __esm({
9511
+ "src/privacy/index.ts"() {
9512
+ IRIS_BASE2 = "https://appstoreconnect.apple.com/iris/v1";
9513
+ OLYMPUS_SESSION2 = "https://appstoreconnect.apple.com/olympus/v1/session";
9514
+ DATA_USAGE_CATEGORIES = [
9515
+ "PAYMENT_INFORMATION",
9516
+ "CREDIT_AND_FRAUD",
9517
+ "OTHER_FINANCIAL_INFO",
9518
+ "PRECISE_LOCATION",
9519
+ "COARSE_LOCATION",
9520
+ "SENSITIVE_INFO",
9521
+ "PHYSICAL_ADDRESS",
9522
+ "EMAIL_ADDRESS",
9523
+ "NAME",
9524
+ "PHONE_NUMBER",
9525
+ "OTHER_CONTACT_INFO",
9526
+ "CONTACTS",
9527
+ "EMAILS_OR_TEXT_MESSAGES",
9528
+ "PHOTOS_OR_VIDEOS",
9529
+ "AUDIO",
9530
+ "GAMEPLAY_CONTENT",
9531
+ "CUSTOMER_SUPPORT",
9532
+ "OTHER_USER_CONTENT",
9533
+ "BROWSING_HISTORY",
9534
+ "SEARCH_HISTORY",
9535
+ "USER_ID",
9536
+ "DEVICE_ID",
9537
+ "PURCHASE_HISTORY",
9538
+ "PRODUCT_INTERACTION",
9539
+ "ADVERTISING_DATA",
9540
+ "OTHER_USAGE_DATA",
9541
+ "CRASH_DATA",
9542
+ "PERFORMANCE_DATA",
9543
+ "OTHER_DIAGNOSTIC_DATA",
9544
+ "OTHER_DATA",
9545
+ "HEALTH",
9546
+ "FITNESS"
9547
+ ];
9548
+ DATA_USAGE_PURPOSES = [
9549
+ "THIRD_PARTY_ADVERTISING",
9550
+ "DEVELOPERS_ADVERTISING",
9551
+ "ANALYTICS",
9552
+ "PRODUCT_PERSONALIZATION",
9553
+ "APP_FUNCTIONALITY",
9554
+ "OTHER_PURPOSES"
9555
+ ];
9556
+ DATA_PROTECTIONS = [
9557
+ "DATA_USED_TO_TRACK_YOU",
9558
+ "DATA_LINKED_TO_YOU",
9559
+ "DATA_NOT_LINKED_TO_YOU",
9560
+ "DATA_NOT_COLLECTED"
9561
+ ];
9562
+ }
9563
+ });
9564
+
9565
+ // src/review/index.ts
9566
+ var review_exports = {};
9567
+ __export(review_exports, {
9568
+ getReviewFeedback: () => getReviewFeedback,
9569
+ getThreadMessages: () => getThreadMessages,
9570
+ htmlToText: () => htmlToText
9571
+ });
9572
+ async function iris2(session, path) {
9573
+ const res = await fetch(`${IRIS_BASE3}${path}`, {
9574
+ headers: {
9575
+ Cookie: Object.entries(session.cookies).map(([k, v]) => `${k}=${v}`).join("; "),
9576
+ Accept: "application/vnd.api+json, application/json",
9577
+ "X-Requested-With": "XMLHttpRequest"
9578
+ }
9579
+ });
9580
+ for (const raw of res.headers.getSetCookie?.() ?? []) {
9581
+ const m = raw.match(/^([^=]+)=([^;]*)/);
9582
+ if (m) session.cookies[m[1]] = m[2];
9583
+ }
9584
+ const text = await res.text();
9585
+ let body = null;
9586
+ try {
9587
+ body = text ? JSON.parse(text) : null;
9588
+ } catch {
9589
+ }
9590
+ if (!res.ok) {
9591
+ const detail = body?.errors?.map((e) => e.detail ?? e.title).join("; ") || text.slice(0, 300);
9592
+ throw new Error(`GET ${path} failed (HTTP ${res.status}): ${detail}`);
9593
+ }
9594
+ return body;
9595
+ }
9596
+ function htmlToText(html) {
9597
+ if (!html) return "";
9598
+ return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|h\d)>/gi, "\n").replace(/<li[^>]*>/gi, "- ").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\n{3,}/g, "\n\n").trim();
9599
+ }
9600
+ function reasonsFrom(rejection) {
9601
+ const reasons = rejection?.attributes?.reasons ?? [];
9602
+ return (Array.isArray(reasons) ? reasons : []).map((r) => ({
9603
+ section: r.reasonSection ?? r.section,
9604
+ description: r.reasonDescription ?? r.description,
9605
+ code: r.reasonCode ?? r.code
9606
+ }));
9607
+ }
9608
+ async function getThreadMessages(session, threadId) {
9609
+ const body = await iris2(session, `/resolutionCenterThreads/${threadId}/resolutionCenterMessages?include=fromActor,rejections`);
9610
+ const included = new Map((body?.included ?? []).map((i) => [`${i.type}:${i.id}`, i]));
9611
+ return (body?.data ?? []).map((m) => {
9612
+ const actor = m.relationships?.fromActor?.data;
9613
+ const actorRow = actor ? included.get(`${actor.type}:${actor.id}`) : null;
9614
+ const rejections = (m.relationships?.rejections?.data ?? []).map((r) => included.get(`${r.type}:${r.id}`)).filter(Boolean);
9615
+ return {
9616
+ id: m.id,
9617
+ createdDate: m.attributes?.createdDate ?? null,
9618
+ from: actorRow?.attributes?.actorType ?? actorRow?.attributes?.userFirstName ?? null,
9619
+ body: htmlToText(m.attributes?.messageBody),
9620
+ reasons: rejections.flatMap(reasonsFrom)
9621
+ };
9622
+ });
9623
+ }
9624
+ async function getReviewFeedback(session, appId, opts = {}) {
9625
+ await selectProviderForApp(session, appId, opts.onLog);
9626
+ const subs = await iris2(session, `/apps/${appId}/reviewSubmissions?limit=10`);
9627
+ const out = [];
9628
+ for (const s of subs?.data ?? []) {
9629
+ const state = s.attributes?.state ?? "";
9630
+ if (opts.onlyOpen && state !== "UNRESOLVED_ISSUES") continue;
9631
+ const threadsBody = await iris2(session, `/resolutionCenterThreads?filter[reviewSubmission]=${s.id}&include=reviewSubmission`);
9632
+ const threads = [];
9633
+ for (const t of threadsBody?.data ?? []) {
9634
+ threads.push({
9635
+ id: t.id,
9636
+ threadType: t.attributes?.threadType ?? null,
9637
+ state: t.attributes?.state ?? null,
9638
+ createdDate: t.attributes?.createdDate ?? null,
9639
+ messages: await getThreadMessages(session, t.id)
9640
+ });
9641
+ }
9642
+ if (threads.length || !opts.onlyOpen) {
9643
+ out.push({ submissionId: s.id, state, submittedDate: s.attributes?.submittedDate ?? null, threads });
9644
+ }
9645
+ }
9646
+ return out;
9647
+ }
9648
+ var IRIS_BASE3;
9649
+ var init_review = __esm({
9650
+ "src/review/index.ts"() {
9651
+ init_privacy();
9652
+ IRIS_BASE3 = "https://appstoreconnect.apple.com/iris/v1";
9653
+ }
9654
+ });
8544
9655
  function createRunCommand() {
8545
9656
  const cmd = new Command("run").description("Execute a lane from Fastfile.ts").argument("<lane>", "Lane name to execute").argument("[params...]", "Key:value parameters").option("-p, --platform <platform>", "Platform (ios, android)").action(async (laneName, params, opts) => {
8546
9657
  const { loadFastfile: loadFastfile2 } = await Promise.resolve().then(() => (init_fastfile(), fastfile_exports));
@@ -8661,10 +9772,10 @@ function createPilotCommand() {
8661
9772
  const cmd = new Command("pilot").description("Manage TestFlight builds and testers");
8662
9773
  cmd.command("upload").description("Upload a build to TestFlight").requiredOption("-i, --ipa <path>", "Path to IPA file").option("--api-key-id <id>", "ASC API Key ID").option("--api-issuer-id <id>", "ASC API Issuer ID").option("--api-key <path>", "Path to .p8 API key file").action(async (opts) => {
8663
9774
  const { uploadToTestFlight: uploadToTestFlight2 } = await Promise.resolve().then(() => (init_pilot(), pilot_exports));
8664
- const { readFile: readFile8 } = await import('fs/promises');
9775
+ const { readFile: readFile9 } = await import('fs/promises');
8665
9776
  let apiPrivateKey = "";
8666
9777
  if (opts.apiKey) {
8667
- apiPrivateKey = await readFile8(opts.apiKey, "utf-8");
9778
+ apiPrivateKey = await readFile9(opts.apiKey, "utf-8");
8668
9779
  }
8669
9780
  console.log(`Uploading ${opts.ipa}...`);
8670
9781
  const result = await uploadToTestFlight2({
@@ -8696,7 +9807,10 @@ function createPilotCommand() {
8696
9807
  return cmd;
8697
9808
  }
8698
9809
  function createDeliverCommand() {
8699
- return new Command("deliver").description("Upload metadata and binary to App Store Connect").option("-a, --app-identifier <id>", "Bundle identifier").option("-i, --ipa <path>", "Path to IPA file").option("-m, --metadata-path <path>", "Path to metadata directory").option("--screenshots-path <path>", "Path to screenshots directory").option("--submit-for-review", "Submit for review after upload").option("--skip-binary-upload", "Skip binary upload").option("--skip-metadata", "Skip metadata upload").option("--skip-screenshots", "Skip screenshot upload").option("--app-version <version>", "App version string").action(async (opts) => {
9810
+ return new Command("deliver").description("Upload metadata and binary to App Store Connect").option("-a, --app-identifier <id>", "Bundle identifier").option("-i, --ipa <path>", "Path to IPA file").option("-m, --metadata-path <path>", "Path to metadata directory").option("--screenshots-path <path>", "Path to screenshots directory").option("--submit-for-review", "Submit for review after upload").option("--skip-binary-upload", "Skip binary upload").option("--skip-metadata", "Skip metadata upload").option("--skip-screenshots", "Skip screenshot upload").option(
9811
+ "--assets-path <path>",
9812
+ "App Asset Library folder: <locale>/{screenshots,previews,header,search_results}/"
9813
+ ).option("--asset-library", "Upload --screenshots-path through the App Asset Library (screenshot sets are deprecated)").option("--overwrite-assets", "Replace existing placements in each slot that gets new files").option("--skip-assets", "Skip asset library upload").option("--app-version <version>", "App version string").action(async (opts) => {
8700
9814
  const { resolveClientFromEnv: resolveClientFromEnv2 } = await Promise.resolve().then(() => (init_options(), options_exports));
8701
9815
  const { uploadToAppStore: uploadToAppStore2 } = await Promise.resolve().then(() => (init_deliver(), deliver_exports));
8702
9816
  const client = await resolveClientFromEnv2();
@@ -8715,7 +9829,12 @@ function createDeliverCommand() {
8715
9829
  submitForReview: opts.submitForReview,
8716
9830
  skipBinaryUpload: opts.skipBinaryUpload,
8717
9831
  skipMetadata: opts.skipMetadata,
8718
- skipScreenshots: opts.skipScreenshots
9832
+ skipScreenshots: opts.skipScreenshots,
9833
+ assetsPath: opts.assetsPath,
9834
+ useAssetLibrary: opts.assetLibrary,
9835
+ overwriteAssets: opts.overwriteAssets,
9836
+ skipAssets: opts.skipAssets,
9837
+ onOutput: (chunk) => process.stdout.write(chunk)
8719
9838
  });
8720
9839
  console.log(`App: ${result.appId}`);
8721
9840
  console.log(`Version: ${result.versionId}`);
@@ -8819,6 +9938,145 @@ function createLanesCommand() {
8819
9938
  }
8820
9939
  });
8821
9940
  }
9941
+ function createPrivacyCommand() {
9942
+ return new Command("privacy").description("Upload App Privacy details (nutrition labels) to App Store Connect").requiredOption("--app-id <id>", "App Store Connect app id (numeric Apple ID)").requiredOption("-j, --json-path <path>", "Fastlane-format app_privacy_details.json").option("-u, --username <email>", "Apple ID", process.env.FASTNODE_USER ?? process.env.FASTLANE_USER).option("--skip-publish", "Save the answers without publishing them", false).action(async (opts) => {
9943
+ const { loginWithAppleId: loginWithAppleId2 } = await Promise.resolve().then(() => (init_apple_session(), apple_session_exports));
9944
+ const { uploadAppPrivacyDetails: uploadAppPrivacyDetails2, validateUsages: validateUsages2 } = await Promise.resolve().then(() => (init_privacy(), privacy_exports));
9945
+ const usages = JSON.parse(readFileSync(opts.jsonPath, "utf8"));
9946
+ validateUsages2(usages);
9947
+ const email = opts.username || await ask("Apple ID: ");
9948
+ const password = process.env.FASTNODE_PASSWORD ?? process.env.FASTLANE_PASSWORD ?? await ask(`Password for ${email}: `, true);
9949
+ const session = await loginWithAppleId2({ email, password, onLog: (m) => console.log(m) });
9950
+ const result = await uploadAppPrivacyDetails2({
9951
+ session,
9952
+ appId: opts.appId,
9953
+ usages,
9954
+ skipPublish: opts.skipPublish,
9955
+ onLog: (m) => console.log(m)
9956
+ });
9957
+ console.log(`Done: ${result.created} answers${result.published ? ", published" : ""}.`);
9958
+ });
9959
+ }
9960
+ function ask(question, secret = false) {
9961
+ let muted = false;
9962
+ const output = new Writable({
9963
+ write(chunk, _enc, cb) {
9964
+ if (!muted) process.stdout.write(chunk);
9965
+ cb();
9966
+ }
9967
+ });
9968
+ const rl = createInterface({ input: process.stdin, output, terminal: true });
9969
+ return new Promise((resolve4) => {
9970
+ rl.question(question, (answer) => {
9971
+ rl.close();
9972
+ if (secret) process.stdout.write("\n");
9973
+ resolve4(answer.trim());
9974
+ });
9975
+ muted = secret;
9976
+ });
9977
+ }
9978
+ function createRejectionsCommand() {
9979
+ return new Command("rejections").description("Show App Review's Resolution Center messages and rejection reasons").requiredOption("--app-id <ids...>", "App Store Connect app id(s) (numeric Apple ID)").option("-u, --username <email>", "Apple ID", process.env.FASTNODE_USER ?? process.env.FASTLANE_USER).option("--all", "Include resolved submissions, not just ones awaiting you", false).option("--json", "Print JSON instead of text", false).action(async (opts) => {
9980
+ const { loginWithAppleId: loginWithAppleId2 } = await Promise.resolve().then(() => (init_apple_session(), apple_session_exports));
9981
+ const { getReviewFeedback: getReviewFeedback2 } = await Promise.resolve().then(() => (init_review(), review_exports));
9982
+ const email = opts.username || await ask2("Apple ID: ");
9983
+ const password = process.env.FASTNODE_PASSWORD ?? process.env.FASTLANE_PASSWORD ?? await ask2(`Password for ${email}: `, true);
9984
+ const session = await loginWithAppleId2({ email, password, onLog: (m) => console.error(m) });
9985
+ const all = {};
9986
+ for (const appId of opts.appId) {
9987
+ const feedback = await getReviewFeedback2(session, appId, { onlyOpen: !opts.all, onLog: (m) => console.error(m) });
9988
+ all[appId] = feedback;
9989
+ if (opts.json) continue;
9990
+ console.log(`
9991
+ \u2550\u2550 App ${appId} \u2550\u2550`);
9992
+ if (!feedback.length) console.log("No open App Review feedback.");
9993
+ for (const s of feedback) {
9994
+ console.log(`
9995
+ Submission ${s.submissionId} \u2014 ${s.state} (submitted ${s.submittedDate ?? "?"})`);
9996
+ for (const t of s.threads) {
9997
+ console.log(` Thread ${t.threadType ?? "?"} \xB7 ${t.state ?? "?"} \xB7 ${t.createdDate ?? ""}`);
9998
+ for (const m of t.messages) {
9999
+ console.log(`
10000
+ \u2500\u2500 ${m.from ?? "App Review"} \xB7 ${m.createdDate ?? ""}`);
10001
+ for (const r of m.reasons) console.log(` \u25B8 ${[r.section, r.description].filter(Boolean).join(" \u2014 ")}${r.code ? ` [${r.code}]` : ""}`);
10002
+ console.log(m.body.split("\n").map((l) => ` ${l}`).join("\n"));
10003
+ }
10004
+ }
10005
+ }
10006
+ }
10007
+ if (opts.json) console.log(JSON.stringify(all, null, 2));
10008
+ });
10009
+ }
10010
+ function ask2(question, secret = false) {
10011
+ let muted = false;
10012
+ const output = new Writable({ write(chunk, _enc, cb) {
10013
+ if (!muted) process.stdout.write(chunk);
10014
+ cb();
10015
+ } });
10016
+ const rl = createInterface({ input: process.stdin, output, terminal: true });
10017
+ return new Promise((resolve4) => {
10018
+ rl.question(question, (a) => {
10019
+ rl.close();
10020
+ if (secret) process.stdout.write("\n");
10021
+ resolve4(a.trim());
10022
+ });
10023
+ muted = secret;
10024
+ });
10025
+ }
10026
+ function createAssetsCommand() {
10027
+ const assets = new Command("assets").description(
10028
+ "App Store asset library \u2014 screenshots, previews, product page headers, search results assets"
10029
+ );
10030
+ assets.command("specs").description("List placement groups and the image sizes Apple accepts for each (from ASC reference data)").option("--feature <feature>", "APP_STORE_VERSIONS, CUSTOM_PRODUCT_PAGES, IN_APP_EVENTS, \u2026", "APP_STORE_VERSIONS").option("--placement-type <type>", "e.g. APP_SCREENSHOT, PRODUCT_PAGE_HEADER_ASSET").option("--json", "Print the raw reference data").action(async (opts) => {
10031
+ const { resolveClientFromEnv: resolveClientFromEnv2 } = await Promise.resolve().then(() => (init_options(), options_exports));
10032
+ const client = await resolveClientFromEnv2();
10033
+ const feature = opts.feature;
10034
+ const refData = await client.appAssetLibraries.refData({
10035
+ features: [feature],
10036
+ placementTypes: opts.placementType ? [opts.placementType] : void 0
10037
+ });
10038
+ if (opts.json) {
10039
+ console.log(JSON.stringify(refData, null, 2));
10040
+ return;
10041
+ }
10042
+ const policies = refData.features?.find((f) => f.featureId === feature)?.placementPolicies ?? [];
10043
+ const specs = new Map([...refData.imageSpecs ?? [], ...refData.videoSpecs ?? []].map((s) => [s.specId, s]));
10044
+ for (const type of refData.placementTypes ?? []) {
10045
+ if (opts.placementType && type.placementTypeId !== opts.placementType) continue;
10046
+ const policy = policies.find((p) => p.placementType === type.placementTypeId);
10047
+ if (policies.length && !policy) continue;
10048
+ console.log(`
10049
+ ${type.placementTypeId} (${(type.acceptsAssetCategories ?? []).join(", ")})`);
10050
+ for (const mapping of type.specMappings ?? []) {
10051
+ const max = policy?.groupLimits?.find((l) => l.groupIds.includes(mapping.placementGroupId))?.maxCount;
10052
+ const sizes = mapping.specs.map((id) => specs.get(id)).filter((s) => Boolean(s)).map((s) => {
10053
+ const d = s.dimensions;
10054
+ const w = d.minWidth === d.maxWidth ? `${d.minWidth}` : `${d.minWidth}-${d.maxWidth}`;
10055
+ const h = d.minHeight === d.maxHeight ? `${d.minHeight}` : `${d.minHeight}-${d.maxHeight}`;
10056
+ return `${w}\xD7${h}`;
10057
+ });
10058
+ console.log(` ${mapping.placementGroupId.padEnd(44)} ${max ? `max ${max} ` : ""}${sizes.join(", ")}`);
10059
+ }
10060
+ }
10061
+ });
10062
+ assets.command("list").description("List images and videos in an app's asset library").requiredOption("-a, --app-identifier <id>", "Bundle identifier").action(async (opts) => {
10063
+ const { resolveClientFromEnv: resolveClientFromEnv2 } = await Promise.resolve().then(() => (init_options(), options_exports));
10064
+ const client = await resolveClientFromEnv2();
10065
+ const app = await client.apps.findByBundleId(opts.appIdentifier);
10066
+ if (!app) throw new Error(`App not found: ${opts.appIdentifier}`);
10067
+ const library = await client.appAssetLibraries.getForApp(app.id);
10068
+ const [images, videos] = await Promise.all([
10069
+ client.appAssetLibraries.listImages(library.id),
10070
+ client.appAssetLibraries.listVideos(library.id)
10071
+ ]);
10072
+ for (const a of [...images, ...videos]) {
10073
+ const at = a.attributes;
10074
+ console.log(`${a.type === "appAssetLibraryImages" ? "image" : "video"} ${a.id} ${String(at.state).padEnd(22)} ${at.category} ${at.referenceName ?? at.fileName}`);
10075
+ }
10076
+ if (!images.length && !videos.length) console.log("Asset library is empty");
10077
+ });
10078
+ return assets;
10079
+ }
8822
10080
 
8823
10081
  // src/cli/index.ts
8824
10082
  init_options();
@@ -8837,6 +10095,9 @@ function createCli() {
8837
10095
  program.addCommand(createMatchCommand());
8838
10096
  program.addCommand(createActionsCommand());
8839
10097
  program.addCommand(createLanesCommand());
10098
+ program.addCommand(createPrivacyCommand());
10099
+ program.addCommand(createRejectionsCommand());
10100
+ program.addCommand(createAssetsCommand());
8840
10101
  return program;
8841
10102
  }
8842
10103