@sitepong/fastnode 0.1.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 (162) hide show
  1. package/README.md +81 -0
  2. package/bin/fastnode.js +13 -0
  3. package/bin/fastnode.ts +5 -0
  4. package/dist/actions/index.cjs +7405 -0
  5. package/dist/actions/index.cjs.map +1 -0
  6. package/dist/actions/index.d.cts +15 -0
  7. package/dist/actions/index.d.ts +15 -0
  8. package/dist/actions/index.mjs +7377 -0
  9. package/dist/actions/index.mjs.map +1 -0
  10. package/dist/cert/index.cjs +219 -0
  11. package/dist/cert/index.cjs.map +1 -0
  12. package/dist/cert/index.d.cts +4 -0
  13. package/dist/cert/index.d.ts +4 -0
  14. package/dist/cert/index.mjs +191 -0
  15. package/dist/cert/index.mjs.map +1 -0
  16. package/dist/cert-BmJGtYtb.d.cts +27 -0
  17. package/dist/cert-owkOFNW8.d.ts +27 -0
  18. package/dist/cli/index.cjs +7998 -0
  19. package/dist/cli/index.cjs.map +1 -0
  20. package/dist/cli/index.d.cts +9 -0
  21. package/dist/cli/index.d.ts +9 -0
  22. package/dist/cli/index.mjs +7972 -0
  23. package/dist/cli/index.mjs.map +1 -0
  24. package/dist/client/index.cjs +2592 -0
  25. package/dist/client/index.cjs.map +1 -0
  26. package/dist/client/index.d.cts +2 -0
  27. package/dist/client/index.d.ts +2 -0
  28. package/dist/client/index.mjs +2582 -0
  29. package/dist/client/index.mjs.map +1 -0
  30. package/dist/context-CymLkfrS.d.cts +13 -0
  31. package/dist/context-CymLkfrS.d.ts +13 -0
  32. package/dist/core/index.cjs +281 -0
  33. package/dist/core/index.cjs.map +1 -0
  34. package/dist/core/index.d.cts +1 -0
  35. package/dist/core/index.d.ts +1 -0
  36. package/dist/core/index.mjs +254 -0
  37. package/dist/core/index.mjs.map +1 -0
  38. package/dist/deliver/index.cjs +328 -0
  39. package/dist/deliver/index.cjs.map +1 -0
  40. package/dist/deliver/index.d.cts +3 -0
  41. package/dist/deliver/index.d.ts +3 -0
  42. package/dist/deliver/index.mjs +322 -0
  43. package/dist/deliver/index.mjs.map +1 -0
  44. package/dist/enums-B609j2rb.d.cts +20 -0
  45. package/dist/enums-B609j2rb.d.ts +20 -0
  46. package/dist/frameit/index.cjs +247 -0
  47. package/dist/frameit/index.cjs.map +1 -0
  48. package/dist/frameit/index.d.cts +1 -0
  49. package/dist/frameit/index.d.ts +1 -0
  50. package/dist/frameit/index.mjs +242 -0
  51. package/dist/frameit/index.mjs.map +1 -0
  52. package/dist/gym/index.cjs +356 -0
  53. package/dist/gym/index.cjs.map +1 -0
  54. package/dist/gym/index.d.cts +2 -0
  55. package/dist/gym/index.d.ts +2 -0
  56. package/dist/gym/index.mjs +346 -0
  57. package/dist/gym/index.mjs.map +1 -0
  58. package/dist/index-1Vu2cRQ2.d.ts +99 -0
  59. package/dist/index-A5MimeQk.d.ts +72 -0
  60. package/dist/index-B0yhu_LZ.d.ts +76 -0
  61. package/dist/index-B9yXicrQ.d.ts +62 -0
  62. package/dist/index-BFdNOmPJ.d.cts +53 -0
  63. package/dist/index-BLubSlgK.d.cts +1513 -0
  64. package/dist/index-BN0ubxjZ.d.ts +70 -0
  65. package/dist/index-BQJIJlK7.d.cts +70 -0
  66. package/dist/index-BYonklf3.d.cts +84 -0
  67. package/dist/index-BYonklf3.d.ts +84 -0
  68. package/dist/index-BimqCmY8.d.cts +78 -0
  69. package/dist/index-BimqCmY8.d.ts +78 -0
  70. package/dist/index-BoU8qt8r.d.cts +59 -0
  71. package/dist/index-BoU8qt8r.d.ts +59 -0
  72. package/dist/index-C2yG3wXA.d.cts +95 -0
  73. package/dist/index-C2yG3wXA.d.ts +95 -0
  74. package/dist/index-CR3BFPIz.d.cts +77 -0
  75. package/dist/index-CR3BFPIz.d.ts +77 -0
  76. package/dist/index-CWNLnSOL.d.cts +99 -0
  77. package/dist/index-Cp6cjtd-.d.cts +65 -0
  78. package/dist/index-Cp6cjtd-.d.ts +65 -0
  79. package/dist/index-D1Izm7qt.d.ts +1513 -0
  80. package/dist/index-D5eihzwM.d.cts +76 -0
  81. package/dist/index-DImN9PWc.d.ts +155 -0
  82. package/dist/index-DfCUVJX1.d.cts +62 -0
  83. package/dist/index-DfirmFbC.d.ts +32 -0
  84. package/dist/index-DkvinY7_.d.ts +54 -0
  85. package/dist/index-DrUpRXRG.d.cts +155 -0
  86. package/dist/index-EcVvViW8.d.cts +72 -0
  87. package/dist/index-HRiaaX-Q.d.cts +54 -0
  88. package/dist/index-TfXqwuac.d.ts +53 -0
  89. package/dist/index-lt819Jgj.d.cts +32 -0
  90. package/dist/index.cjs +8464 -0
  91. package/dist/index.cjs.map +1 -0
  92. package/dist/index.d.cts +23 -0
  93. package/dist/index.d.ts +23 -0
  94. package/dist/index.mjs +8410 -0
  95. package/dist/index.mjs.map +1 -0
  96. package/dist/lane/index.cjs +7641 -0
  97. package/dist/lane/index.cjs.map +1 -0
  98. package/dist/lane/index.d.cts +51 -0
  99. package/dist/lane/index.d.ts +51 -0
  100. package/dist/lane/index.mjs +7603 -0
  101. package/dist/lane/index.mjs.map +1 -0
  102. package/dist/match/index.cjs +942 -0
  103. package/dist/match/index.cjs.map +1 -0
  104. package/dist/match/index.d.cts +5 -0
  105. package/dist/match/index.d.ts +5 -0
  106. package/dist/match/index.mjs +913 -0
  107. package/dist/match/index.mjs.map +1 -0
  108. package/dist/pem/index.cjs +195 -0
  109. package/dist/pem/index.cjs.map +1 -0
  110. package/dist/pem/index.d.cts +3 -0
  111. package/dist/pem/index.d.ts +3 -0
  112. package/dist/pem/index.mjs +170 -0
  113. package/dist/pem/index.mjs.map +1 -0
  114. package/dist/pilot/index.cjs +206 -0
  115. package/dist/pilot/index.cjs.map +1 -0
  116. package/dist/pilot/index.d.cts +3 -0
  117. package/dist/pilot/index.d.ts +3 -0
  118. package/dist/pilot/index.mjs +196 -0
  119. package/dist/pilot/index.mjs.map +1 -0
  120. package/dist/precheck/index.cjs +277 -0
  121. package/dist/precheck/index.cjs.map +1 -0
  122. package/dist/precheck/index.d.cts +3 -0
  123. package/dist/precheck/index.d.ts +3 -0
  124. package/dist/precheck/index.mjs +274 -0
  125. package/dist/precheck/index.mjs.map +1 -0
  126. package/dist/produce/index.cjs +200 -0
  127. package/dist/produce/index.cjs.map +1 -0
  128. package/dist/produce/index.d.cts +3 -0
  129. package/dist/produce/index.d.ts +3 -0
  130. package/dist/produce/index.mjs +191 -0
  131. package/dist/produce/index.mjs.map +1 -0
  132. package/dist/scan/index.cjs +187 -0
  133. package/dist/scan/index.cjs.map +1 -0
  134. package/dist/scan/index.d.cts +1 -0
  135. package/dist/scan/index.d.ts +1 -0
  136. package/dist/scan/index.mjs +183 -0
  137. package/dist/scan/index.mjs.map +1 -0
  138. package/dist/sigh/index.cjs +203 -0
  139. package/dist/sigh/index.cjs.map +1 -0
  140. package/dist/sigh/index.d.cts +3 -0
  141. package/dist/sigh/index.d.ts +3 -0
  142. package/dist/sigh/index.mjs +198 -0
  143. package/dist/sigh/index.mjs.map +1 -0
  144. package/dist/snapshot/index.cjs +448 -0
  145. package/dist/snapshot/index.cjs.map +1 -0
  146. package/dist/snapshot/index.d.cts +1 -0
  147. package/dist/snapshot/index.d.ts +1 -0
  148. package/dist/snapshot/index.mjs +438 -0
  149. package/dist/snapshot/index.mjs.map +1 -0
  150. package/dist/trainer/index.cjs +395 -0
  151. package/dist/trainer/index.cjs.map +1 -0
  152. package/dist/trainer/index.d.cts +1 -0
  153. package/dist/trainer/index.d.ts +1 -0
  154. package/dist/trainer/index.mjs +372 -0
  155. package/dist/trainer/index.mjs.map +1 -0
  156. package/dist/xcode/index.cjs +188 -0
  157. package/dist/xcode/index.cjs.map +1 -0
  158. package/dist/xcode/index.d.cts +1 -0
  159. package/dist/xcode/index.d.ts +1 -0
  160. package/dist/xcode/index.mjs +176 -0
  161. package/dist/xcode/index.mjs.map +1 -0
  162. package/package.json +223 -0
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+
3
+ var child_process = require('child_process');
4
+ var fs = require('fs');
5
+ var promises = require('fs/promises');
6
+ var path = require('path');
7
+ var os = require('os');
8
+ var emojiRegex = require('emoji-regex');
9
+ require('crypto');
10
+ require('readline');
11
+
12
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
+
14
+ var emojiRegex__default = /*#__PURE__*/_interopDefault(emojiRegex);
15
+
16
+ // src/pilot/index.ts
17
+ BigInt(
18
+ "0xAC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73"
19
+ );
20
+
21
+ // src/client/request.ts
22
+ var AppStoreConnectError = class extends Error {
23
+ constructor(message, status, errors = []) {
24
+ super(message);
25
+ this.status = status;
26
+ this.errors = errors;
27
+ this.name = "AppStoreConnectError";
28
+ }
29
+ status;
30
+ errors;
31
+ };
32
+
33
+ // src/pilot/index.ts
34
+ async function uploadToTestFlight(options) {
35
+ if (!fs.existsSync(options.ipaPath)) {
36
+ throw new Error(`File not found: ${options.ipaPath}`);
37
+ }
38
+ const platform = options.platform ?? "ios";
39
+ const timeout = options.timeout ?? 18e5;
40
+ const keyDir = await promises.mkdtemp(path.join(os.tmpdir(), "fastnode-key-"));
41
+ const keyPath = path.join(keyDir, `AuthKey_${options.apiKeyId}.p8`);
42
+ await promises.writeFile(keyPath, options.apiPrivateKey);
43
+ return new Promise((resolve) => {
44
+ const args = [
45
+ "altool",
46
+ "--upload-app",
47
+ "-f",
48
+ options.ipaPath,
49
+ "-t",
50
+ platform,
51
+ "--apiKey",
52
+ options.apiKeyId,
53
+ "--apiIssuer",
54
+ options.apiIssuerId
55
+ ];
56
+ const child = child_process.spawn("xcrun", args, {
57
+ env: { ...process.env, API_PRIVATE_KEYS_DIR: keyDir },
58
+ stdio: ["ignore", "pipe", "pipe"]
59
+ });
60
+ let output = "";
61
+ const timer = setTimeout(() => {
62
+ child.kill("SIGTERM");
63
+ }, timeout);
64
+ child.stdout.on("data", (data) => {
65
+ const chunk = data.toString();
66
+ output += chunk;
67
+ options.onOutput?.(chunk);
68
+ });
69
+ child.stderr.on("data", (data) => {
70
+ const chunk = data.toString();
71
+ output += chunk;
72
+ options.onOutput?.(chunk);
73
+ });
74
+ child.on("close", async (code) => {
75
+ clearTimeout(timer);
76
+ await promises.unlink(keyPath).catch(() => {
77
+ });
78
+ resolve({ success: code === 0, output });
79
+ });
80
+ });
81
+ }
82
+ async function waitForProcessing(client, params) {
83
+ const timeout = params.timeout ?? 18e5;
84
+ const pollInterval = params.pollInterval ?? 3e4;
85
+ const deadline = Date.now() + timeout;
86
+ const app = await client.apps.findByBundleId(params.bundleId);
87
+ if (!app) throw new Error(`App not found for bundle ID: ${params.bundleId}`);
88
+ while (Date.now() < deadline) {
89
+ const builds = await client.builds.list({
90
+ filter: {
91
+ app: app.id,
92
+ version: params.buildNumber,
93
+ "preReleaseVersion.version": params.appVersion
94
+ },
95
+ limit: 1
96
+ });
97
+ if (builds.length > 0) {
98
+ const build = builds[0];
99
+ const state = build.attributes.processingState;
100
+ if (state === "VALID") return build;
101
+ if (state === "FAILED" || state === "INVALID") {
102
+ throw new Error(`Build processing ${state}: ${params.buildNumber}`);
103
+ }
104
+ }
105
+ await sleep(pollInterval);
106
+ }
107
+ throw new Error(
108
+ `Timeout waiting for build ${params.buildNumber} after ${timeout / 1e3}s`
109
+ );
110
+ }
111
+ async function distributeToGroups(options) {
112
+ const groupNames = options.groupNames ?? [];
113
+ if (groupNames.length === 0) return [];
114
+ const build = await options.client.builds.get(options.buildId, {
115
+ include: "app"
116
+ });
117
+ const appId = build.relationships?.app?.data?.id;
118
+ if (!appId) throw new Error("Could not determine app for build");
119
+ if (options.changelog) {
120
+ await setChangelog({
121
+ client: options.client,
122
+ buildId: options.buildId,
123
+ changelog: options.changelog,
124
+ locale: options.locale
125
+ });
126
+ }
127
+ if (options.expirePreviousBuilds) {
128
+ const allBuilds = await options.client.builds.list({
129
+ filter: { app: appId, expired: "false" },
130
+ limit: 50
131
+ });
132
+ for (const b of allBuilds) {
133
+ if (b.id !== options.buildId) {
134
+ await options.client.builds.expireBuild(b.id).catch(() => {
135
+ });
136
+ }
137
+ }
138
+ }
139
+ const allGroups = await options.client.betaGroups.listForApp(appId);
140
+ const matched = [];
141
+ for (const name of groupNames) {
142
+ const group = allGroups.find((g) => g.attributes.name === name);
143
+ if (!group) continue;
144
+ try {
145
+ await options.client.betaGroups.addBuilds(group.id, [options.buildId]);
146
+ matched.push(name);
147
+ } catch (err) {
148
+ if (err instanceof AppStoreConnectError && err.status === 409) {
149
+ matched.push(name);
150
+ }
151
+ }
152
+ }
153
+ if (options.submitForReview) {
154
+ try {
155
+ await options.client.builds.submitForBetaReview(options.buildId);
156
+ } catch {
157
+ }
158
+ }
159
+ return matched;
160
+ }
161
+ async function setChangelog(options) {
162
+ const locale = options.locale ?? "en-US";
163
+ const sanitized = sanitizeChangelog(options.changelog);
164
+ const localizations = await options.client.builds.getLocalizations(
165
+ options.buildId
166
+ );
167
+ const existing = localizations.find(
168
+ (l) => l.attributes.locale === locale
169
+ );
170
+ if (existing) {
171
+ await options.client.builds.updateLocalization(existing.id, {
172
+ whatsNew: sanitized
173
+ });
174
+ } else {
175
+ await options.client.builds.createLocalization({
176
+ buildId: options.buildId,
177
+ locale,
178
+ whatsNew: sanitized
179
+ });
180
+ }
181
+ }
182
+ async function expireBuild(client, buildId) {
183
+ await client.builds.expireBuild(buildId);
184
+ }
185
+ function sanitizeChangelog(text) {
186
+ let sanitized = text.replace(emojiRegex__default.default(), "");
187
+ sanitized = sanitized.replace(/</g, "");
188
+ const encoder = new TextEncoder();
189
+ const encoded = encoder.encode(sanitized);
190
+ if (encoded.length > 4e3) {
191
+ const decoder = new TextDecoder();
192
+ sanitized = decoder.decode(encoded.slice(0, 3997)) + "...";
193
+ }
194
+ return sanitized;
195
+ }
196
+ function sleep(ms) {
197
+ return new Promise((resolve) => setTimeout(resolve, ms));
198
+ }
199
+
200
+ exports.distributeToGroups = distributeToGroups;
201
+ exports.expireBuild = expireBuild;
202
+ exports.setChangelog = setChangelog;
203
+ exports.uploadToTestFlight = uploadToTestFlight;
204
+ exports.waitForProcessing = waitForProcessing;
205
+ //# sourceMappingURL=index.cjs.map
206
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/srp.ts","../../src/client/request.ts","../../src/pilot/index.ts"],"names":["existsSync","mkdtemp","join","tmpdir","writeFile","spawn","unlink","emojiRegex"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * SRP (Secure Remote Password) implementation\n * 1:1 port of the fastlane-sirp Ruby gem\n * Used for Apple ID authentication\n */\n\nimport { createHash, randomBytes, pbkdf2Sync } from \"node:crypto\";\n\n// ── RFC 5054 SRP Group Parameters ──────────────────────────────────────────\n\n// Copied exactly from fastlane-sirp parameters.rb (2048-bit group)\nconst N_2048 = BigInt(\n \"0x\" +\n \"AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050\" +\n \"A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50\" +\n \"E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B8\" +\n \"55F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773B\" +\n \"CA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748\" +\n \"544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6\" +\n \"AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB\" +\n \"694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73\"\n);\nconst g_2048 = 2n;\n\n// ── Utility Functions ──────────────────────────────────────────────────────\n\nfunction numToHex(num: bigint): string {\n let hex = num.toString(16);\n if (hex.length % 2 !== 0) hex = \"0\" + hex;\n return hex.toLowerCase();\n}\n\nfunction hexToBytes(hex: string): Buffer {\n return Buffer.from(hex, \"hex\");\n}\n\nfunction shaHex(h: string, algorithm: string): string {\n return createHash(algorithm).update(hexToBytes(h)).digest(\"hex\");\n}\n\nfunction shaStr(s: string, algorithm: string): string {\n return createHash(algorithm).update(s).digest(\"hex\");\n}\n\nfunction modExp(base: bigint, exp: bigint, mod: bigint): bigint {\n if (mod === 1n) return 0n;\n let result = 1n;\n base = base % mod;\n while (exp > 0n) {\n if (exp % 2n === 1n) {\n result = (result * base) % mod;\n }\n exp = exp / 2n;\n base = (base * base) % mod;\n }\n return result;\n}\n\nfunction H(algorithm: string, n: bigint, ...args: (bigint | string | undefined)[]): bigint {\n const nHex = numToHex(n);\n // Must match Ruby: nlen = 2 * ((('%x' % [n]).length * 4 + 7) >> 3)\n // Use floor division (>>3), NOT ceiling\n const nlen = 2 * (((nHex.length * 4 + 7) >> 3));\n\n let hashin = \"\";\n for (const s of args) {\n if (s === undefined) continue;\n const shex = typeof s === \"string\" ? s : numToHex(s);\n if (shex.length > nlen) {\n throw new Error(\"Bit width does not match - client uses different prime\");\n }\n hashin += \"0\".repeat(nlen - shex.length) + shex;\n }\n\n const hashed = shaHex(hashin, algorithm);\n return BigInt(\"0x\" + hashed) % n;\n}\n\n// ── SRP Client ─────────────────────────────────────────────────────────────\n\nexport class SRPClient {\n private N: bigint;\n private g: bigint;\n private hashAlg: string;\n private k: bigint;\n private a: bigint;\n A: string;\n private S: string | null = null;\n private K: string | null = null;\n M: string | null = null;\n H_AMK: string | null = null;\n\n constructor(group: number = 2048) {\n if (group !== 2048) throw new Error(\"Only 2048-bit group is supported\");\n this.N = N_2048;\n this.g = g_2048;\n this.hashAlg = \"sha256\";\n this.k = H(this.hashAlg, this.N, this.N, this.g);\n // Generate random 'a'\n this.a = BigInt(\"0x\" + randomBytes(256).toString(\"hex\"));\n // Compute A = g^a mod N\n this.A = numToHex(modExp(this.g, this.a, this.N));\n }\n\n startAuthentication(): string {\n return this.A;\n }\n\n processChallenge(\n username: string,\n password: string, // hex-encoded encrypted password\n xsalt: string, // hex\n xbb: string, // hex\n isPasswordEncrypted: boolean = false\n ): string | false {\n const bb = BigInt(\"0x\" + xbb);\n\n // SRP-6a safety check\n if (bb % this.N === 0n) return false;\n\n let x: bigint;\n if (isPasswordEncrypted) {\n x = this.calcXHex(password, xsalt);\n } else {\n x = this.calcX(username, password, xsalt);\n }\n\n const u = H(this.hashAlg, this.N, this.A, xbb);\n if (u === 0n) return false;\n\n // S = (B - k * g^x) ^ (a + u*x) mod N\n const gx = modExp(this.g, x, this.N);\n let base = (bb - this.k * gx) % this.N;\n if (base < 0n) base += this.N;\n const exp = this.a + x * u;\n this.S = numToHex(modExp(base, exp, this.N));\n this.K = shaHex(this.S, this.hashAlg);\n\n // M = H(H(N) xor H(g), H(I), s, A, B, K)\n const hxor = H(this.hashAlg, this.N, this.N) ^ H(this.hashAlg, this.N, this.g);\n const buf =\n numToHex(hxor) +\n shaStr(username, this.hashAlg) +\n xsalt +\n this.A +\n xbb +\n this.K;\n const bytes = hexToBytes(buf);\n this.M = createHash(this.hashAlg).update(bytes).digest(\"hex\");\n\n // H_AMK = H(A, M, K)\n const amkBytes = hexToBytes(this.A + this.M + this.K);\n this.H_AMK = numToHex(\n BigInt(\"0x\" + createHash(this.hashAlg).update(amkBytes).digest(\"hex\"))\n );\n\n return this.M;\n }\n\n private calcX(username: string, password: string, xsalt: string): bigint {\n const spad = xsalt.length % 2 !== 0 ? \"0\" : \"\";\n const inner = shaStr(`${username}:${password}`, this.hashAlg);\n return BigInt(\"0x\" + shaHex(spad + xsalt + inner, this.hashAlg));\n }\n\n private calcXHex(xpassword: string, xsalt: string): bigint {\n // calc_x_hex: sha_hex(xsalt + sha_hex(\":\" hex + xpassword), hash)\n const colonHex = \":\".charCodeAt(0).toString(16);\n const inner = shaHex(colonHex + xpassword, this.hashAlg);\n return BigInt(\"0x\" + shaHex(xsalt + inner, this.hashAlg));\n }\n}\n\n// ── Apple PBKDF2 password derivation ───────────────────────────────────────\n\nexport function applePbkdf2(\n password: string,\n salt: Buffer,\n iterations: number,\n keyLength: number,\n protocol: string\n): Buffer {\n if (![\"s2k\", \"s2k_fo\"].includes(protocol)) {\n throw new Error(`Unsupported protocol '${protocol}' for pbkdf2`);\n }\n\n // First hash the password with SHA-256\n let passwordBytes: Buffer | string = createHash(\"sha256\").update(password).digest();\n\n if (protocol === \"s2k_fo\") {\n // Legacy: convert to hex string\n passwordBytes = passwordBytes.toString(\"hex\");\n }\n\n return pbkdf2Sync(passwordBytes, salt, iterations, keyLength, \"sha256\");\n}\n","import type { TokenManager } from \"./auth.js\";\nimport { ProgramLicenseAgreementError } from \"./apple-session.js\";\nimport type {\n JsonApiErrorResponse,\n JsonApiListResponse,\n JsonApiResponse,\n ListParams,\n RequestOptions,\n} from \"./types.js\";\n\nconst PRODUCTION_BASE = \"https://api.appstoreconnect.apple.com\";\nconst ENTERPRISE_BASE = \"https://api.enterprise.developer.apple.com\";\n\nconst MAX_RETRIES = 5;\nconst INITIAL_BACKOFF_MS = 1000;\nconst MAX_BACKOFF_MS = 60_000;\n/** Fastlane's TooManyRequestsError gives up after 3600s cumulative backoff. */\nconst MAX_RATE_LIMIT_BACKOFF_MS = 3_600_000;\n\nexport class AppStoreConnectError extends Error {\n constructor(\n message: string,\n public status: number,\n public errors: JsonApiErrorResponse[\"errors\"] = []\n ) {\n super(message);\n this.name = \"AppStoreConnectError\";\n }\n}\n\nexport class RateLimitError extends AppStoreConnectError {\n constructor(public retryAfter: number) {\n super(`Rate limited. Retry after ${retryAfter}s`, 429);\n this.name = \"RateLimitError\";\n }\n}\n\nexport class RequestClient {\n private baseUrl: string;\n private tokenManager: TokenManager;\n\n constructor(tokenManager: TokenManager, enterprise = false) {\n this.tokenManager = tokenManager;\n this.baseUrl = enterprise ? ENTERPRISE_BASE : PRODUCTION_BASE;\n }\n\n async request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const { method = \"GET\", body, params, headers: extraHeaders } = options;\n\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n searchParams.set(key, value.join(\",\"));\n } else {\n searchParams.set(key, String(value));\n }\n }\n const qs = searchParams.toString();\n if (qs) url += `?${qs}`;\n }\n\n let lastError: Error | null = null;\n // Soft-cumulative backoff for 429 that doubles on each hit, mirroring\n // Fastlane's Spaceship::ConnectAPI::APIClient#with_asc_retry 429 handler:\n // `Kernel.sleep(backoff); backoff *= 2`, giving up past 3600s.\n let rateLimitBackoffMs = INITIAL_BACKOFF_MS;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n const token = await this.tokenManager.getToken();\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...extraHeaders,\n };\n\n const res = await fetch(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n if (res.ok) {\n if (res.status === 204) return undefined as T;\n return (await res.json()) as T;\n }\n\n // Handle 401 — refresh token and retry once\n if (res.status === 401 && attempt === 0) {\n this.tokenManager.invalidate();\n continue;\n }\n\n // Handle 429 — rate limit. Prefer server's Retry-After header when\n // provided, otherwise fall back to Fastlane's doubling backoff.\n if (res.status === 429) {\n if (rateLimitBackoffMs > MAX_RATE_LIMIT_BACKOFF_MS) {\n throw new RateLimitError(Math.floor(rateLimitBackoffMs / 1000));\n }\n const retryAfterHeader = res.headers.get(\"Retry-After\");\n const sleepMs = retryAfterHeader\n ? parseInt(retryAfterHeader, 10) * 1000\n : rateLimitBackoffMs;\n await sleep(sleepMs);\n rateLimitBackoffMs *= 2;\n continue;\n }\n\n // Parse error body early so we can inspect it for 403/502 special cases.\n const errorBody = await res.json().catch(() => null);\n const errors = (errorBody as JsonApiErrorResponse)?.errors || [];\n\n // Handle 403 — detect missing/expired program license agreement.\n // Mirrors api_client.rb's ProgramLicenseAgreementUpdated branch.\n if (res.status === 403) {\n const firstCode = errors[0]?.code;\n if (firstCode === \"FORBIDDEN.REQUIRED_AGREEMENTS_MISSING_OR_EXPIRED\") {\n const detail =\n errors.map((e) => e.detail || e.title).join(\"; \") ||\n \"Required agreements are missing or expired.\";\n throw new ProgramLicenseAgreementError(\n `${detail} Please manually log into your Apple Developer account to review and accept the updated agreement.`,\n []\n );\n }\n }\n\n // Handle 502 — special-case \"Could not process this request\" retry.\n // Mirrors api_client.rb's BadGatewayError branch (issue fastlane#19264).\n if (res.status === 502 && attempt < MAX_RETRIES) {\n const bodyText = errorBody ? JSON.stringify(errorBody) : \"\";\n if (bodyText.includes(\"Could not process this request\")) {\n const backoff = Math.min(\n INITIAL_BACKOFF_MS * Math.pow(2, attempt),\n MAX_BACKOFF_MS\n );\n await sleep(backoff + Math.random() * 500);\n continue;\n }\n }\n\n // Handle 5xx — retry with exponential backoff\n if (res.status >= 500 && attempt < MAX_RETRIES) {\n const backoff = Math.min(\n INITIAL_BACKOFF_MS * Math.pow(2, attempt),\n MAX_BACKOFF_MS\n );\n await sleep(backoff + Math.random() * 500);\n continue;\n }\n\n const detail = errors.map((e) => e.detail || e.title).join(\"; \") || `HTTP ${res.status}`;\n\n lastError = new AppStoreConnectError(detail, res.status, errors);\n break;\n }\n\n throw lastError || new Error(\"Request failed after retries\");\n }\n\n async get<T>(path: string, params?: Record<string, string | string[] | number | undefined>): Promise<T> {\n return this.request<T>(path, { method: \"GET\", params });\n }\n\n async post<T>(path: string, body: unknown): Promise<T> {\n return this.request<T>(path, { method: \"POST\", body });\n }\n\n async patch<T>(path: string, body: unknown): Promise<T> {\n return this.request<T>(path, { method: \"PATCH\", body });\n }\n\n async delete(path: string): Promise<void> {\n await this.request(path, { method: \"DELETE\" });\n }\n\n // Build query params from ListParams\n buildListParams(params: ListParams): Record<string, string | undefined> {\n const result: Record<string, string | undefined> = {};\n\n if (params.filter) {\n for (const [key, value] of Object.entries(params.filter)) {\n result[`filter[${key}]`] = Array.isArray(value) ? value.join(\",\") : value;\n }\n }\n\n if (params.include) {\n result[\"include\"] = params.include;\n }\n\n if (params.fields) {\n for (const [key, value] of Object.entries(params.fields)) {\n result[`fields[${key}]`] = value;\n }\n }\n\n if (params.limit !== undefined) {\n result[\"limit\"] = String(params.limit);\n }\n\n if (params.sort) {\n result[\"sort\"] = params.sort;\n }\n\n if (params.cursor) {\n // ASC uses page[after] for cursor pagination\n result[\"page[after]\"] = params.cursor;\n }\n\n return result;\n }\n\n async list<T>(path: string, params: ListParams = {}): Promise<JsonApiListResponse<T>> {\n const queryParams = this.buildListParams(params);\n return this.get<JsonApiListResponse<T>>(path, queryParams);\n }\n\n async *paginate<T>(path: string, params: ListParams = {}): AsyncGenerator<T, void, undefined> {\n let cursor: string | undefined = params.cursor;\n\n while (true) {\n const response = await this.list<T>(path, { ...params, cursor });\n\n for (const item of response.data) {\n yield item;\n }\n\n // Check for next page\n const nextLink = response.links?.next;\n if (!nextLink) break;\n\n // Extract cursor from next link\n const nextUrl = new URL(nextLink);\n cursor = nextUrl.searchParams.get(\"page[after]\") || undefined;\n if (!cursor) break;\n }\n }\n\n async all<T>(path: string, params: ListParams = {}): Promise<T[]> {\n const results: T[] = [];\n for await (const item of this.paginate<T>(path, params)) {\n results.push(item);\n }\n return results;\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { spawn } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { writeFile, unlink, mkdtemp } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport emojiRegex from \"emoji-regex\";\nimport type { AppStoreConnectClient } from \"../client/index.js\";\nimport { AppStoreConnectError } from \"../client/request.js\";\nimport type { Build } from \"../types/index.js\";\n\nexport interface PilotUploadOptions {\n ipaPath: string;\n apiKeyId: string;\n apiIssuerId: string;\n apiPrivateKey: string;\n platform?: string;\n timeout?: number;\n onOutput?: (chunk: string) => void;\n}\n\nexport interface PilotDistributeOptions {\n client: AppStoreConnectClient;\n buildId: string;\n groupNames?: string[];\n changelog?: string;\n locale?: string;\n expirePreviousBuilds?: boolean;\n submitForReview?: boolean;\n}\n\nexport interface UploadResult {\n success: boolean;\n output: string;\n}\n\n// ── Upload ─────────────────────────────────────────────────────────────────\n\nexport async function uploadToTestFlight(\n options: PilotUploadOptions\n): Promise<UploadResult> {\n if (!existsSync(options.ipaPath)) {\n throw new Error(`File not found: ${options.ipaPath}`);\n }\n\n const platform = options.platform ?? \"ios\";\n const timeout = options.timeout ?? 1_800_000; // 30 min\n\n // Write temp .p8 key file\n const keyDir = await mkdtemp(join(tmpdir(), \"fastnode-key-\"));\n const keyPath = join(keyDir, `AuthKey_${options.apiKeyId}.p8`);\n await writeFile(keyPath, options.apiPrivateKey);\n\n return new Promise<UploadResult>((resolve) => {\n const args = [\n \"altool\",\n \"--upload-app\",\n \"-f\",\n options.ipaPath,\n \"-t\",\n platform,\n \"--apiKey\",\n options.apiKeyId,\n \"--apiIssuer\",\n options.apiIssuerId,\n ];\n\n const child = spawn(\"xcrun\", args, {\n env: { ...process.env, API_PRIVATE_KEYS_DIR: keyDir },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let output = \"\";\n const timer = setTimeout(() => {\n child.kill(\"SIGTERM\");\n }, timeout);\n\n child.stdout.on(\"data\", (data: Buffer) => {\n const chunk = data.toString();\n output += chunk;\n options.onOutput?.(chunk);\n });\n\n child.stderr.on(\"data\", (data: Buffer) => {\n const chunk = data.toString();\n output += chunk;\n options.onOutput?.(chunk);\n });\n\n child.on(\"close\", async (code) => {\n clearTimeout(timer);\n await unlink(keyPath).catch(() => {});\n resolve({ success: code === 0, output });\n });\n });\n}\n\n// ── Wait for Processing ────────────────────────────────────────────────────\n\nexport async function waitForProcessing(\n client: AppStoreConnectClient,\n params: {\n bundleId: string;\n buildNumber: string;\n appVersion: string;\n timeout?: number;\n pollInterval?: number;\n }\n): Promise<Build> {\n const timeout = params.timeout ?? 1_800_000; // 30 min\n const pollInterval = params.pollInterval ?? 30_000; // 30s\n const deadline = Date.now() + timeout;\n\n // Find app\n const app = await client.apps.findByBundleId(params.bundleId);\n if (!app) throw new Error(`App not found for bundle ID: ${params.bundleId}`);\n\n while (Date.now() < deadline) {\n const builds = await client.builds.list({\n filter: {\n app: app.id,\n version: params.buildNumber,\n \"preReleaseVersion.version\": params.appVersion,\n },\n limit: 1,\n });\n\n if (builds.length > 0) {\n const build = builds[0];\n const state = build.attributes.processingState;\n\n if (state === \"VALID\") return build;\n if (state === \"FAILED\" || state === \"INVALID\") {\n throw new Error(`Build processing ${state}: ${params.buildNumber}`);\n }\n }\n\n await sleep(pollInterval);\n }\n\n throw new Error(\n `Timeout waiting for build ${params.buildNumber} after ${timeout / 1000}s`\n );\n}\n\n// ── Distribute ─────────────────────────────────────────────────────────────\n\nexport async function distributeToGroups(\n options: PilotDistributeOptions\n): Promise<string[]> {\n const groupNames = options.groupNames ?? [];\n if (groupNames.length === 0) return [];\n\n // Get the build's app\n const build = await options.client.builds.get(options.buildId, {\n include: \"app\",\n });\n const appId =\n build.relationships?.app?.data?.id;\n if (!appId) throw new Error(\"Could not determine app for build\");\n\n // Set changelog if provided\n if (options.changelog) {\n await setChangelog({\n client: options.client,\n buildId: options.buildId,\n changelog: options.changelog,\n locale: options.locale,\n });\n }\n\n // Expire previous builds if requested\n if (options.expirePreviousBuilds) {\n const allBuilds = await options.client.builds.list({\n filter: { app: appId, expired: \"false\" },\n limit: 50,\n });\n for (const b of allBuilds) {\n if (b.id !== options.buildId) {\n await options.client.builds.expireBuild(b.id).catch(() => {});\n }\n }\n }\n\n // Find and add to groups\n const allGroups = await options.client.betaGroups.listForApp(appId);\n const matched: string[] = [];\n\n for (const name of groupNames) {\n const group = allGroups.find((g) => g.attributes.name === name);\n if (!group) continue;\n\n try {\n await options.client.betaGroups.addBuilds(group.id, [options.buildId]);\n matched.push(name);\n } catch (err) {\n // 409 = already added, that's fine\n if (err instanceof AppStoreConnectError && err.status === 409) {\n matched.push(name);\n }\n }\n }\n\n // Submit for beta review if requested\n if (options.submitForReview) {\n try {\n await options.client.builds.submitForBetaReview(options.buildId);\n } catch {\n // May already be submitted\n }\n }\n\n return matched;\n}\n\n// ── Changelog ──────────────────────────────────────────────────────────────\n\nexport async function setChangelog(options: {\n client: AppStoreConnectClient;\n buildId: string;\n changelog: string;\n locale?: string;\n}): Promise<void> {\n const locale = options.locale ?? \"en-US\";\n const sanitized = sanitizeChangelog(options.changelog);\n\n const localizations = await options.client.builds.getLocalizations(\n options.buildId\n );\n const existing = localizations.find(\n (l) => l.attributes.locale === locale\n );\n\n if (existing) {\n await options.client.builds.updateLocalization(existing.id, {\n whatsNew: sanitized,\n });\n } else {\n await options.client.builds.createLocalization({\n buildId: options.buildId,\n locale,\n whatsNew: sanitized,\n });\n }\n}\n\n// ── Expire ─────────────────────────────────────────────────────────────────\n\nexport async function expireBuild(\n client: AppStoreConnectClient,\n buildId: string\n): Promise<void> {\n await client.builds.expireBuild(buildId);\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction sanitizeChangelog(text: string): string {\n // Strip RGI-standard emojis using the `emoji-regex` package — same\n // approach as Fastlane's pilot (gem `emoji_regex`), which tracks the\n // Unicode \"Recommended for General Interchange\" spec so new emoji\n // sequences are covered without hand-maintaining ranges.\n let sanitized = text.replace(emojiRegex(), \"\");\n\n // Strip < characters (not allowed by Apple)\n sanitized = sanitized.replace(/</g, \"\");\n\n // Truncate to 4000 bytes\n const encoder = new TextEncoder();\n const encoded = encoder.encode(sanitized);\n if (encoded.length > 4000) {\n const decoder = new TextDecoder();\n sanitized = decoder.decode(encoded.slice(0, 3997)) + \"...\";\n }\n\n return sanitized;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import '../index-BLubSlgK.cjs';
2
+ export { a as PilotDistributeOptions, P as PilotUploadOptions, U as UploadResult, d as distributeToGroups, e as expireBuild, s as setChangelog, u as uploadToTestFlight, w as waitForProcessing } from '../index-HRiaaX-Q.cjs';
3
+ import '../enums-B609j2rb.cjs';
@@ -0,0 +1,3 @@
1
+ import '../index-D1Izm7qt.js';
2
+ export { a as PilotDistributeOptions, P as PilotUploadOptions, U as UploadResult, d as distributeToGroups, e as expireBuild, s as setChangelog, u as uploadToTestFlight, w as waitForProcessing } from '../index-DkvinY7_.js';
3
+ import '../enums-B609j2rb.js';
@@ -0,0 +1,196 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { mkdtemp, writeFile, unlink } from 'fs/promises';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+ import emojiRegex from 'emoji-regex';
7
+ import 'crypto';
8
+ import 'readline';
9
+
10
+ // src/pilot/index.ts
11
+ BigInt(
12
+ "0xAC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73"
13
+ );
14
+
15
+ // src/client/request.ts
16
+ var AppStoreConnectError = class extends Error {
17
+ constructor(message, status, errors = []) {
18
+ super(message);
19
+ this.status = status;
20
+ this.errors = errors;
21
+ this.name = "AppStoreConnectError";
22
+ }
23
+ status;
24
+ errors;
25
+ };
26
+
27
+ // src/pilot/index.ts
28
+ async function uploadToTestFlight(options) {
29
+ if (!existsSync(options.ipaPath)) {
30
+ throw new Error(`File not found: ${options.ipaPath}`);
31
+ }
32
+ const platform = options.platform ?? "ios";
33
+ const timeout = options.timeout ?? 18e5;
34
+ const keyDir = await mkdtemp(join(tmpdir(), "fastnode-key-"));
35
+ const keyPath = join(keyDir, `AuthKey_${options.apiKeyId}.p8`);
36
+ await writeFile(keyPath, options.apiPrivateKey);
37
+ return new Promise((resolve) => {
38
+ const args = [
39
+ "altool",
40
+ "--upload-app",
41
+ "-f",
42
+ options.ipaPath,
43
+ "-t",
44
+ platform,
45
+ "--apiKey",
46
+ options.apiKeyId,
47
+ "--apiIssuer",
48
+ options.apiIssuerId
49
+ ];
50
+ const child = spawn("xcrun", args, {
51
+ env: { ...process.env, API_PRIVATE_KEYS_DIR: keyDir },
52
+ stdio: ["ignore", "pipe", "pipe"]
53
+ });
54
+ let output = "";
55
+ const timer = setTimeout(() => {
56
+ child.kill("SIGTERM");
57
+ }, timeout);
58
+ child.stdout.on("data", (data) => {
59
+ const chunk = data.toString();
60
+ output += chunk;
61
+ options.onOutput?.(chunk);
62
+ });
63
+ child.stderr.on("data", (data) => {
64
+ const chunk = data.toString();
65
+ output += chunk;
66
+ options.onOutput?.(chunk);
67
+ });
68
+ child.on("close", async (code) => {
69
+ clearTimeout(timer);
70
+ await unlink(keyPath).catch(() => {
71
+ });
72
+ resolve({ success: code === 0, output });
73
+ });
74
+ });
75
+ }
76
+ async function waitForProcessing(client, params) {
77
+ const timeout = params.timeout ?? 18e5;
78
+ const pollInterval = params.pollInterval ?? 3e4;
79
+ const deadline = Date.now() + timeout;
80
+ const app = await client.apps.findByBundleId(params.bundleId);
81
+ if (!app) throw new Error(`App not found for bundle ID: ${params.bundleId}`);
82
+ while (Date.now() < deadline) {
83
+ const builds = await client.builds.list({
84
+ filter: {
85
+ app: app.id,
86
+ version: params.buildNumber,
87
+ "preReleaseVersion.version": params.appVersion
88
+ },
89
+ limit: 1
90
+ });
91
+ if (builds.length > 0) {
92
+ const build = builds[0];
93
+ const state = build.attributes.processingState;
94
+ if (state === "VALID") return build;
95
+ if (state === "FAILED" || state === "INVALID") {
96
+ throw new Error(`Build processing ${state}: ${params.buildNumber}`);
97
+ }
98
+ }
99
+ await sleep(pollInterval);
100
+ }
101
+ throw new Error(
102
+ `Timeout waiting for build ${params.buildNumber} after ${timeout / 1e3}s`
103
+ );
104
+ }
105
+ async function distributeToGroups(options) {
106
+ const groupNames = options.groupNames ?? [];
107
+ if (groupNames.length === 0) return [];
108
+ const build = await options.client.builds.get(options.buildId, {
109
+ include: "app"
110
+ });
111
+ const appId = build.relationships?.app?.data?.id;
112
+ if (!appId) throw new Error("Could not determine app for build");
113
+ if (options.changelog) {
114
+ await setChangelog({
115
+ client: options.client,
116
+ buildId: options.buildId,
117
+ changelog: options.changelog,
118
+ locale: options.locale
119
+ });
120
+ }
121
+ if (options.expirePreviousBuilds) {
122
+ const allBuilds = await options.client.builds.list({
123
+ filter: { app: appId, expired: "false" },
124
+ limit: 50
125
+ });
126
+ for (const b of allBuilds) {
127
+ if (b.id !== options.buildId) {
128
+ await options.client.builds.expireBuild(b.id).catch(() => {
129
+ });
130
+ }
131
+ }
132
+ }
133
+ const allGroups = await options.client.betaGroups.listForApp(appId);
134
+ const matched = [];
135
+ for (const name of groupNames) {
136
+ const group = allGroups.find((g) => g.attributes.name === name);
137
+ if (!group) continue;
138
+ try {
139
+ await options.client.betaGroups.addBuilds(group.id, [options.buildId]);
140
+ matched.push(name);
141
+ } catch (err) {
142
+ if (err instanceof AppStoreConnectError && err.status === 409) {
143
+ matched.push(name);
144
+ }
145
+ }
146
+ }
147
+ if (options.submitForReview) {
148
+ try {
149
+ await options.client.builds.submitForBetaReview(options.buildId);
150
+ } catch {
151
+ }
152
+ }
153
+ return matched;
154
+ }
155
+ async function setChangelog(options) {
156
+ const locale = options.locale ?? "en-US";
157
+ const sanitized = sanitizeChangelog(options.changelog);
158
+ const localizations = await options.client.builds.getLocalizations(
159
+ options.buildId
160
+ );
161
+ const existing = localizations.find(
162
+ (l) => l.attributes.locale === locale
163
+ );
164
+ if (existing) {
165
+ await options.client.builds.updateLocalization(existing.id, {
166
+ whatsNew: sanitized
167
+ });
168
+ } else {
169
+ await options.client.builds.createLocalization({
170
+ buildId: options.buildId,
171
+ locale,
172
+ whatsNew: sanitized
173
+ });
174
+ }
175
+ }
176
+ async function expireBuild(client, buildId) {
177
+ await client.builds.expireBuild(buildId);
178
+ }
179
+ function sanitizeChangelog(text) {
180
+ let sanitized = text.replace(emojiRegex(), "");
181
+ sanitized = sanitized.replace(/</g, "");
182
+ const encoder = new TextEncoder();
183
+ const encoded = encoder.encode(sanitized);
184
+ if (encoded.length > 4e3) {
185
+ const decoder = new TextDecoder();
186
+ sanitized = decoder.decode(encoded.slice(0, 3997)) + "...";
187
+ }
188
+ return sanitized;
189
+ }
190
+ function sleep(ms) {
191
+ return new Promise((resolve) => setTimeout(resolve, ms));
192
+ }
193
+
194
+ export { distributeToGroups, expireBuild, setChangelog, uploadToTestFlight, waitForProcessing };
195
+ //# sourceMappingURL=index.mjs.map
196
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/srp.ts","../../src/client/request.ts","../../src/pilot/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * SRP (Secure Remote Password) implementation\n * 1:1 port of the fastlane-sirp Ruby gem\n * Used for Apple ID authentication\n */\n\nimport { createHash, randomBytes, pbkdf2Sync } from \"node:crypto\";\n\n// ── RFC 5054 SRP Group Parameters ──────────────────────────────────────────\n\n// Copied exactly from fastlane-sirp parameters.rb (2048-bit group)\nconst N_2048 = BigInt(\n \"0x\" +\n \"AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050\" +\n \"A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50\" +\n \"E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B8\" +\n \"55F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773B\" +\n \"CA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748\" +\n \"544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6\" +\n \"AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB\" +\n \"694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73\"\n);\nconst g_2048 = 2n;\n\n// ── Utility Functions ──────────────────────────────────────────────────────\n\nfunction numToHex(num: bigint): string {\n let hex = num.toString(16);\n if (hex.length % 2 !== 0) hex = \"0\" + hex;\n return hex.toLowerCase();\n}\n\nfunction hexToBytes(hex: string): Buffer {\n return Buffer.from(hex, \"hex\");\n}\n\nfunction shaHex(h: string, algorithm: string): string {\n return createHash(algorithm).update(hexToBytes(h)).digest(\"hex\");\n}\n\nfunction shaStr(s: string, algorithm: string): string {\n return createHash(algorithm).update(s).digest(\"hex\");\n}\n\nfunction modExp(base: bigint, exp: bigint, mod: bigint): bigint {\n if (mod === 1n) return 0n;\n let result = 1n;\n base = base % mod;\n while (exp > 0n) {\n if (exp % 2n === 1n) {\n result = (result * base) % mod;\n }\n exp = exp / 2n;\n base = (base * base) % mod;\n }\n return result;\n}\n\nfunction H(algorithm: string, n: bigint, ...args: (bigint | string | undefined)[]): bigint {\n const nHex = numToHex(n);\n // Must match Ruby: nlen = 2 * ((('%x' % [n]).length * 4 + 7) >> 3)\n // Use floor division (>>3), NOT ceiling\n const nlen = 2 * (((nHex.length * 4 + 7) >> 3));\n\n let hashin = \"\";\n for (const s of args) {\n if (s === undefined) continue;\n const shex = typeof s === \"string\" ? s : numToHex(s);\n if (shex.length > nlen) {\n throw new Error(\"Bit width does not match - client uses different prime\");\n }\n hashin += \"0\".repeat(nlen - shex.length) + shex;\n }\n\n const hashed = shaHex(hashin, algorithm);\n return BigInt(\"0x\" + hashed) % n;\n}\n\n// ── SRP Client ─────────────────────────────────────────────────────────────\n\nexport class SRPClient {\n private N: bigint;\n private g: bigint;\n private hashAlg: string;\n private k: bigint;\n private a: bigint;\n A: string;\n private S: string | null = null;\n private K: string | null = null;\n M: string | null = null;\n H_AMK: string | null = null;\n\n constructor(group: number = 2048) {\n if (group !== 2048) throw new Error(\"Only 2048-bit group is supported\");\n this.N = N_2048;\n this.g = g_2048;\n this.hashAlg = \"sha256\";\n this.k = H(this.hashAlg, this.N, this.N, this.g);\n // Generate random 'a'\n this.a = BigInt(\"0x\" + randomBytes(256).toString(\"hex\"));\n // Compute A = g^a mod N\n this.A = numToHex(modExp(this.g, this.a, this.N));\n }\n\n startAuthentication(): string {\n return this.A;\n }\n\n processChallenge(\n username: string,\n password: string, // hex-encoded encrypted password\n xsalt: string, // hex\n xbb: string, // hex\n isPasswordEncrypted: boolean = false\n ): string | false {\n const bb = BigInt(\"0x\" + xbb);\n\n // SRP-6a safety check\n if (bb % this.N === 0n) return false;\n\n let x: bigint;\n if (isPasswordEncrypted) {\n x = this.calcXHex(password, xsalt);\n } else {\n x = this.calcX(username, password, xsalt);\n }\n\n const u = H(this.hashAlg, this.N, this.A, xbb);\n if (u === 0n) return false;\n\n // S = (B - k * g^x) ^ (a + u*x) mod N\n const gx = modExp(this.g, x, this.N);\n let base = (bb - this.k * gx) % this.N;\n if (base < 0n) base += this.N;\n const exp = this.a + x * u;\n this.S = numToHex(modExp(base, exp, this.N));\n this.K = shaHex(this.S, this.hashAlg);\n\n // M = H(H(N) xor H(g), H(I), s, A, B, K)\n const hxor = H(this.hashAlg, this.N, this.N) ^ H(this.hashAlg, this.N, this.g);\n const buf =\n numToHex(hxor) +\n shaStr(username, this.hashAlg) +\n xsalt +\n this.A +\n xbb +\n this.K;\n const bytes = hexToBytes(buf);\n this.M = createHash(this.hashAlg).update(bytes).digest(\"hex\");\n\n // H_AMK = H(A, M, K)\n const amkBytes = hexToBytes(this.A + this.M + this.K);\n this.H_AMK = numToHex(\n BigInt(\"0x\" + createHash(this.hashAlg).update(amkBytes).digest(\"hex\"))\n );\n\n return this.M;\n }\n\n private calcX(username: string, password: string, xsalt: string): bigint {\n const spad = xsalt.length % 2 !== 0 ? \"0\" : \"\";\n const inner = shaStr(`${username}:${password}`, this.hashAlg);\n return BigInt(\"0x\" + shaHex(spad + xsalt + inner, this.hashAlg));\n }\n\n private calcXHex(xpassword: string, xsalt: string): bigint {\n // calc_x_hex: sha_hex(xsalt + sha_hex(\":\" hex + xpassword), hash)\n const colonHex = \":\".charCodeAt(0).toString(16);\n const inner = shaHex(colonHex + xpassword, this.hashAlg);\n return BigInt(\"0x\" + shaHex(xsalt + inner, this.hashAlg));\n }\n}\n\n// ── Apple PBKDF2 password derivation ───────────────────────────────────────\n\nexport function applePbkdf2(\n password: string,\n salt: Buffer,\n iterations: number,\n keyLength: number,\n protocol: string\n): Buffer {\n if (![\"s2k\", \"s2k_fo\"].includes(protocol)) {\n throw new Error(`Unsupported protocol '${protocol}' for pbkdf2`);\n }\n\n // First hash the password with SHA-256\n let passwordBytes: Buffer | string = createHash(\"sha256\").update(password).digest();\n\n if (protocol === \"s2k_fo\") {\n // Legacy: convert to hex string\n passwordBytes = passwordBytes.toString(\"hex\");\n }\n\n return pbkdf2Sync(passwordBytes, salt, iterations, keyLength, \"sha256\");\n}\n","import type { TokenManager } from \"./auth.js\";\nimport { ProgramLicenseAgreementError } from \"./apple-session.js\";\nimport type {\n JsonApiErrorResponse,\n JsonApiListResponse,\n JsonApiResponse,\n ListParams,\n RequestOptions,\n} from \"./types.js\";\n\nconst PRODUCTION_BASE = \"https://api.appstoreconnect.apple.com\";\nconst ENTERPRISE_BASE = \"https://api.enterprise.developer.apple.com\";\n\nconst MAX_RETRIES = 5;\nconst INITIAL_BACKOFF_MS = 1000;\nconst MAX_BACKOFF_MS = 60_000;\n/** Fastlane's TooManyRequestsError gives up after 3600s cumulative backoff. */\nconst MAX_RATE_LIMIT_BACKOFF_MS = 3_600_000;\n\nexport class AppStoreConnectError extends Error {\n constructor(\n message: string,\n public status: number,\n public errors: JsonApiErrorResponse[\"errors\"] = []\n ) {\n super(message);\n this.name = \"AppStoreConnectError\";\n }\n}\n\nexport class RateLimitError extends AppStoreConnectError {\n constructor(public retryAfter: number) {\n super(`Rate limited. Retry after ${retryAfter}s`, 429);\n this.name = \"RateLimitError\";\n }\n}\n\nexport class RequestClient {\n private baseUrl: string;\n private tokenManager: TokenManager;\n\n constructor(tokenManager: TokenManager, enterprise = false) {\n this.tokenManager = tokenManager;\n this.baseUrl = enterprise ? ENTERPRISE_BASE : PRODUCTION_BASE;\n }\n\n async request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const { method = \"GET\", body, params, headers: extraHeaders } = options;\n\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n searchParams.set(key, value.join(\",\"));\n } else {\n searchParams.set(key, String(value));\n }\n }\n const qs = searchParams.toString();\n if (qs) url += `?${qs}`;\n }\n\n let lastError: Error | null = null;\n // Soft-cumulative backoff for 429 that doubles on each hit, mirroring\n // Fastlane's Spaceship::ConnectAPI::APIClient#with_asc_retry 429 handler:\n // `Kernel.sleep(backoff); backoff *= 2`, giving up past 3600s.\n let rateLimitBackoffMs = INITIAL_BACKOFF_MS;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n const token = await this.tokenManager.getToken();\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...extraHeaders,\n };\n\n const res = await fetch(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n if (res.ok) {\n if (res.status === 204) return undefined as T;\n return (await res.json()) as T;\n }\n\n // Handle 401 — refresh token and retry once\n if (res.status === 401 && attempt === 0) {\n this.tokenManager.invalidate();\n continue;\n }\n\n // Handle 429 — rate limit. Prefer server's Retry-After header when\n // provided, otherwise fall back to Fastlane's doubling backoff.\n if (res.status === 429) {\n if (rateLimitBackoffMs > MAX_RATE_LIMIT_BACKOFF_MS) {\n throw new RateLimitError(Math.floor(rateLimitBackoffMs / 1000));\n }\n const retryAfterHeader = res.headers.get(\"Retry-After\");\n const sleepMs = retryAfterHeader\n ? parseInt(retryAfterHeader, 10) * 1000\n : rateLimitBackoffMs;\n await sleep(sleepMs);\n rateLimitBackoffMs *= 2;\n continue;\n }\n\n // Parse error body early so we can inspect it for 403/502 special cases.\n const errorBody = await res.json().catch(() => null);\n const errors = (errorBody as JsonApiErrorResponse)?.errors || [];\n\n // Handle 403 — detect missing/expired program license agreement.\n // Mirrors api_client.rb's ProgramLicenseAgreementUpdated branch.\n if (res.status === 403) {\n const firstCode = errors[0]?.code;\n if (firstCode === \"FORBIDDEN.REQUIRED_AGREEMENTS_MISSING_OR_EXPIRED\") {\n const detail =\n errors.map((e) => e.detail || e.title).join(\"; \") ||\n \"Required agreements are missing or expired.\";\n throw new ProgramLicenseAgreementError(\n `${detail} Please manually log into your Apple Developer account to review and accept the updated agreement.`,\n []\n );\n }\n }\n\n // Handle 502 — special-case \"Could not process this request\" retry.\n // Mirrors api_client.rb's BadGatewayError branch (issue fastlane#19264).\n if (res.status === 502 && attempt < MAX_RETRIES) {\n const bodyText = errorBody ? JSON.stringify(errorBody) : \"\";\n if (bodyText.includes(\"Could not process this request\")) {\n const backoff = Math.min(\n INITIAL_BACKOFF_MS * Math.pow(2, attempt),\n MAX_BACKOFF_MS\n );\n await sleep(backoff + Math.random() * 500);\n continue;\n }\n }\n\n // Handle 5xx — retry with exponential backoff\n if (res.status >= 500 && attempt < MAX_RETRIES) {\n const backoff = Math.min(\n INITIAL_BACKOFF_MS * Math.pow(2, attempt),\n MAX_BACKOFF_MS\n );\n await sleep(backoff + Math.random() * 500);\n continue;\n }\n\n const detail = errors.map((e) => e.detail || e.title).join(\"; \") || `HTTP ${res.status}`;\n\n lastError = new AppStoreConnectError(detail, res.status, errors);\n break;\n }\n\n throw lastError || new Error(\"Request failed after retries\");\n }\n\n async get<T>(path: string, params?: Record<string, string | string[] | number | undefined>): Promise<T> {\n return this.request<T>(path, { method: \"GET\", params });\n }\n\n async post<T>(path: string, body: unknown): Promise<T> {\n return this.request<T>(path, { method: \"POST\", body });\n }\n\n async patch<T>(path: string, body: unknown): Promise<T> {\n return this.request<T>(path, { method: \"PATCH\", body });\n }\n\n async delete(path: string): Promise<void> {\n await this.request(path, { method: \"DELETE\" });\n }\n\n // Build query params from ListParams\n buildListParams(params: ListParams): Record<string, string | undefined> {\n const result: Record<string, string | undefined> = {};\n\n if (params.filter) {\n for (const [key, value] of Object.entries(params.filter)) {\n result[`filter[${key}]`] = Array.isArray(value) ? value.join(\",\") : value;\n }\n }\n\n if (params.include) {\n result[\"include\"] = params.include;\n }\n\n if (params.fields) {\n for (const [key, value] of Object.entries(params.fields)) {\n result[`fields[${key}]`] = value;\n }\n }\n\n if (params.limit !== undefined) {\n result[\"limit\"] = String(params.limit);\n }\n\n if (params.sort) {\n result[\"sort\"] = params.sort;\n }\n\n if (params.cursor) {\n // ASC uses page[after] for cursor pagination\n result[\"page[after]\"] = params.cursor;\n }\n\n return result;\n }\n\n async list<T>(path: string, params: ListParams = {}): Promise<JsonApiListResponse<T>> {\n const queryParams = this.buildListParams(params);\n return this.get<JsonApiListResponse<T>>(path, queryParams);\n }\n\n async *paginate<T>(path: string, params: ListParams = {}): AsyncGenerator<T, void, undefined> {\n let cursor: string | undefined = params.cursor;\n\n while (true) {\n const response = await this.list<T>(path, { ...params, cursor });\n\n for (const item of response.data) {\n yield item;\n }\n\n // Check for next page\n const nextLink = response.links?.next;\n if (!nextLink) break;\n\n // Extract cursor from next link\n const nextUrl = new URL(nextLink);\n cursor = nextUrl.searchParams.get(\"page[after]\") || undefined;\n if (!cursor) break;\n }\n }\n\n async all<T>(path: string, params: ListParams = {}): Promise<T[]> {\n const results: T[] = [];\n for await (const item of this.paginate<T>(path, params)) {\n results.push(item);\n }\n return results;\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { spawn } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { writeFile, unlink, mkdtemp } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport emojiRegex from \"emoji-regex\";\nimport type { AppStoreConnectClient } from \"../client/index.js\";\nimport { AppStoreConnectError } from \"../client/request.js\";\nimport type { Build } from \"../types/index.js\";\n\nexport interface PilotUploadOptions {\n ipaPath: string;\n apiKeyId: string;\n apiIssuerId: string;\n apiPrivateKey: string;\n platform?: string;\n timeout?: number;\n onOutput?: (chunk: string) => void;\n}\n\nexport interface PilotDistributeOptions {\n client: AppStoreConnectClient;\n buildId: string;\n groupNames?: string[];\n changelog?: string;\n locale?: string;\n expirePreviousBuilds?: boolean;\n submitForReview?: boolean;\n}\n\nexport interface UploadResult {\n success: boolean;\n output: string;\n}\n\n// ── Upload ─────────────────────────────────────────────────────────────────\n\nexport async function uploadToTestFlight(\n options: PilotUploadOptions\n): Promise<UploadResult> {\n if (!existsSync(options.ipaPath)) {\n throw new Error(`File not found: ${options.ipaPath}`);\n }\n\n const platform = options.platform ?? \"ios\";\n const timeout = options.timeout ?? 1_800_000; // 30 min\n\n // Write temp .p8 key file\n const keyDir = await mkdtemp(join(tmpdir(), \"fastnode-key-\"));\n const keyPath = join(keyDir, `AuthKey_${options.apiKeyId}.p8`);\n await writeFile(keyPath, options.apiPrivateKey);\n\n return new Promise<UploadResult>((resolve) => {\n const args = [\n \"altool\",\n \"--upload-app\",\n \"-f\",\n options.ipaPath,\n \"-t\",\n platform,\n \"--apiKey\",\n options.apiKeyId,\n \"--apiIssuer\",\n options.apiIssuerId,\n ];\n\n const child = spawn(\"xcrun\", args, {\n env: { ...process.env, API_PRIVATE_KEYS_DIR: keyDir },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let output = \"\";\n const timer = setTimeout(() => {\n child.kill(\"SIGTERM\");\n }, timeout);\n\n child.stdout.on(\"data\", (data: Buffer) => {\n const chunk = data.toString();\n output += chunk;\n options.onOutput?.(chunk);\n });\n\n child.stderr.on(\"data\", (data: Buffer) => {\n const chunk = data.toString();\n output += chunk;\n options.onOutput?.(chunk);\n });\n\n child.on(\"close\", async (code) => {\n clearTimeout(timer);\n await unlink(keyPath).catch(() => {});\n resolve({ success: code === 0, output });\n });\n });\n}\n\n// ── Wait for Processing ────────────────────────────────────────────────────\n\nexport async function waitForProcessing(\n client: AppStoreConnectClient,\n params: {\n bundleId: string;\n buildNumber: string;\n appVersion: string;\n timeout?: number;\n pollInterval?: number;\n }\n): Promise<Build> {\n const timeout = params.timeout ?? 1_800_000; // 30 min\n const pollInterval = params.pollInterval ?? 30_000; // 30s\n const deadline = Date.now() + timeout;\n\n // Find app\n const app = await client.apps.findByBundleId(params.bundleId);\n if (!app) throw new Error(`App not found for bundle ID: ${params.bundleId}`);\n\n while (Date.now() < deadline) {\n const builds = await client.builds.list({\n filter: {\n app: app.id,\n version: params.buildNumber,\n \"preReleaseVersion.version\": params.appVersion,\n },\n limit: 1,\n });\n\n if (builds.length > 0) {\n const build = builds[0];\n const state = build.attributes.processingState;\n\n if (state === \"VALID\") return build;\n if (state === \"FAILED\" || state === \"INVALID\") {\n throw new Error(`Build processing ${state}: ${params.buildNumber}`);\n }\n }\n\n await sleep(pollInterval);\n }\n\n throw new Error(\n `Timeout waiting for build ${params.buildNumber} after ${timeout / 1000}s`\n );\n}\n\n// ── Distribute ─────────────────────────────────────────────────────────────\n\nexport async function distributeToGroups(\n options: PilotDistributeOptions\n): Promise<string[]> {\n const groupNames = options.groupNames ?? [];\n if (groupNames.length === 0) return [];\n\n // Get the build's app\n const build = await options.client.builds.get(options.buildId, {\n include: \"app\",\n });\n const appId =\n build.relationships?.app?.data?.id;\n if (!appId) throw new Error(\"Could not determine app for build\");\n\n // Set changelog if provided\n if (options.changelog) {\n await setChangelog({\n client: options.client,\n buildId: options.buildId,\n changelog: options.changelog,\n locale: options.locale,\n });\n }\n\n // Expire previous builds if requested\n if (options.expirePreviousBuilds) {\n const allBuilds = await options.client.builds.list({\n filter: { app: appId, expired: \"false\" },\n limit: 50,\n });\n for (const b of allBuilds) {\n if (b.id !== options.buildId) {\n await options.client.builds.expireBuild(b.id).catch(() => {});\n }\n }\n }\n\n // Find and add to groups\n const allGroups = await options.client.betaGroups.listForApp(appId);\n const matched: string[] = [];\n\n for (const name of groupNames) {\n const group = allGroups.find((g) => g.attributes.name === name);\n if (!group) continue;\n\n try {\n await options.client.betaGroups.addBuilds(group.id, [options.buildId]);\n matched.push(name);\n } catch (err) {\n // 409 = already added, that's fine\n if (err instanceof AppStoreConnectError && err.status === 409) {\n matched.push(name);\n }\n }\n }\n\n // Submit for beta review if requested\n if (options.submitForReview) {\n try {\n await options.client.builds.submitForBetaReview(options.buildId);\n } catch {\n // May already be submitted\n }\n }\n\n return matched;\n}\n\n// ── Changelog ──────────────────────────────────────────────────────────────\n\nexport async function setChangelog(options: {\n client: AppStoreConnectClient;\n buildId: string;\n changelog: string;\n locale?: string;\n}): Promise<void> {\n const locale = options.locale ?? \"en-US\";\n const sanitized = sanitizeChangelog(options.changelog);\n\n const localizations = await options.client.builds.getLocalizations(\n options.buildId\n );\n const existing = localizations.find(\n (l) => l.attributes.locale === locale\n );\n\n if (existing) {\n await options.client.builds.updateLocalization(existing.id, {\n whatsNew: sanitized,\n });\n } else {\n await options.client.builds.createLocalization({\n buildId: options.buildId,\n locale,\n whatsNew: sanitized,\n });\n }\n}\n\n// ── Expire ─────────────────────────────────────────────────────────────────\n\nexport async function expireBuild(\n client: AppStoreConnectClient,\n buildId: string\n): Promise<void> {\n await client.builds.expireBuild(buildId);\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction sanitizeChangelog(text: string): string {\n // Strip RGI-standard emojis using the `emoji-regex` package — same\n // approach as Fastlane's pilot (gem `emoji_regex`), which tracks the\n // Unicode \"Recommended for General Interchange\" spec so new emoji\n // sequences are covered without hand-maintaining ranges.\n let sanitized = text.replace(emojiRegex(), \"\");\n\n // Strip < characters (not allowed by Apple)\n sanitized = sanitized.replace(/</g, \"\");\n\n // Truncate to 4000 bytes\n const encoder = new TextEncoder();\n const encoded = encoder.encode(sanitized);\n if (encoded.length > 4000) {\n const decoder = new TextDecoder();\n sanitized = decoder.decode(encoded.slice(0, 3997)) + \"...\";\n }\n\n return sanitized;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"]}