@autifyhq/aximo-cli 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -70
- package/dist/client.js +16 -1
- package/dist/commands/mobile/connect-core.d.ts +2 -1
- package/dist/commands/mobile/connect-core.js +112 -9
- package/dist/commands/mobile/connect.js +6 -3
- package/dist/commands/mobile/connect.test.js +7 -1
- package/dist/commands/mobile/doctor.d.ts +4 -0
- package/dist/commands/mobile/doctor.js +62 -16
- package/dist/commands/mobile/doctor.test.d.ts +1 -0
- package/dist/commands/mobile/doctor.test.js +28 -0
- package/dist/commands/mobile/install.d.ts +10 -2
- package/dist/commands/mobile/install.js +57 -15
- package/dist/commands/mobile/install.test.js +44 -6
- package/dist/commands/mobile/list.d.ts +3 -0
- package/dist/commands/mobile/list.js +40 -14
- package/dist/commands/mobile/list.test.d.ts +1 -0
- package/dist/commands/mobile/list.test.js +24 -0
- package/dist/commands/mobile/output.d.ts +13 -0
- package/dist/commands/mobile/output.js +21 -0
- package/dist/commands/mobile/output.test.d.ts +1 -0
- package/dist/commands/mobile/output.test.js +54 -0
- package/dist/commands/mobile/register.d.ts +1 -0
- package/dist/commands/mobile/register.js +11 -3
- package/dist/commands/mobile/setup.d.ts +1 -0
- package/dist/commands/mobile/setup.js +7 -2
- package/dist/commands/mobile/unregister.d.ts +7 -3
- package/dist/commands/mobile/unregister.js +20 -9
- package/dist/mobile-control/app-install.d.ts +18 -5
- package/dist/mobile-control/app-install.js +33 -6
- package/dist/mobile-control/app-install.test.js +42 -7
- package/dist/mobile-control/appium-controller.d.ts +8 -0
- package/dist/mobile-control/appium-controller.js +19 -4
- package/dist/mobile-control/bundle-id-map.d.ts +9 -0
- package/dist/mobile-control/bundle-id-map.js +27 -0
- package/dist/mobile-control/devices.d.ts +6 -3
- package/dist/mobile-control/devices.js +26 -4
- package/dist/mobile-control/env-flags.d.ts +5 -0
- package/dist/mobile-control/env-flags.js +10 -0
- package/dist/mobile-control/env-flags.test.d.ts +1 -0
- package/dist/mobile-control/env-flags.test.js +21 -0
- package/dist/mobile-control/exec.d.ts +5 -1
- package/dist/mobile-control/exec.js +2 -2
- package/dist/mobile-control/ios-auto-resign.d.ts +45 -0
- package/dist/mobile-control/ios-auto-resign.js +108 -0
- package/dist/mobile-control/ios-auto-resign.test.d.ts +1 -0
- package/dist/mobile-control/ios-auto-resign.test.js +150 -0
- package/dist/mobile-control/ios-devicectl.d.ts +39 -0
- package/dist/mobile-control/ios-devicectl.js +87 -0
- package/dist/mobile-control/ios-devicectl.test.d.ts +1 -0
- package/dist/mobile-control/ios-devicectl.test.js +110 -0
- package/dist/mobile-control/ios-identity.d.ts +27 -0
- package/dist/mobile-control/ios-identity.js +49 -0
- package/dist/mobile-control/ios-identity.test.d.ts +1 -0
- package/dist/mobile-control/ios-identity.test.js +102 -0
- package/dist/mobile-control/ios-mint.d.ts +39 -0
- package/dist/mobile-control/ios-mint.js +78 -0
- package/dist/mobile-control/ios-mint.test.d.ts +1 -0
- package/dist/mobile-control/ios-mint.test.js +94 -0
- package/dist/mobile-control/ios-profiles.d.ts +46 -0
- package/dist/mobile-control/ios-profiles.js +106 -0
- package/dist/mobile-control/ios-profiles.test.d.ts +1 -0
- package/dist/mobile-control/ios-profiles.test.js +154 -0
- package/dist/mobile-control/ios-resign.d.ts +37 -0
- package/dist/mobile-control/ios-resign.js +101 -0
- package/dist/mobile-control/ios-resign.test.d.ts +1 -0
- package/dist/mobile-control/ios-resign.test.js +84 -0
- package/dist/mobile-control/ios-session-error.d.ts +10 -0
- package/dist/mobile-control/ios-session-error.js +34 -0
- package/dist/mobile-control/ios-session-error.test.d.ts +1 -0
- package/dist/mobile-control/ios-session-error.test.js +30 -0
- package/dist/mobile-control/ios-signing.d.ts +33 -5
- package/dist/mobile-control/ios-signing.js +56 -0
- package/dist/mobile-control/ios-signing.test.js +87 -0
- package/dist/mobile-control/ios-tunnel.d.ts +57 -0
- package/dist/mobile-control/ios-tunnel.js +150 -0
- package/dist/mobile-control/ios-tunnel.test.d.ts +1 -0
- package/dist/mobile-control/ios-tunnel.test.js +120 -0
- package/dist/mobile-control/ios-wda-cache.d.ts +39 -0
- package/dist/mobile-control/ios-wda-cache.js +157 -0
- package/dist/mobile-control/ios-wda-cache.test.d.ts +1 -0
- package/dist/mobile-control/ios-wda-cache.test.js +128 -0
- package/dist/mobile-control/ios-wda-diagnostics.d.ts +14 -0
- package/dist/mobile-control/ios-wda-diagnostics.js +138 -0
- package/dist/mobile-control/ios-wda-diagnostics.test.d.ts +1 -0
- package/dist/mobile-control/ios-wda-diagnostics.test.js +77 -0
- package/dist/mobile-control/ios-wda-preinstall.d.ts +64 -0
- package/dist/mobile-control/ios-wda-preinstall.js +165 -0
- package/dist/mobile-control/ios-wda-preinstall.test.d.ts +1 -0
- package/dist/mobile-control/ios-wda-preinstall.test.js +111 -0
- package/dist/mobile-control/mobile-errors.d.ts +12 -0
- package/dist/mobile-control/mobile-errors.js +91 -0
- package/dist/mobile-control/mobile-errors.test.d.ts +1 -0
- package/dist/mobile-control/mobile-errors.test.js +48 -0
- package/dist/mobile-control/tunnel.js +79 -2
- package/dist/mobile-control/tunnel.test.js +82 -0
- package/package.json +2 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { listSigningIdentities, selectIdentity } from "./ios-identity.js";
|
|
3
|
+
// Self-signed cert with an Apple-Development-shaped subject:
|
|
4
|
+
// CN=Apple Development: test@example.com (ABC123DEF4), OU=TESTTEAM99.
|
|
5
|
+
const TEST_CERT_PEM = `-----BEGIN CERTIFICATE-----
|
|
6
|
+
MIID9zCCAt+gAwIBAgIUOJubshaRqESC9uQhAwvzfa34H8swDQYJKoZIhvcNAQEL
|
|
7
|
+
BQAwgYoxFzAVBgoJkiaJk/IsZAEBDAdURVNUVUlEMTkwNwYDVQQDDDBBcHBsZSBE
|
|
8
|
+
ZXZlbG9wbWVudDogdGVzdEBleGFtcGxlLmNvbSAoQUJDMTIzREVGNCkxEzARBgNV
|
|
9
|
+
BAsMClRFU1RURUFNOTkxEjAQBgNVBAoMCVRlc3QgVXNlcjELMAkGA1UEBhMCVVMw
|
|
10
|
+
HhcNMjYwNzEzMDA1NDA5WhcNMzYwNzEwMDA1NDA5WjCBijEXMBUGCgmSJomT8ixk
|
|
11
|
+
AQEMB1RFU1RVSUQxOTA3BgNVBAMMMEFwcGxlIERldmVsb3BtZW50OiB0ZXN0QGV4
|
|
12
|
+
YW1wbGUuY29tIChBQkMxMjNERUY0KTETMBEGA1UECwwKVEVTVFRFQU05OTESMBAG
|
|
13
|
+
A1UECgwJVGVzdCBVc2VyMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQAD
|
|
14
|
+
ggEPADCCAQoCggEBAJw2xHLbanqoyZM6IbH2g5RJGnFW6IROyOiZrowGQ9IgsOOf
|
|
15
|
+
LpVCT3iIwknt0pLLy0pIoGqpygW34DvtR0NNmh8sv82Ek3U4gHnp7BUcaJ7NzjbX
|
|
16
|
+
xOppqP3GHBfiVCakfDmSYvwLHuPKr9kwrQyGTWzAobNHVOFAuHAvJrvlkellgBfj
|
|
17
|
+
uyEoVBCX5NVwpsUpKpFgscOzuSaKsxu+aoyBJcHdXlJV2FGJoiaN8Y90Rt2apZEc
|
|
18
|
+
Mom1lp1yvNV9dOhuyIrJcKox1XNn6f5Eu6V1PqIgU1CrcRpXcKEgOWzug4enqEMf
|
|
19
|
+
xZbgo4rCetI5TmJm9fyLh8UfaxWrVFBwZcW6eSECAwEAAaNTMFEwHQYDVR0OBBYE
|
|
20
|
+
FK3Fb+zexJRT0DpmhVneTEdjInJZMB8GA1UdIwQYMBaAFK3Fb+zexJRT0DpmhVne
|
|
21
|
+
TEdjInJZMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFaeA8NP
|
|
22
|
+
hM2JS5l16lOcrjgPMWpWMKH6jRlvZ8T6+Lo2O1ehA72kXm6hZ+dO1WNV/Ry7/Hpa
|
|
23
|
+
S6SdJ/HcXwJzM4aJ0tMOpN92YfL/L3l2ogA63/2P1aP9YHhuyPYnb5mRHE4/d6+g
|
|
24
|
+
ZeRhjWKp+x/7T418ma+NdP73u+Tkgdp/NEGEs3UQOBSmEFGCo09QUTaE1eZlqSCw
|
|
25
|
+
t1MxzdXx3+TtgyvDZnhKEF+FrDSyCaPCGCveactFi3sCiHuall7NdDu9O1xV1ASP
|
|
26
|
+
HGeFYCBbAiCSRQ8RAkLqUnCfuZM5lE6JkyZuN0drsXw0WTnf/4KYDB4Hnv543mD4
|
|
27
|
+
ROvQLb+7ZuM7l9o=
|
|
28
|
+
-----END CERTIFICATE-----
|
|
29
|
+
`;
|
|
30
|
+
const HASH_A = "0B0DF9CFF90EC3358C9A2CB52F006EB83FD7748C";
|
|
31
|
+
const HASH_B = "1122334455667788990011223344556677889900";
|
|
32
|
+
function fakeExec(findIdentityOut, certPem = TEST_CERT_PEM) {
|
|
33
|
+
return async (file, args) => {
|
|
34
|
+
if (file === "security" && args[0] === "find-identity") {
|
|
35
|
+
return { stdout: Buffer.from(findIdentityOut), stderr: "" };
|
|
36
|
+
}
|
|
37
|
+
if (file === "security" && args[0] === "find-certificate") {
|
|
38
|
+
if (certPem instanceof Error)
|
|
39
|
+
throw certPem;
|
|
40
|
+
return { stdout: Buffer.from(certPem), stderr: "" };
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`unexpected exec: ${file} ${args.join(" ")}`);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
describe("listSigningIdentities", () => {
|
|
46
|
+
it("parses development identities and resolves the team id from the cert OU", async () => {
|
|
47
|
+
const out = ` 1) ${HASH_A} "Apple Development: test@example.com (ABC123DEF4)"\n 1 valid identities found\n`;
|
|
48
|
+
const identities = await listSigningIdentities(fakeExec(out));
|
|
49
|
+
expect(identities).toEqual([
|
|
50
|
+
{
|
|
51
|
+
hash: HASH_A,
|
|
52
|
+
name: "Apple Development: test@example.com (ABC123DEF4)",
|
|
53
|
+
teamId: "TESTTEAM99",
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
it("ignores non-development identities and duplicate hashes", async () => {
|
|
58
|
+
const out = [
|
|
59
|
+
` 1) ${HASH_A} "Apple Development: test@example.com (ABC123DEF4)"`,
|
|
60
|
+
` 2) ${HASH_A} "Apple Development: test@example.com (ABC123DEF4)"`,
|
|
61
|
+
` 3) ${HASH_B} "Apple Distribution: Some Corp (TEAM000001)"`,
|
|
62
|
+
" 3 valid identities found",
|
|
63
|
+
].join("\n");
|
|
64
|
+
const identities = await listSigningIdentities(fakeExec(out));
|
|
65
|
+
expect(identities).toHaveLength(1);
|
|
66
|
+
expect(identities[0]?.hash).toBe(HASH_A);
|
|
67
|
+
});
|
|
68
|
+
it("returns teamId null when the certificate cannot be read", async () => {
|
|
69
|
+
const out = ` 1) ${HASH_A} "Apple Development: test@example.com (ABC123DEF4)"\n`;
|
|
70
|
+
const identities = await listSigningIdentities(fakeExec(out, new Error("not found")));
|
|
71
|
+
expect(identities).toEqual([
|
|
72
|
+
{ hash: HASH_A, name: "Apple Development: test@example.com (ABC123DEF4)", teamId: null },
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
it("returns [] when security itself fails (not macOS / no keychain)", async () => {
|
|
76
|
+
const exec = async () => {
|
|
77
|
+
throw new Error("ENOENT");
|
|
78
|
+
};
|
|
79
|
+
expect(await listSigningIdentities(exec)).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
it("returns [] when no identities are listed", async () => {
|
|
82
|
+
expect(await listSigningIdentities(fakeExec(" 0 valid identities found\n"))).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe("selectIdentity", () => {
|
|
86
|
+
const identities = [
|
|
87
|
+
{ hash: HASH_A, name: "Apple Development: a@x.com (AAAA111111)", teamId: "TEAM_A" },
|
|
88
|
+
{ hash: HASH_B, name: "Apple Development: b@x.com (BBBB222222)", teamId: "TEAM_B" },
|
|
89
|
+
];
|
|
90
|
+
it("returns the first identity without a preferred team", () => {
|
|
91
|
+
expect(selectIdentity(identities)?.hash).toBe(HASH_A);
|
|
92
|
+
});
|
|
93
|
+
it("filters by preferred team id", () => {
|
|
94
|
+
expect(selectIdentity(identities, "TEAM_B")?.hash).toBe(HASH_B);
|
|
95
|
+
});
|
|
96
|
+
it("returns null when the preferred team has no identity", () => {
|
|
97
|
+
expect(selectIdentity(identities, "TEAM_C")).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
it("returns null for an empty list", () => {
|
|
100
|
+
expect(selectIdentity([])).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ExecFn } from "./exec.js";
|
|
2
|
+
/**
|
|
3
|
+
* Mint (or renew) an Apple development provisioning profile without any user
|
|
4
|
+
* input, using Xcode automatic signing.
|
|
5
|
+
*
|
|
6
|
+
* Free/personal teams cannot create profiles through the developer portal API —
|
|
7
|
+
* the ONLY way is an Xcode automatic-signing build. The WebDriverAgent Xcode
|
|
8
|
+
* project that ships inside the XCUITest driver works as a generic minting rig:
|
|
9
|
+
* building its runner scheme with an arbitrary PRODUCT_BUNDLE_IDENTIFIER makes
|
|
10
|
+
* Xcode register the App ID, register the destination device, and write a fresh
|
|
11
|
+
* 7-day profile for `<bundle-id>.xctrunner` into the profiles directory.
|
|
12
|
+
*
|
|
13
|
+
* Requirements it cannot work around (surfaced as actionable errors): the Apple
|
|
14
|
+
* ID must be signed into Xcode (Settings → Accounts), free teams are capped at
|
|
15
|
+
* 10 App IDs per 7 days, and well-known bundle ids may be "taken" for personal
|
|
16
|
+
* teams.
|
|
17
|
+
*/
|
|
18
|
+
export interface MintProfileOptions {
|
|
19
|
+
teamId: string;
|
|
20
|
+
/** Base bundle id; the minted profile's App ID is `<bundleId>.xctrunner`. */
|
|
21
|
+
bundleId: string;
|
|
22
|
+
udid: string;
|
|
23
|
+
signingId?: string;
|
|
24
|
+
exec?: ExecFn;
|
|
25
|
+
wdaProjectPath?: string;
|
|
26
|
+
env?: NodeJS.ProcessEnv;
|
|
27
|
+
}
|
|
28
|
+
/** App ID the rig actually mints for a given PRODUCT_BUNDLE_IDENTIFIER. */
|
|
29
|
+
export declare function mintedAppId(bundleId: string): string;
|
|
30
|
+
/** Locate the WDA project inside the managed Appium home (hoisted or nested). */
|
|
31
|
+
export declare function findWdaProject(appiumHome?: string): string | null;
|
|
32
|
+
/** Map a raw xcodebuild failure to an actionable next step, when recognized. */
|
|
33
|
+
export declare function mintFailureHint(message: string): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Run the minting build. Resolves when xcodebuild succeeds (the fresh profile is
|
|
36
|
+
* then discoverable via ios-profiles); throws with an actionable message when it
|
|
37
|
+
* fails. Build output goes to a throwaway DerivedData dir.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mintProfileViaWdaRig(options: MintProfileOptions): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getAppiumHome } from "./appium-server.js";
|
|
5
|
+
import { execBinary } from "./exec.js";
|
|
6
|
+
/** App ID the rig actually mints for a given PRODUCT_BUNDLE_IDENTIFIER. */
|
|
7
|
+
export function mintedAppId(bundleId) {
|
|
8
|
+
return `${bundleId}.xctrunner`;
|
|
9
|
+
}
|
|
10
|
+
/** Locate the WDA project inside the managed Appium home (hoisted or nested). */
|
|
11
|
+
export function findWdaProject(appiumHome = getAppiumHome()) {
|
|
12
|
+
const candidates = [
|
|
13
|
+
join(appiumHome, "node_modules", "appium-xcuitest-driver", "node_modules", "appium-webdriveragent", "WebDriverAgent.xcodeproj"),
|
|
14
|
+
join(appiumHome, "node_modules", "appium-webdriveragent", "WebDriverAgent.xcodeproj"),
|
|
15
|
+
];
|
|
16
|
+
return candidates.find((p) => existsSync(p)) ?? null;
|
|
17
|
+
}
|
|
18
|
+
const FAILURE_HINTS = [
|
|
19
|
+
{
|
|
20
|
+
test: /no accounts|No Apple ID|requires you to be signed in|No profiles .* were found.*automatic/is,
|
|
21
|
+
hint: "Sign into Xcode with your Apple ID (Xcode → Settings → Accounts), then retry. Profile minting needs an Xcode-managed account.",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
test: /cannot be registered to your development team|not available.*bundle|Failed Registering Bundle Identifier/is,
|
|
25
|
+
hint: "This bundle id can't be registered to your team. It will be retried with an Aximo-owned id; if that also fails, set AXIMO_IOS_WDA_BUNDLE_ID to a unique id.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
test: /maximum (number of )?App IDs|reached the maximum|too many App IDs/i,
|
|
29
|
+
hint: "Your free Apple team hit the 10-App-IDs-per-week cap. Wait for the cap to reset, or use a paid Apple Developer account.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
test: /is not installed\. Please download and install the platform|iOS [\d.]+ is not installed/i,
|
|
33
|
+
hint: "Install the iOS platform components: `xcodebuild -downloadPlatform iOS` (or Xcode → Settings → Components).",
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
/** Map a raw xcodebuild failure to an actionable next step, when recognized. */
|
|
37
|
+
export function mintFailureHint(message) {
|
|
38
|
+
return FAILURE_HINTS.find((r) => r.test.test(message))?.hint;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run the minting build. Resolves when xcodebuild succeeds (the fresh profile is
|
|
42
|
+
* then discoverable via ios-profiles); throws with an actionable message when it
|
|
43
|
+
* fails. Build output goes to a throwaway DerivedData dir.
|
|
44
|
+
*/
|
|
45
|
+
export async function mintProfileViaWdaRig(options) {
|
|
46
|
+
const exec = options.exec ?? execBinary;
|
|
47
|
+
const project = options.wdaProjectPath ?? findWdaProject();
|
|
48
|
+
if (!project || !existsSync(project)) {
|
|
49
|
+
throw new Error("WebDriverAgent project not found in the Appium home. Run `aximo mobile setup --platform ios` first.");
|
|
50
|
+
}
|
|
51
|
+
const scratch = mkdtempSync(join(tmpdir(), "aximo-mint-"));
|
|
52
|
+
try {
|
|
53
|
+
await exec("xcodebuild", [
|
|
54
|
+
"-project",
|
|
55
|
+
project,
|
|
56
|
+
"-scheme",
|
|
57
|
+
"WebDriverAgentRunner",
|
|
58
|
+
"-destination",
|
|
59
|
+
`id=${options.udid}`,
|
|
60
|
+
"-derivedDataPath",
|
|
61
|
+
scratch,
|
|
62
|
+
"-allowProvisioningUpdates",
|
|
63
|
+
"-allowProvisioningDeviceRegistration",
|
|
64
|
+
`DEVELOPMENT_TEAM=${options.teamId}`,
|
|
65
|
+
`PRODUCT_BUNDLE_IDENTIFIER=${options.bundleId}`,
|
|
66
|
+
`CODE_SIGN_IDENTITY=${options.signingId ?? "Apple Development"}`,
|
|
67
|
+
"build-for-testing",
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
const hint = mintFailureHint(message);
|
|
73
|
+
throw new Error(hint ? `${message} — ${hint}` : message);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { findWdaProject, mintFailureHint, mintProfileViaWdaRig, mintedAppId } from "./ios-mint.js";
|
|
6
|
+
describe("mintedAppId", () => {
|
|
7
|
+
it("appends the .xctrunner suffix Xcode gives runner apps", () => {
|
|
8
|
+
expect(mintedAppId("com.aximo.resign.team1")).toBe("com.aximo.resign.team1.xctrunner");
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
describe("findWdaProject", () => {
|
|
12
|
+
let home;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
home = mkdtempSync(join(tmpdir(), "aximo-mint-home-"));
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(home, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
it("finds the nested driver copy", () => {
|
|
20
|
+
const proj = join(home, "node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj");
|
|
21
|
+
mkdirSync(proj, { recursive: true });
|
|
22
|
+
expect(findWdaProject(home)).toBe(proj);
|
|
23
|
+
});
|
|
24
|
+
it("falls back to the hoisted copy and null when absent", () => {
|
|
25
|
+
const hoisted = join(home, "node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj");
|
|
26
|
+
mkdirSync(hoisted, { recursive: true });
|
|
27
|
+
expect(findWdaProject(home)).toBe(hoisted);
|
|
28
|
+
rmSync(hoisted, { recursive: true, force: true });
|
|
29
|
+
expect(findWdaProject(home)).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("mintProfileViaWdaRig", () => {
|
|
33
|
+
let home;
|
|
34
|
+
let project;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
home = mkdtempSync(join(tmpdir(), "aximo-mint-home-"));
|
|
37
|
+
project = join(home, "WebDriverAgent.xcodeproj");
|
|
38
|
+
mkdirSync(project, { recursive: true });
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
rmSync(home, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
it("invokes xcodebuild with automatic-signing + registration flags", async () => {
|
|
44
|
+
let seen = [];
|
|
45
|
+
const exec = async (file, args) => {
|
|
46
|
+
seen = [file, ...args];
|
|
47
|
+
return { stdout: Buffer.from("** TEST BUILD SUCCEEDED **"), stderr: "" };
|
|
48
|
+
};
|
|
49
|
+
await mintProfileViaWdaRig({
|
|
50
|
+
teamId: "TESTTEAM99",
|
|
51
|
+
bundleId: "com.aximo.resign.testteam99",
|
|
52
|
+
udid: "UDID-1",
|
|
53
|
+
exec,
|
|
54
|
+
wdaProjectPath: project,
|
|
55
|
+
});
|
|
56
|
+
expect(seen[0]).toBe("xcodebuild");
|
|
57
|
+
expect(seen).toContain("-allowProvisioningUpdates");
|
|
58
|
+
expect(seen).toContain("-allowProvisioningDeviceRegistration");
|
|
59
|
+
expect(seen).toContain("id=UDID-1");
|
|
60
|
+
expect(seen).toContain("DEVELOPMENT_TEAM=TESTTEAM99");
|
|
61
|
+
expect(seen).toContain("PRODUCT_BUNDLE_IDENTIFIER=com.aximo.resign.testteam99");
|
|
62
|
+
expect(seen).toContain("build-for-testing");
|
|
63
|
+
});
|
|
64
|
+
it("appends an actionable hint for recognized failures", async () => {
|
|
65
|
+
const exec = async () => {
|
|
66
|
+
throw new Error("xcodebuild: error: No Accounts: Add a new account in Accounts settings.");
|
|
67
|
+
};
|
|
68
|
+
await expect(mintProfileViaWdaRig({
|
|
69
|
+
teamId: "T",
|
|
70
|
+
bundleId: "b",
|
|
71
|
+
udid: "U",
|
|
72
|
+
exec,
|
|
73
|
+
wdaProjectPath: project,
|
|
74
|
+
})).rejects.toThrow(/Sign into Xcode with your Apple ID/);
|
|
75
|
+
});
|
|
76
|
+
it("throws a setup hint when the WDA project is missing", async () => {
|
|
77
|
+
await expect(mintProfileViaWdaRig({
|
|
78
|
+
teamId: "T",
|
|
79
|
+
bundleId: "b",
|
|
80
|
+
udid: "U",
|
|
81
|
+
wdaProjectPath: "/nonexistent/WebDriverAgent.xcodeproj",
|
|
82
|
+
exec: async () => ({ stdout: Buffer.from(""), stderr: "" }),
|
|
83
|
+
})).rejects.toThrow(/aximo mobile setup/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe("mintFailureHint", () => {
|
|
87
|
+
it("maps the common failure families", () => {
|
|
88
|
+
expect(mintFailureHint("No Accounts: Add a new account")).toMatch(/Sign into Xcode/);
|
|
89
|
+
expect(mintFailureHint('The app identifier "X" cannot be registered to your development team')).toMatch(/can't be registered/);
|
|
90
|
+
expect(mintFailureHint("You have reached the maximum number of App IDs")).toMatch(/10-App-IDs-per-week/);
|
|
91
|
+
expect(mintFailureHint("iOS 26.5 is not installed. Please download and install the platform")).toMatch(/downloadPlatform/);
|
|
92
|
+
expect(mintFailureHint("some unrelated error")).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ExecFn } from "./exec.js";
|
|
2
|
+
/**
|
|
3
|
+
* Discover and match installed Apple provisioning profiles.
|
|
4
|
+
*
|
|
5
|
+
* Re-signing (the app under test, or a prebuilt WebDriverAgent) needs a profile
|
|
6
|
+
* that covers the target bundle id AND the device UDID and is still valid.
|
|
7
|
+
* Profiles are scanned from disk instead of asking the user for paths:
|
|
8
|
+
* Xcode 16+ stores them under ~/Library/Developer/Xcode/UserData, older
|
|
9
|
+
* toolchains under ~/Library/MobileDevice — both are checked.
|
|
10
|
+
*/
|
|
11
|
+
export interface ProvisioningProfile {
|
|
12
|
+
path: string;
|
|
13
|
+
name: string;
|
|
14
|
+
teamId: string;
|
|
15
|
+
/** Bundle-id portion of the App ID; "*" (or a ".*" suffix) for wildcards. */
|
|
16
|
+
bundleId: string;
|
|
17
|
+
isWildcard: boolean;
|
|
18
|
+
expirationDate: Date;
|
|
19
|
+
/** Devices the profile embeds; null when it has no device list (enterprise). */
|
|
20
|
+
provisionedDevices: string[] | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function profileDirs(home?: string): string[];
|
|
23
|
+
/** Parse one decoded (XML plist) profile; null when required fields are missing. */
|
|
24
|
+
export declare function parseProfileXml(xml: string, path: string): ProvisioningProfile | null;
|
|
25
|
+
export interface ListProfilesOptions {
|
|
26
|
+
exec?: ExecFn;
|
|
27
|
+
dirs?: string[];
|
|
28
|
+
}
|
|
29
|
+
/** Scan both profile directories, decode each profile via `security cms -D`. */
|
|
30
|
+
export declare function listProvisioningProfiles(options?: ListProfilesOptions): Promise<ProvisioningProfile[]>;
|
|
31
|
+
/** True when the profile's App ID covers the bundle id (exact or wildcard). */
|
|
32
|
+
export declare function coversBundleId(profile: ProvisioningProfile, bundleId: string): boolean;
|
|
33
|
+
export interface SelectProfileCriteria {
|
|
34
|
+
bundleId: string;
|
|
35
|
+
udid: string;
|
|
36
|
+
teamId?: string;
|
|
37
|
+
now?: number;
|
|
38
|
+
minRemainingMs?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Pick the best usable profile: covers the bundle id + device UDID (a missing
|
|
42
|
+
* device list means enterprise — always covered) and stays valid for at least
|
|
43
|
+
* `minRemainingMs`. Exact App IDs beat wildcards (no bundle-id surprises), then
|
|
44
|
+
* the later expiry wins.
|
|
45
|
+
*/
|
|
46
|
+
export declare function selectProfile(profiles: ProvisioningProfile[], criteria: SelectProfileCriteria): ProvisioningProfile | null;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { execBinary } from "./exec.js";
|
|
5
|
+
const DEFAULT_MIN_REMAINING_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
export function profileDirs(home = homedir()) {
|
|
7
|
+
return [
|
|
8
|
+
join(home, "Library", "Developer", "Xcode", "UserData", "Provisioning Profiles"),
|
|
9
|
+
join(home, "Library", "MobileDevice", "Provisioning Profiles"),
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
function extract(xml, key) {
|
|
13
|
+
const match = xml.match(new RegExp(`<key>${key}</key>\\s*<(?:string|date)>([^<]+)</(?:string|date)>`));
|
|
14
|
+
return match?.[1] ?? null;
|
|
15
|
+
}
|
|
16
|
+
function extractStringArray(xml, key) {
|
|
17
|
+
const block = xml.match(new RegExp(`<key>${key}</key>\\s*<array>([\\s\\S]*?)</array>`));
|
|
18
|
+
if (!block?.[1])
|
|
19
|
+
return null;
|
|
20
|
+
return [...block[1].matchAll(/<string>([^<]+)<\/string>/g)]
|
|
21
|
+
.map((m) => m[1])
|
|
22
|
+
.filter((s) => Boolean(s));
|
|
23
|
+
}
|
|
24
|
+
/** Parse one decoded (XML plist) profile; null when required fields are missing. */
|
|
25
|
+
export function parseProfileXml(xml, path) {
|
|
26
|
+
const name = extract(xml, "Name");
|
|
27
|
+
const expiration = extract(xml, "ExpirationDate");
|
|
28
|
+
const appId = xml.match(/<key>application-identifier<\/key>\s*<string>([^<]+)<\/string>/)?.[1];
|
|
29
|
+
const teamId = extractStringArray(xml, "TeamIdentifier")?.[0];
|
|
30
|
+
if (!name || !expiration || !appId || !teamId)
|
|
31
|
+
return null;
|
|
32
|
+
const expirationDate = new Date(expiration);
|
|
33
|
+
if (Number.isNaN(expirationDate.getTime()))
|
|
34
|
+
return null;
|
|
35
|
+
const bundleId = appId.startsWith(`${teamId}.`) ? appId.slice(teamId.length + 1) : appId;
|
|
36
|
+
return {
|
|
37
|
+
path,
|
|
38
|
+
name,
|
|
39
|
+
teamId,
|
|
40
|
+
bundleId,
|
|
41
|
+
isWildcard: bundleId === "*" || bundleId.endsWith(".*"),
|
|
42
|
+
expirationDate,
|
|
43
|
+
provisionedDevices: extractStringArray(xml, "ProvisionedDevices"),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Scan both profile directories, decode each profile via `security cms -D`. */
|
|
47
|
+
export async function listProvisioningProfiles(options = {}) {
|
|
48
|
+
const exec = options.exec ?? execBinary;
|
|
49
|
+
const dirs = options.dirs ?? profileDirs();
|
|
50
|
+
const profiles = [];
|
|
51
|
+
for (const dir of dirs) {
|
|
52
|
+
if (!existsSync(dir))
|
|
53
|
+
continue;
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = readdirSync(dir);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!entry.endsWith(".mobileprovision"))
|
|
63
|
+
continue;
|
|
64
|
+
const path = join(dir, entry);
|
|
65
|
+
try {
|
|
66
|
+
const { stdout } = await exec("security", ["cms", "-D", "-i", path]);
|
|
67
|
+
const profile = parseProfileXml(stdout.toString("utf-8"), path);
|
|
68
|
+
if (profile)
|
|
69
|
+
profiles.push(profile);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Unreadable/corrupt profile — skip it rather than fail discovery.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return profiles;
|
|
77
|
+
}
|
|
78
|
+
/** True when the profile's App ID covers the bundle id (exact or wildcard). */
|
|
79
|
+
export function coversBundleId(profile, bundleId) {
|
|
80
|
+
if (profile.bundleId === "*")
|
|
81
|
+
return true;
|
|
82
|
+
if (profile.isWildcard) {
|
|
83
|
+
return bundleId.startsWith(profile.bundleId.slice(0, -1)); // "com.foo.*" → "com.foo."
|
|
84
|
+
}
|
|
85
|
+
return profile.bundleId === bundleId;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Pick the best usable profile: covers the bundle id + device UDID (a missing
|
|
89
|
+
* device list means enterprise — always covered) and stays valid for at least
|
|
90
|
+
* `minRemainingMs`. Exact App IDs beat wildcards (no bundle-id surprises), then
|
|
91
|
+
* the later expiry wins.
|
|
92
|
+
*/
|
|
93
|
+
export function selectProfile(profiles, criteria) {
|
|
94
|
+
const now = criteria.now ?? Date.now();
|
|
95
|
+
const minRemainingMs = criteria.minRemainingMs ?? DEFAULT_MIN_REMAINING_MS;
|
|
96
|
+
const usable = profiles.filter((p) => (!criteria.teamId || p.teamId === criteria.teamId) &&
|
|
97
|
+
coversBundleId(p, criteria.bundleId) &&
|
|
98
|
+
(p.provisionedDevices === null || p.provisionedDevices.includes(criteria.udid)) &&
|
|
99
|
+
p.expirationDate.getTime() - now > minRemainingMs);
|
|
100
|
+
usable.sort((a, b) => {
|
|
101
|
+
if (a.isWildcard !== b.isWildcard)
|
|
102
|
+
return a.isWildcard ? 1 : -1;
|
|
103
|
+
return b.expirationDate.getTime() - a.expirationDate.getTime();
|
|
104
|
+
});
|
|
105
|
+
return usable[0] ?? null;
|
|
106
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { coversBundleId, listProvisioningProfiles, parseProfileXml, profileDirs, selectProfile, } from "./ios-profiles.js";
|
|
6
|
+
function profileXml(opts) {
|
|
7
|
+
const devices = opts.devices === null
|
|
8
|
+
? ""
|
|
9
|
+
: `<key>ProvisionedDevices</key><array>${(opts.devices ?? ["UDID-1"])
|
|
10
|
+
.map((d) => `<string>${d}</string>`)
|
|
11
|
+
.join("")}</array>`;
|
|
12
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
13
|
+
<plist version="1.0"><dict>
|
|
14
|
+
<key>Name</key><string>${opts.name ?? "Test Profile"}</string>
|
|
15
|
+
<key>TeamIdentifier</key><array><string>${opts.teamId ?? "TESTTEAM99"}</string></array>
|
|
16
|
+
<key>Entitlements</key><dict>
|
|
17
|
+
<key>application-identifier</key><string>${opts.appId ?? "TESTTEAM99.com.example.app"}</string>
|
|
18
|
+
</dict>
|
|
19
|
+
${devices}
|
|
20
|
+
<key>ExpirationDate</key><date>${opts.expiration ?? "2036-01-01T00:00:00Z"}</date>
|
|
21
|
+
</dict></plist>`;
|
|
22
|
+
}
|
|
23
|
+
describe("parseProfileXml", () => {
|
|
24
|
+
it("parses an explicit app-id development profile", () => {
|
|
25
|
+
const profile = parseProfileXml(profileXml({}), "/p/a.mobileprovision");
|
|
26
|
+
expect(profile).toMatchObject({
|
|
27
|
+
path: "/p/a.mobileprovision",
|
|
28
|
+
name: "Test Profile",
|
|
29
|
+
teamId: "TESTTEAM99",
|
|
30
|
+
bundleId: "com.example.app",
|
|
31
|
+
isWildcard: false,
|
|
32
|
+
provisionedDevices: ["UDID-1"],
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it("classifies wildcard profiles", () => {
|
|
36
|
+
const star = parseProfileXml(profileXml({ appId: "TESTTEAM99.*" }), "/p");
|
|
37
|
+
expect(star).toMatchObject({ bundleId: "*", isWildcard: true });
|
|
38
|
+
const prefix = parseProfileXml(profileXml({ appId: "TESTTEAM99.com.foo.*" }), "/p");
|
|
39
|
+
expect(prefix).toMatchObject({ bundleId: "com.foo.*", isWildcard: true });
|
|
40
|
+
});
|
|
41
|
+
it("returns null provisionedDevices when the profile embeds no device list", () => {
|
|
42
|
+
const profile = parseProfileXml(profileXml({ devices: null }), "/p");
|
|
43
|
+
expect(profile?.provisionedDevices).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
it("returns null when required fields are missing", () => {
|
|
46
|
+
expect(parseProfileXml("<plist/>", "/p")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe("listProvisioningProfiles", () => {
|
|
50
|
+
let dir;
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
dir = mkdtempSync(join(tmpdir(), "aximo-profiles-"));
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
it("decodes every .mobileprovision across the given dirs and skips failures", async () => {
|
|
58
|
+
const dirA = join(dir, "a");
|
|
59
|
+
const dirB = join(dir, "b");
|
|
60
|
+
mkdirSync(dirA);
|
|
61
|
+
mkdirSync(dirB);
|
|
62
|
+
writeFileSync(join(dirA, "one.mobileprovision"), "raw");
|
|
63
|
+
writeFileSync(join(dirA, "ignored.txt"), "raw");
|
|
64
|
+
writeFileSync(join(dirB, "two.mobileprovision"), "raw");
|
|
65
|
+
writeFileSync(join(dirB, "broken.mobileprovision"), "raw");
|
|
66
|
+
const exec = async (_file, args) => {
|
|
67
|
+
const path = args[3] ?? "";
|
|
68
|
+
if (path.endsWith("broken.mobileprovision"))
|
|
69
|
+
throw new Error("decode failed");
|
|
70
|
+
const name = path.endsWith("one.mobileprovision") ? "One" : "Two";
|
|
71
|
+
return { stdout: Buffer.from(profileXml({ name })), stderr: "" };
|
|
72
|
+
};
|
|
73
|
+
const profiles = await listProvisioningProfiles({ exec, dirs: [dirA, dirB, "/nonexistent"] });
|
|
74
|
+
expect(profiles.map((p) => p.name).sort()).toEqual(["One", "Two"]);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("coversBundleId", () => {
|
|
78
|
+
const base = {
|
|
79
|
+
path: "/p",
|
|
80
|
+
name: "n",
|
|
81
|
+
teamId: "T",
|
|
82
|
+
bundleId: "com.example.app",
|
|
83
|
+
isWildcard: false,
|
|
84
|
+
expirationDate: new Date("2036-01-01"),
|
|
85
|
+
provisionedDevices: null,
|
|
86
|
+
};
|
|
87
|
+
it("matches exact ids and rejects others", () => {
|
|
88
|
+
expect(coversBundleId(base, "com.example.app")).toBe(true);
|
|
89
|
+
expect(coversBundleId(base, "com.example.other")).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
it("matches '*' and prefix wildcards", () => {
|
|
92
|
+
expect(coversBundleId({ ...base, bundleId: "*", isWildcard: true }, "anything.at.all")).toBe(true);
|
|
93
|
+
const prefix = { ...base, bundleId: "com.foo.*", isWildcard: true };
|
|
94
|
+
expect(coversBundleId(prefix, "com.foo.bar")).toBe(true);
|
|
95
|
+
expect(coversBundleId(prefix, "com.other.bar")).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe("selectProfile", () => {
|
|
99
|
+
const NOW = new Date("2026-07-13T00:00:00Z").getTime();
|
|
100
|
+
const make = (over) => ({
|
|
101
|
+
path: "/p",
|
|
102
|
+
name: "n",
|
|
103
|
+
teamId: "TESTTEAM99",
|
|
104
|
+
bundleId: "com.example.app",
|
|
105
|
+
isWildcard: false,
|
|
106
|
+
expirationDate: new Date("2026-07-20T00:00:00Z"),
|
|
107
|
+
provisionedDevices: ["UDID-1"],
|
|
108
|
+
...over,
|
|
109
|
+
});
|
|
110
|
+
it("requires bundle-id + udid coverage and validity margin", () => {
|
|
111
|
+
const expired = make({ expirationDate: new Date("2026-07-13T06:00:00Z") }); // <1 day left
|
|
112
|
+
const wrongDevice = make({ provisionedDevices: ["UDID-2"] });
|
|
113
|
+
const good = make({});
|
|
114
|
+
expect(selectProfile([expired, wrongDevice, good], {
|
|
115
|
+
bundleId: "com.example.app",
|
|
116
|
+
udid: "UDID-1",
|
|
117
|
+
now: NOW,
|
|
118
|
+
})).toBe(good);
|
|
119
|
+
});
|
|
120
|
+
it("treats a missing device list (enterprise) as covering any udid", () => {
|
|
121
|
+
const enterprise = make({ provisionedDevices: null });
|
|
122
|
+
expect(selectProfile([enterprise], { bundleId: "com.example.app", udid: "ANY", now: NOW })).toBe(enterprise);
|
|
123
|
+
});
|
|
124
|
+
it("prefers exact app-ids over wildcards, then later expiry", () => {
|
|
125
|
+
const wildcard = make({
|
|
126
|
+
bundleId: "*",
|
|
127
|
+
isWildcard: true,
|
|
128
|
+
expirationDate: new Date("2036-01-01T00:00:00Z"),
|
|
129
|
+
});
|
|
130
|
+
const exactSoon = make({});
|
|
131
|
+
const exactLater = make({ expirationDate: new Date("2026-08-01T00:00:00Z") });
|
|
132
|
+
expect(selectProfile([wildcard, exactSoon, exactLater], {
|
|
133
|
+
bundleId: "com.example.app",
|
|
134
|
+
udid: "UDID-1",
|
|
135
|
+
now: NOW,
|
|
136
|
+
})).toBe(exactLater);
|
|
137
|
+
});
|
|
138
|
+
it("filters by team id when given and returns null when nothing matches", () => {
|
|
139
|
+
const otherTeam = make({ teamId: "OTHERTEAM0" });
|
|
140
|
+
expect(selectProfile([otherTeam], {
|
|
141
|
+
bundleId: "com.example.app",
|
|
142
|
+
udid: "UDID-1",
|
|
143
|
+
teamId: "TESTTEAM99",
|
|
144
|
+
now: NOW,
|
|
145
|
+
})).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe("profileDirs", () => {
|
|
149
|
+
it("returns the Xcode 16+ UserData dir before the legacy MobileDevice dir", () => {
|
|
150
|
+
const dirs = profileDirs("/Users/me");
|
|
151
|
+
expect(dirs[0]).toContain("Library/Developer/Xcode/UserData/Provisioning Profiles");
|
|
152
|
+
expect(dirs[1]).toContain("Library/MobileDevice/Provisioning Profiles");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ExecFn } from "./exec.js";
|
|
2
|
+
import { type ProvisioningProfile } from "./ios-profiles.js";
|
|
3
|
+
/**
|
|
4
|
+
* Mechanics of inspecting and re-signing iOS .ipa files.
|
|
5
|
+
*
|
|
6
|
+
* An IPA only installs on a device its embedded profile covers, so BYOD needs
|
|
7
|
+
* the same move AWS Device Farm makes server-side: strip the original signature
|
|
8
|
+
* and re-sign with a local identity + profile. applesign (node-applesign) does
|
|
9
|
+
* the heavy lifting — nested-framework signing order, profile embedding, and
|
|
10
|
+
* bundle-id remapping — and was verified on-device (see
|
|
11
|
+
* docs/ios-resign-e2e-verification.md).
|
|
12
|
+
*/
|
|
13
|
+
export interface IpaInfo {
|
|
14
|
+
/** CFBundleIdentifier of the top-level app. */
|
|
15
|
+
bundleId: string;
|
|
16
|
+
/** Embedded provisioning profile, when present (missing on store builds). */
|
|
17
|
+
profile: ProvisioningProfile | null;
|
|
18
|
+
/** Main-binary FairPlay state: true = encrypted (cannot be re-signed). */
|
|
19
|
+
encrypted: boolean | null;
|
|
20
|
+
}
|
|
21
|
+
/** Inspect an IPA's identity/signature surface without a full extraction. */
|
|
22
|
+
export declare function inspectIpa(ipaPath: string, exec?: ExecFn): Promise<IpaInfo>;
|
|
23
|
+
/** True when the IPA (per its embedded profile) can install on `udid` as-is. */
|
|
24
|
+
export declare function isInstallableOn(info: IpaInfo, udid: string, now?: number): boolean;
|
|
25
|
+
export interface ResignIpaOptions {
|
|
26
|
+
ipaPath: string;
|
|
27
|
+
outPath: string;
|
|
28
|
+
/** Signing identity SHA-1 hash (from ios-identity). */
|
|
29
|
+
identityHash: string;
|
|
30
|
+
/** Provisioning profile to embed. */
|
|
31
|
+
profilePath: string;
|
|
32
|
+
/** Remap CFBundleIdentifier (required with explicit-App-ID profiles). */
|
|
33
|
+
bundleId?: string;
|
|
34
|
+
exec?: ExecFn;
|
|
35
|
+
}
|
|
36
|
+
/** Re-sign an IPA via the bundled applesign (never PATH-dependent). */
|
|
37
|
+
export declare function resignIpa(options: ResignIpaOptions): Promise<void>;
|