catalyst-core 1.1.0 → 1.2.0-canary.2

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 (69) hide show
  1. package/bin/catalyst.js +54 -6
  2. package/changelog.md +27 -1
  3. package/dist/errors/registry.js +306 -18
  4. package/dist/errors-index.json +273 -70
  5. package/dist/native/androidProject/app/build.gradle.kts +53 -79
  6. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
  10. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
  11. package/dist/native/androidSetup.js +76 -170
  12. package/dist/native/androidToolchain.js +642 -0
  13. package/dist/native/appIcon.js +100 -0
  14. package/dist/native/buildAndroid/assets.js +29 -5
  15. package/dist/native/buildAndroid/build.js +110 -223
  16. package/dist/native/buildAndroid/config.js +57 -65
  17. package/dist/native/buildAndroid/devices.js +176 -0
  18. package/dist/native/buildAndroid/index.js +271 -46
  19. package/dist/native/buildAndroid/output.js +43 -0
  20. package/dist/native/buildAndroid/signing.js +456 -0
  21. package/dist/native/buildAppAndroid.js +27 -7
  22. package/dist/native/buildAppIos.js +139 -20
  23. package/dist/native/buildIos/assets.js +76 -4
  24. package/dist/native/buildIos/build.js +31 -181
  25. package/dist/native/buildIos/config.js +126 -11
  26. package/dist/native/buildIos/index.js +354 -34
  27. package/dist/native/buildIos/localhostCert.js +87 -0
  28. package/dist/native/buildIos/output.js +28 -0
  29. package/dist/native/buildIos/signing.js +596 -0
  30. package/dist/native/credentials.js +309 -0
  31. package/dist/native/exportCredentials.js +51 -0
  32. package/dist/native/iosToolchain.js +583 -0
  33. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
  34. package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
  35. package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
  36. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
  37. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
  38. package/dist/native/localState.js +148 -0
  39. package/dist/native/nativeConfig.js +621 -0
  40. package/dist/native/releaseOutput.js +53 -0
  41. package/dist/native/renameAndroidProject.js +154 -369
  42. package/dist/native/setupEmulatorIos.js +58 -275
  43. package/dist/native/setupServer.js +520 -124
  44. package/dist/native/utils.js +418 -115
  45. package/dist/vite/resolveDevServerConfig.js +21 -0
  46. package/mcp_v2/knowledge-base.json +13 -11
  47. package/mcp_v2/test/config.test.ts +40 -0
  48. package/mcp_v2/tools/build.js +80 -104
  49. package/mcp_v2/tools/config.js +181 -174
  50. package/package.json +1 -1
  51. package/test/cli/nativeFlags.test.ts +97 -0
  52. package/test/native/androidDevices.test.ts +230 -0
  53. package/test/native/androidElfAlignment.test.ts +148 -0
  54. package/test/native/androidSigning.test.ts +646 -0
  55. package/test/native/androidToolchain.test.ts +766 -0
  56. package/test/native/appIcon.test.ts +124 -0
  57. package/test/native/credentials.test.ts +241 -0
  58. package/test/native/interaction.test.ts +179 -0
  59. package/test/native/iosConfigPhase.test.ts +611 -0
  60. package/test/native/iosSigning.test.ts +1060 -0
  61. package/test/native/iosToolchain.test.ts +821 -0
  62. package/test/native/localState.test.ts +80 -0
  63. package/test/native/nativeConfig.test.ts +602 -0
  64. package/test/native/setupServer.test.ts +529 -0
  65. package/test/native/utils.test.ts +102 -127
  66. package/test/vite/resolveDevServerConfig.test.js +25 -0
  67. package/dist/native/androidProject/keystore/Untitled +0 -0
  68. package/dist/native/androidProject/keystore/release-key.jks +0 -0
  69. package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
@@ -0,0 +1,596 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.SIGNING_PROPERTIES = exports.EXPORT_METHODS = void 0;
7
+ exports.buildExportOptions = buildExportOptions;
8
+ exports.isReleaseBuildType = isReleaseBuildType;
9
+ exports.isSigningFailure = isSigningFailure;
10
+ exports.parseIdentity = parseIdentity;
11
+ exports.parseKeychainList = parseKeychainList;
12
+ exports.parseProperties = parseProperties;
13
+ exports.planIosSigning = planIosSigning;
14
+ exports.profileDirectories = profileDirectories;
15
+ exports.profileMatchesBundleId = profileMatchesBundleId;
16
+ exports.readIosCredentials = readIosCredentials;
17
+ exports.readProvisioningProfile = readProvisioningProfile;
18
+ exports.resolveExportMethod = resolveExportMethod;
19
+ exports.setupP12Signing = setupP12Signing;
20
+ exports.signingSetupError = signingSetupError;
21
+ exports.writeExportOptionsPlist = writeExportOptionsPlist;
22
+ exports.xcodeAuthArgs = xcodeAuthArgs;
23
+ var _child_process = require("child_process");
24
+ var _crypto = _interopRequireDefault(require("crypto"));
25
+ var _fs = _interopRequireDefault(require("fs"));
26
+ var _os = _interopRequireDefault(require("os"));
27
+ var _path = _interopRequireDefault(require("path"));
28
+ var _utils = require("../utils.js");
29
+ var _credentials = require("../credentials.js");
30
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
31
+ // iOS signing without opening Xcode. Signing files live in the app's
32
+ // git-ignored credentials/ios/ folder, or in CI in the one
33
+ // CATALYST_IOS_CREDENTIALS secret (`catalyst credentials:export ios`), which
34
+ // resolveCredentials("ios") unpacks to a private temp folder:
35
+ //
36
+ // credentials/ios/
37
+ // AuthKey_<KEYID>.p8 App Store Connect API key: cloud signing
38
+ // <name>.p12 distribution certificate and its private key: local signing
39
+ // <name>.mobileprovision the provisioning profile for that certificate
40
+ // signing.properties ascIssuerId=<uuid>, ascKeyId=<id> (optional, else from
41
+ // the .p8 file name), p12Password=<password of the .p12>
42
+ //
43
+ // A release signs, in this order:
44
+ //
45
+ // 1. Locally with the .p12 and profile: the p12 goes into a temporary
46
+ // keychain and the profile into Xcode's profile folders, and both are
47
+ // removed again when the build ends, also on Ctrl+C.
48
+ // 2. Manually with an installed profile: ios.provisioningProfile.
49
+ // 3. Automatically for ios.developmentTeam: cloud signing with the API key,
50
+ // or whatever Apple account is signed in to Xcode.
51
+ //
52
+ // `security` takes the p12 password (`import -P`) and the throwaway keychain
53
+ // password (`create-keychain -p`, `unlock-keychain -p`, `set-key-partition-list
54
+ // -k`) only as arguments: it has no environment or stdin form for them, and
55
+ // leaving them out makes it ask through a GUI prompt. They are never logged,
56
+ // they are masked under GitHub Actions (maskSecret), and every error from
57
+ // these commands is scrubbed of both before it is shown.
58
+
59
+ // Xcode 26's names. The pre-Xcode 15.3 names still work there, with a warning.
60
+ const EXPORT_METHODS = exports.EXPORT_METHODS = ["app-store-connect", "release-testing", "enterprise", "debugging"];
61
+ const LEGACY_EXPORT_METHODS = {
62
+ "app-store": "app-store-connect",
63
+ "ad-hoc": "release-testing",
64
+ development: "debugging"
65
+ };
66
+ const KEYCHAIN_LOCK_TIMEOUT_SECONDS = 6 * 60 * 60;
67
+ const SIGNING_PROPERTIES = exports.SIGNING_PROPERTIES = "signing.properties";
68
+
69
+ /** `ios.buildType: "release"`, in any case, asks for a release build like `--release`. */
70
+ function isReleaseBuildType(buildType) {
71
+ return String(buildType || "").trim().toLowerCase() === "release";
72
+ }
73
+ function defaultRun(command, args, {
74
+ input,
75
+ env
76
+ } = {}) {
77
+ return (0, _child_process.execFileSync)(command, args, {
78
+ encoding: "utf8",
79
+ stdio: ["pipe", "pipe", "pipe"],
80
+ input,
81
+ env: env || process.env,
82
+ maxBuffer: 64 * 1024 * 1024
83
+ });
84
+ }
85
+
86
+ /**
87
+ * The export method for `-exportArchive`: ios.exportMethod, default
88
+ * app-store-connect. Legacy names map to their Xcode 26 equivalent.
89
+ *
90
+ * @param {Record<string, any>} [iosConfig]
91
+ * @param {{ log?: (message: string, type?: string) => void }} [options]
92
+ */
93
+ function resolveExportMethod(iosConfig = {}, {
94
+ log = () => {}
95
+ } = {}) {
96
+ const raw = iosConfig.exportMethod;
97
+ if (raw === undefined || raw === null || String(raw).trim() === "") return "app-store-connect";
98
+ const value = String(raw).trim().toLowerCase();
99
+ if (EXPORT_METHODS.includes(value)) return value;
100
+ if (LEGACY_EXPORT_METHODS[value]) {
101
+ log(`ios.exportMethod "${raw}" is the old name; use "${LEGACY_EXPORT_METHODS[value]}" instead.`, "warning");
102
+ return LEGACY_EXPORT_METHODS[value];
103
+ }
104
+ throw (0, _utils.nativeError)("IOS-013", `"${raw}"`);
105
+ }
106
+
107
+ /** Java-style .properties, the subset people write by hand: key=value (or key: value) lines, # and ! comments. */
108
+ function parseProperties(text) {
109
+ const values = {};
110
+ for (const raw of String(text || "").split(/\r?\n/)) {
111
+ const line = raw.trim();
112
+ if (!line || line.startsWith("#") || line.startsWith("!")) continue;
113
+ const match = /^([^=:\s]+)\s*[=:]\s*(.*)$/.exec(line);
114
+ if (match) values[match[1]] = match[2].trim();
115
+ }
116
+ return values;
117
+ }
118
+
119
+ /**
120
+ * What credentials/ios/ (or the unpacked CATALYST_IOS_CREDENTIALS) holds.
121
+ * Each password read is masked under GitHub Actions before anything else can
122
+ * print it.
123
+ *
124
+ * @param {string} dir - from resolveCredentials("ios")
125
+ * @param {object} [options]
126
+ * @param {Record<string, string | undefined>} [options.env] - for maskSecret
127
+ * @param {(value: string, options?: object) => boolean} [options.mask]
128
+ * @returns {{
129
+ * dir: string,
130
+ * apiKey: { keyPath: string, keyId: string, issuerId: string } | null,
131
+ * p12: { file: string, password: string, profileFile: string } | null,
132
+ * }}
133
+ */
134
+ function readIosCredentials(dir, {
135
+ env = process.env,
136
+ mask = _credentials.maskSecret
137
+ } = {}) {
138
+ let names = [];
139
+ try {
140
+ names = _fs.default.readdirSync(dir).filter(name => _fs.default.statSync(_path.default.join(dir, name)).isFile());
141
+ } catch (error) {
142
+ return {
143
+ dir,
144
+ apiKey: null,
145
+ p12: null
146
+ };
147
+ }
148
+ const withExt = ext => names.filter(name => name.toLowerCase().endsWith(ext));
149
+ const one = (ext, code) => {
150
+ const found = withExt(ext);
151
+ if (found.length > 1) {
152
+ throw (0, _utils.nativeError)(code, `credentials/ios/ holds ${found.length} ${ext} files (${found.join(", ")}); keep one`);
153
+ }
154
+ return found[0] ? _path.default.join(dir, found[0]) : null;
155
+ };
156
+ const props = names.includes(SIGNING_PROPERTIES) ? parseProperties(_fs.default.readFileSync(_path.default.join(dir, SIGNING_PROPERTIES), "utf8")) : {};
157
+ if (props.p12Password) mask(props.p12Password, {
158
+ env
159
+ });
160
+
161
+ // App Store Connect API key.
162
+ let apiKey = null;
163
+ const keyFile = one(".p8", "IOS-011");
164
+ if (keyFile || props.ascKeyId || props.ascIssuerId) {
165
+ const fromName = keyFile ? /^AuthKey_([A-Za-z0-9]+)\.p8$/i.exec(_path.default.basename(keyFile)) : null;
166
+ const keyId = String(props.ascKeyId || fromName?.[1] || "").trim();
167
+ const issuerId = String(props.ascIssuerId || "").trim();
168
+ const missing = [];
169
+ if (!keyFile) missing.push("the key file (AuthKey_<KEYID>.p8)");
170
+ if (!keyId) missing.push(`the key ID (ascKeyId in ${SIGNING_PROPERTIES}, or the AuthKey_<KEYID>.p8 file name)`);
171
+ if (!issuerId) missing.push(`the issuer ID (ascIssuerId in ${SIGNING_PROPERTIES})`);
172
+ if (missing.length) throw (0, _utils.nativeError)("IOS-011", `credentials/ios/ is missing ${missing.join(", ")}`);
173
+ if (!/^[A-Za-z0-9]+$/.test(keyId)) {
174
+ throw (0, _utils.nativeError)("IOS-011", `the key ID "${keyId}" should be letters and digits only`);
175
+ }
176
+ apiKey = {
177
+ keyPath: keyFile,
178
+ keyId,
179
+ issuerId
180
+ };
181
+ }
182
+
183
+ // Distribution certificate and profile.
184
+ let p12 = null;
185
+ const p12File = one(".p12", "IOS-012");
186
+ const profileFile = one(".mobileprovision", "IOS-012");
187
+ if (p12File && !profileFile) {
188
+ throw (0, _utils.nativeError)("IOS-012", "credentials/ios/ has a .p12 but no .mobileprovision for it");
189
+ }
190
+ if (profileFile && !p12File) {
191
+ throw (0, _utils.nativeError)("IOS-012", "credentials/ios/ has a .mobileprovision but no .p12 to sign with");
192
+ }
193
+ if (p12File) p12 = {
194
+ file: p12File,
195
+ password: props.p12Password ?? "",
196
+ profileFile
197
+ };
198
+ return {
199
+ dir,
200
+ apiKey,
201
+ p12
202
+ };
203
+ }
204
+
205
+ /**
206
+ * xcodebuild flags that let automatic signing create and fetch certificates
207
+ * and profiles: with the API key when there is one (cloud signing),
208
+ * otherwise with the Apple account signed in to Xcode.
209
+ */
210
+ function xcodeAuthArgs(apiKey) {
211
+ const args = ["-allowProvisioningUpdates", "-allowProvisioningDeviceRegistration"];
212
+ if (apiKey) {
213
+ args.push("-authenticationKeyPath", apiKey.keyPath, "-authenticationKeyID", apiKey.keyId, "-authenticationKeyIssuerID", apiKey.issuerId);
214
+ }
215
+ return args;
216
+ }
217
+
218
+ /**
219
+ * How this build signs, decided from the config and the credentials alone
220
+ * (no side effects), so the entitlements can be written before Xcode runs.
221
+ *
222
+ * aps-environment: Xcode's entitlements merger takes aps-environment from the
223
+ * provisioning profile it signs with. An automatic-signing archive is signed
224
+ * for development and export re-signs it with the distribution profile, so
225
+ * the archive keeps "development". A manual release archive is signed with
226
+ * the distribution profile directly, so it must already say "production".
227
+ *
228
+ * @param {object} options
229
+ * @param {Record<string, any>} [options.iosConfig]
230
+ * @param {ReturnType<typeof readIosCredentials> | null} [options.credentials]
231
+ * @param {boolean} [options.release]
232
+ * @param {string} [options.exportMethod]
233
+ */
234
+ function planIosSigning({
235
+ iosConfig = {},
236
+ credentials = null,
237
+ release = false,
238
+ exportMethod = "app-store-connect"
239
+ } = {}) {
240
+ const teamId = String(iosConfig.developmentTeam || "").trim();
241
+ const distribution = release && exportMethod !== "debugging";
242
+ if (release && credentials?.p12) {
243
+ return {
244
+ style: "manual",
245
+ source: "credentials/ios .p12",
246
+ p12: true,
247
+ teamId,
248
+ apsEnvironment: distribution ? "production" : "development"
249
+ };
250
+ }
251
+ if (iosConfig.provisioningProfile) {
252
+ return {
253
+ style: "manual",
254
+ source: "ios.provisioningProfile",
255
+ p12: false,
256
+ teamId,
257
+ provisioningProfile: String(iosConfig.provisioningProfile),
258
+ apsEnvironment: distribution ? "production" : "development"
259
+ };
260
+ }
261
+ return {
262
+ style: "automatic",
263
+ source: credentials?.apiKey ? "App Store Connect API key" : "Xcode account",
264
+ p12: false,
265
+ teamId,
266
+ apsEnvironment: "development"
267
+ };
268
+ }
269
+
270
+ /** The quoted paths `security list-keychains` prints, one per line. */
271
+ function parseKeychainList(output) {
272
+ return [...String(output || "").matchAll(/"([^"]+)"/g)].map(match => match[1]);
273
+ }
274
+
275
+ /** The first valid identity `security find-identity -v -p codesigning` lists. */
276
+ function parseIdentity(output) {
277
+ const match = /^\s*\d+\)\s+([0-9A-Fa-f]{40})\s+"([^"]+)"/m.exec(String(output || ""));
278
+ return match ? {
279
+ sha1: match[1].toUpperCase(),
280
+ name: match[2]
281
+ } : null;
282
+ }
283
+
284
+ /** Whether a profile's application-identifier (TEAM.bundle, maybe ending in *) covers the bundle id. */
285
+ function profileMatchesBundleId(applicationIdentifier, bundleId) {
286
+ if (!applicationIdentifier || !bundleId) return true;
287
+ const appId = applicationIdentifier.slice(applicationIdentifier.indexOf(".") + 1);
288
+ if (appId === "*" || appId === bundleId) return true;
289
+ return appId.endsWith("*") && bundleId.startsWith(appId.slice(0, -1));
290
+ }
291
+
292
+ /** Xcode 16+ keeps profiles under UserData; older Xcode (and Xcode 26 still) also reads MobileDevice. */
293
+ function profileDirectories(homeDir = _os.default.homedir()) {
294
+ return [_path.default.join(homeDir, "Library", "Developer", "Xcode", "UserData", "Provisioning Profiles"), _path.default.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles")];
295
+ }
296
+
297
+ /**
298
+ * Turn a failed command into an error that carries neither secret, wherever
299
+ * the tool or Node put it (execFileSync's message holds the whole argv).
300
+ */
301
+ function scrubbedFailure(error, secrets, prefix) {
302
+ const scrub = text => secrets.reduce((acc, secret) => secret ? acc.split(secret).join("***") : acc, String(text || ""));
303
+ const said = String(error?.stderr || error?.stdout || "").trim().split("\n").filter(Boolean).pop();
304
+ const failure = new Error(scrub(`${prefix}${said ? `: ${said}` : ""}`));
305
+ if (error?.code === "ENOENT") failure.code = "ENOENT";
306
+ return failure;
307
+ }
308
+
309
+ /**
310
+ * Read the fields a build needs from a .mobileprovision (a CMS-signed plist).
311
+ *
312
+ * @param {string} file
313
+ * @param {object} options
314
+ * @param {string} options.workDir - a private folder for the decoded plist
315
+ * @param {Function} [options.run]
316
+ */
317
+ function readProvisioningProfile(file, {
318
+ workDir,
319
+ run = defaultRun
320
+ }) {
321
+ const decoded = _path.default.join(workDir, "profile.plist");
322
+ try {
323
+ run("security", ["cms", "-D", "-i", file, "-o", decoded]);
324
+ } catch (error) {
325
+ throw (0, _utils.nativeError)("IOS-012", `${_path.default.basename(file)} is not a provisioning profile`);
326
+ }
327
+ const extract = keyPath => {
328
+ try {
329
+ return String(run("plutil", ["-extract", keyPath, "raw", "-o", "-", decoded])).trim() || undefined;
330
+ } catch (error) {
331
+ return undefined;
332
+ }
333
+ };
334
+ const profile = {
335
+ uuid: extract("UUID"),
336
+ name: extract("Name"),
337
+ teamId: extract("TeamIdentifier.0"),
338
+ applicationIdentifier: extract("Entitlements.application-identifier"),
339
+ apsEnvironment: extract("Entitlements.aps-environment"),
340
+ expirationDate: extract("ExpirationDate")
341
+ };
342
+ if (!profile.uuid || !/^[0-9A-Fa-f-]+$/.test(profile.uuid)) {
343
+ throw (0, _utils.nativeError)("IOS-012", "the provisioning profile has no UUID");
344
+ }
345
+ return profile;
346
+ }
347
+
348
+ /**
349
+ * Local signing with credentials/ios/: import the .p12 into a temporary
350
+ * keychain, put that keychain on the user search list, and install the
351
+ * .mobileprovision where Xcode looks for profiles.
352
+ *
353
+ * `cleanup()` restores the exact search list read before anything changed,
354
+ * deletes the keychain (and the decoded profile beside it), and removes the
355
+ * profile files this call created. It never touches credentials/ios/. It runs on failure here, and on SIGINT/SIGTERM until the
356
+ * caller has run it. Call it in a `finally`.
357
+ *
358
+ * @param {object} options
359
+ * @param {{ file: string, password?: string, profileFile: string }} options.p12 - from readIosCredentials
360
+ * @param {string} [options.bundleId] - checked against the profile
361
+ * @param {Record<string, string | undefined>} [options.env] - for maskSecret
362
+ * @param {(value: string, options?: object) => boolean} [options.mask]
363
+ * @param {(command: string, args: string[], options?: object) => string} [options.run]
364
+ * @param {string} [options.homeDir]
365
+ * @param {string} [options.tmpRoot]
366
+ * @param {(message: string, type?: string) => void} [options.log]
367
+ * @param {{ once: Function, removeListener: Function }} [options.signals]
368
+ * @param {(code: number) => void} [options.exit]
369
+ * @param {() => Date} [options.now]
370
+ */
371
+ function setupP12Signing({
372
+ p12,
373
+ bundleId,
374
+ env = process.env,
375
+ mask = _credentials.maskSecret,
376
+ run = defaultRun,
377
+ homeDir = _os.default.homedir(),
378
+ tmpRoot = _os.default.tmpdir(),
379
+ log = () => {},
380
+ signals = process,
381
+ exit = code => process.exit(code),
382
+ now = () => new Date()
383
+ } = {}) {
384
+ const p12Password = p12?.password ?? "";
385
+ const keychainPassword = _crypto.default.randomBytes(24).toString("base64url");
386
+ if (p12Password) mask(p12Password, {
387
+ env
388
+ });
389
+ mask(keychainPassword, {
390
+ env
391
+ });
392
+ const p12Name = _path.default.basename(p12?.file || "the .p12");
393
+ const secrets = [p12Password, keychainPassword].filter(Boolean);
394
+ const exec = (command, args, prefix) => {
395
+ try {
396
+ return run(command, args);
397
+ } catch (error) {
398
+ throw scrubbedFailure(error, secrets, prefix);
399
+ }
400
+ };
401
+ const workDir = _fs.default.mkdtempSync(_path.default.join(tmpRoot, "catalyst-ios-signing-"));
402
+ _fs.default.chmodSync(workDir, 0o700);
403
+ const keychainPath = _path.default.join(workDir, "catalyst-signing.keychain-db");
404
+ const state = {
405
+ originalList: null,
406
+ keychainCreated: false,
407
+ installed: [],
408
+ createdDirs: []
409
+ };
410
+ let cleaned = false;
411
+ let releaseSignals = () => {};
412
+ function cleanup() {
413
+ if (cleaned) return;
414
+ cleaned = true;
415
+ releaseSignals();
416
+ const step = (what, fn) => {
417
+ try {
418
+ fn();
419
+ } catch (error) {
420
+ log(`Signing cleanup could not ${what}: ${scrubbedFailure(error, secrets, "").message}`, "warning");
421
+ }
422
+ };
423
+ if (state.originalList) {
424
+ step("restore the keychain search list", () => run("security", ["list-keychains", "-d", "user", "-s", ...state.originalList]));
425
+ }
426
+ if (state.keychainCreated) {
427
+ step("delete the temporary keychain", () => run("security", ["delete-keychain", keychainPath]));
428
+ }
429
+ for (const file of state.installed) step(`remove ${file}`, () => _fs.default.rmSync(file, {
430
+ force: true
431
+ }));
432
+ for (const dir of state.createdDirs.reverse()) {
433
+ step(`remove ${dir}`, () => {
434
+ if (_fs.default.existsSync(dir) && _fs.default.readdirSync(dir).length === 0) _fs.default.rmdirSync(dir);
435
+ });
436
+ }
437
+ step("remove the temporary signing folder", () => _fs.default.rmSync(workDir, {
438
+ recursive: true,
439
+ force: true
440
+ }));
441
+ }
442
+ releaseSignals = (0, _credentials.cleanupOnSignal)(cleanup, {
443
+ signals,
444
+ exit
445
+ });
446
+ try {
447
+ const p12File = p12.file;
448
+ const profileFile = p12.profileFile;
449
+
450
+ // Read first: whatever happens next, cleanup puts exactly this back.
451
+ state.originalList = parseKeychainList(exec("security", ["list-keychains", "-d", "user"], "Could not read the keychain search list"));
452
+ state.keychainCreated = true;
453
+ exec("security", ["create-keychain", "-p", keychainPassword, keychainPath], "Could not create the temporary keychain");
454
+ exec("security", ["set-keychain-settings", "-lut", String(KEYCHAIN_LOCK_TIMEOUT_SECONDS), keychainPath], "Could not configure the temporary keychain");
455
+ exec("security", ["unlock-keychain", "-p", keychainPassword, keychainPath], "Could not unlock the temporary keychain");
456
+ try {
457
+ exec("security", ["import", p12File, "-k", keychainPath, "-P", p12Password, "-f", "pkcs12", "-T", "/usr/bin/codesign", "-T", "/usr/bin/security"], "security import failed");
458
+ } catch (error) {
459
+ throw (0, _utils.nativeError)("IOS-012", `could not import ${p12Name}; check p12Password in credentials/ios/signing.properties (${error.message})`);
460
+ }
461
+ exec("security", ["set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", keychainPassword, keychainPath], "Could not allow codesign to use the imported key");
462
+ exec("security", ["list-keychains", "-d", "user", "-s", keychainPath, ...state.originalList], "Could not add the temporary keychain to the search list");
463
+ const identity = parseIdentity(exec("security", ["find-identity", "-v", "-p", "codesigning", keychainPath], "Could not list the imported identities"));
464
+ if (!identity) {
465
+ throw (0, _utils.nativeError)("IOS-012", `${p12Name} holds no valid code signing identity`);
466
+ }
467
+ const profile = readProvisioningProfile(profileFile, {
468
+ workDir,
469
+ run
470
+ });
471
+ if (profile.expirationDate && new Date(profile.expirationDate) < now()) {
472
+ throw (0, _utils.nativeError)("IOS-012", `the provisioning profile "${profile.name}" expired on ${profile.expirationDate}`);
473
+ }
474
+ if (!profileMatchesBundleId(profile.applicationIdentifier, bundleId)) {
475
+ throw (0, _utils.nativeError)("IOS-012", `the provisioning profile "${profile.name}" is for ${profile.applicationIdentifier}, not ${bundleId}`);
476
+ }
477
+ for (const dir of profileDirectories(homeDir)) {
478
+ if (!_fs.default.existsSync(dir)) {
479
+ // Track each new level, so cleanup removes only what it added.
480
+ let missing = dir;
481
+ const created = [];
482
+ while (!_fs.default.existsSync(missing)) {
483
+ created.unshift(missing);
484
+ missing = _path.default.dirname(missing);
485
+ }
486
+ _fs.default.mkdirSync(dir, {
487
+ recursive: true
488
+ });
489
+ state.createdDirs.push(...created);
490
+ }
491
+ const target = _path.default.join(dir, `${profile.uuid}.mobileprovision`);
492
+ // A profile already there is the developer's own: use it, never remove it.
493
+ if (_fs.default.existsSync(target)) continue;
494
+ _fs.default.copyFileSync(profileFile, target);
495
+ _fs.default.chmodSync(target, 0o600);
496
+ state.installed.push(target);
497
+ }
498
+ log(`Signing with "${identity.name}" and profile "${profile.name}" (${profile.uuid})`, "info");
499
+ return {
500
+ keychainPath,
501
+ identity,
502
+ profile,
503
+ cleanup
504
+ };
505
+ } catch (error) {
506
+ cleanup();
507
+ throw error;
508
+ }
509
+ }
510
+
511
+ /**
512
+ * The ExportOptions.plist contents for `xcodebuild -exportArchive`.
513
+ *
514
+ * manageAppVersionAndBuildNumber is off so the IPA carries exactly the
515
+ * version and build number Catalyst set.
516
+ *
517
+ * @param {object} options
518
+ * @param {string} options.method
519
+ * @param {string} [options.teamId]
520
+ * @param {string} options.signingStyle - "automatic" or "manual"
521
+ * @param {string} [options.bundleId]
522
+ * @param {string} [options.profileName]
523
+ * @param {string} [options.signingCertificate]
524
+ * @returns {Record<string, any>}
525
+ */
526
+ function buildExportOptions({
527
+ method,
528
+ teamId,
529
+ signingStyle,
530
+ bundleId,
531
+ profileName,
532
+ signingCertificate
533
+ }) {
534
+ const options = {
535
+ method,
536
+ signingStyle,
537
+ destination: "export",
538
+ stripSwiftSymbols: true
539
+ };
540
+ if (teamId) options.teamID = teamId;
541
+ if (signingStyle === "manual") {
542
+ if (bundleId && profileName) options.provisioningProfiles = {
543
+ [bundleId]: profileName
544
+ };
545
+ if (signingCertificate) options.signingCertificate = signingCertificate;
546
+ }
547
+ if (method === "app-store-connect") {
548
+ options.uploadSymbols = true;
549
+ options.manageAppVersionAndBuildNumber = false;
550
+ }
551
+ return options;
552
+ }
553
+
554
+ /**
555
+ * Write the export options as an XML plist. plutil does the encoding, so a
556
+ * profile name or team with &, < or quotes comes out escaped.
557
+ */
558
+ function writeExportOptionsPlist(options, file, {
559
+ run = defaultRun
560
+ } = {}) {
561
+ const json = `${file}.json`;
562
+ _fs.default.writeFileSync(json, JSON.stringify(options), {
563
+ mode: 0o600
564
+ });
565
+ try {
566
+ run("plutil", ["-convert", "xml1", "-o", file, json]);
567
+ } finally {
568
+ _fs.default.rmSync(json, {
569
+ force: true
570
+ });
571
+ }
572
+ return file;
573
+ }
574
+ const SIGNING_FAILURE_PATTERNS = [/requires a development team/i, /No Accounts?\b/i, /No Account for Team/i, /No profiles? for ['"].*['"] (?:were|was) found/i, /No signing certificate/i, /requires a provisioning profile/i, /Provisioning profile ".*" (?:doesn't|does not|is not|has expired)/i, /doesn't match the entitlements file's value/i, /errSecInternalComponent/i, /Code ?Sign(?:ing)? error/i, /Automatic signing is (?:disabled|unable)/i, /authentication ?key/i, /Failed Registering Bundle Identifier/i];
575
+
576
+ /** True when xcodebuild failed because of signing rather than the code. */
577
+ function isSigningFailure(output) {
578
+ const text = String(output || "");
579
+ return SIGNING_FAILURE_PATTERNS.some(pattern => pattern.test(text));
580
+ }
581
+
582
+ /**
583
+ * The coded error for a signing failure, keeping xcodebuild's transcript for the diagnostic.
584
+ *
585
+ * @param {string} detail
586
+ * @param {any} [cause]
587
+ * @returns {any}
588
+ */
589
+ function signingSetupError(detail, cause) {
590
+ const error = (0, _utils.nativeError)("IOS-010", detail);
591
+ if (cause) {
592
+ error.cause = cause;
593
+ if (cause.output) error.output = cause.output;
594
+ }
595
+ return error;
596
+ }