@limrun/ui 0.13.8 → 0.14.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-store-relay/index.cjs +3 -3
- package/dist/app-store-relay/index.d.ts +3 -5
- package/dist/app-store-relay/index.js +56 -58
- package/dist/app-store-relay/react.cjs +1 -1
- package/dist/app-store-relay/react.d.ts +2 -2
- package/dist/app-store-relay/react.js +35 -34
- package/dist/client-BtC4n_qf.js +1 -0
- package/dist/client-CBX-hLnL.mjs +189 -0
- package/dist/core/device-install/apple/client.d.ts +5 -4
- package/dist/core/device-install/apple/relay.d.ts +24 -13
- package/dist/core/device-install/operations/limbuild-client.d.ts +2 -1
- package/dist/core/device-install/operations/operations.d.ts +19 -4
- package/dist/core/device-install/operations/relay-client.d.ts +2 -1
- package/dist/device-build/index.cjs +1 -1
- package/dist/device-build/index.js +1 -1
- package/dist/device-build/react.cjs +1 -1
- package/dist/device-build/react.d.ts +1 -0
- package/dist/device-build/react.js +34 -33
- package/dist/device-install/index.cjs +1 -1
- package/dist/device-install/index.d.ts +1 -1
- package/dist/device-install/index.js +121 -119
- package/dist/device-install/react.cjs +1 -1
- package/dist/device-install/react.d.ts +4 -3
- package/dist/device-install/react.js +58 -54
- package/dist/limbuild-client-BagK_FWE.mjs +83 -0
- package/dist/limbuild-client-CVDwq54z.js +1 -0
- package/package.json +1 -1
- package/src/app-store-relay/index.test.ts +19 -29
- package/src/app-store-relay/index.ts +6 -9
- package/src/app-store-relay/react.ts +5 -4
- package/src/core/device-install/apple/client.ts +22 -33
- package/src/core/device-install/apple/relay.ts +108 -148
- package/src/core/device-install/operations/limbuild-client.ts +3 -0
- package/src/core/device-install/operations/operations.ts +21 -9
- package/src/core/device-install/operations/relay-client.ts +3 -2
- package/src/device-build/react.ts +2 -0
- package/src/device-install/README.md +5 -5
- package/src/device-install/demo/demo.tsx +26 -28
- package/src/device-install/index.ts +1 -0
- package/src/device-install/react.ts +12 -6
- package/dist/client-ASLtHFLh.js +0 -1
- package/dist/client-CGdh8yoJ.mjs +0 -234
- package/dist/limbuild-client-CgEncrgx.js +0 -1
- package/dist/limbuild-client-D7U7w342.mjs +0 -81
|
@@ -15,178 +15,146 @@ export type AppleProvisioningRequest = {
|
|
|
15
15
|
payload?: unknown;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
type AppleRelayWebSocketMessage<T = unknown> = {
|
|
19
|
+
id: string;
|
|
20
|
+
ok: boolean;
|
|
21
|
+
response?: AppleRelayResponse<T>;
|
|
22
|
+
error?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class AppleRelayWebSocketClient {
|
|
26
|
+
private socket?: WebSocket;
|
|
27
|
+
private nextId = 1;
|
|
28
|
+
private readonly pending = new Map<
|
|
29
|
+
string,
|
|
30
|
+
{
|
|
31
|
+
resolve: (response: AppleRelayResponse) => void;
|
|
32
|
+
reject: (error: Error) => void;
|
|
33
|
+
}
|
|
34
|
+
>();
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly apiUrl: string,
|
|
38
|
+
private readonly token?: string,
|
|
39
|
+
private readonly organizationId?: string,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
async connect() {
|
|
43
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) return;
|
|
44
|
+
const socket = new WebSocket(appleRelayWebSocketURL(this.apiUrl, this.token, this.organizationId));
|
|
45
|
+
this.socket = socket;
|
|
46
|
+
socket.onmessage = (event) => this.handleMessage(event);
|
|
47
|
+
socket.onclose = () => this.rejectPending(new Error('Apple relay WebSocket closed'));
|
|
48
|
+
socket.onerror = () => this.rejectPending(new Error('Apple relay WebSocket failed'));
|
|
49
|
+
await new Promise<void>((resolve, reject) => {
|
|
50
|
+
socket.onopen = () => resolve();
|
|
51
|
+
socket.onerror = () => reject(new Error('Apple relay WebSocket connection failed'));
|
|
52
|
+
});
|
|
25
53
|
}
|
|
26
|
-
return (await response.json()) as { appleSessionId: string };
|
|
27
|
-
}
|
|
28
54
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
request<T = unknown>(type: string, payload?: unknown) {
|
|
56
|
+
const socket = this.socket;
|
|
57
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
58
|
+
throw new Error('Apple relay WebSocket is not connected.');
|
|
59
|
+
}
|
|
60
|
+
const id = String(this.nextId++);
|
|
61
|
+
const response = new Promise<AppleRelayResponse<T>>((resolve, reject) => {
|
|
62
|
+
this.pending.set(id, {
|
|
63
|
+
resolve: (value) => resolve(normalizeAppleProxyResponse<T>(value as AppleRelayResponse<T>)),
|
|
64
|
+
reject,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
socket.send(JSON.stringify({ id, type, ...(payload === undefined ? {} : { payload }) }));
|
|
68
|
+
return response;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
close() {
|
|
72
|
+
this.rejectPending(new Error('Apple relay WebSocket closed'));
|
|
73
|
+
this.socket?.close();
|
|
74
|
+
this.socket = undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private handleMessage(event: MessageEvent) {
|
|
78
|
+
const message = JSON.parse(String(event.data)) as AppleRelayWebSocketMessage;
|
|
79
|
+
const waiter = this.pending.get(message.id);
|
|
80
|
+
if (!waiter) return;
|
|
81
|
+
this.pending.delete(message.id);
|
|
82
|
+
if (!message.ok) {
|
|
83
|
+
waiter.reject(new Error(message.error || 'Apple relay request failed'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!message.response) {
|
|
87
|
+
waiter.reject(new Error('Apple relay response was empty.'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
waiter.resolve(message.response);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private rejectPending(error: Error) {
|
|
94
|
+
for (const waiter of this.pending.values()) {
|
|
95
|
+
waiter.reject(error);
|
|
96
|
+
}
|
|
97
|
+
this.pending.clear();
|
|
41
98
|
}
|
|
42
99
|
}
|
|
43
100
|
|
|
101
|
+
export async function openAppleRelayWebSocket(apiUrl: string, token?: string, organizationId?: string) {
|
|
102
|
+
const client = new AppleRelayWebSocketClient(apiUrl, token, organizationId);
|
|
103
|
+
await client.connect();
|
|
104
|
+
return client;
|
|
105
|
+
}
|
|
106
|
+
|
|
44
107
|
export async function proxySrpInit(
|
|
45
|
-
|
|
46
|
-
appleSessionId: string,
|
|
108
|
+
relay: AppleRelayWebSocketClient,
|
|
47
109
|
payload: AppleSRPInitRequest,
|
|
48
|
-
token?: string,
|
|
49
110
|
) {
|
|
50
|
-
return
|
|
51
|
-
limbuildApiUrl,
|
|
52
|
-
'/apple/auth/srp/init',
|
|
53
|
-
appleSessionId,
|
|
54
|
-
payload,
|
|
55
|
-
token,
|
|
56
|
-
);
|
|
111
|
+
return relay.request<AppleSRPInitResponse>('srpInit', payload);
|
|
57
112
|
}
|
|
58
113
|
|
|
59
114
|
export async function proxySrpComplete(
|
|
60
|
-
|
|
61
|
-
appleSessionId: string,
|
|
115
|
+
relay: AppleRelayWebSocketClient,
|
|
62
116
|
payload: AppleSRPCompleteProof & {
|
|
63
117
|
rememberMe: boolean;
|
|
64
118
|
trustTokens: string[];
|
|
65
119
|
},
|
|
66
|
-
token?: string,
|
|
67
120
|
) {
|
|
68
|
-
return
|
|
121
|
+
return relay.request('srpComplete', payload);
|
|
69
122
|
}
|
|
70
123
|
|
|
71
|
-
export async function triggerTrustedDeviceTwoFactor(
|
|
72
|
-
|
|
73
|
-
appleSessionId: string,
|
|
74
|
-
token?: string,
|
|
75
|
-
) {
|
|
76
|
-
const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/2fa/trigger', token), {
|
|
77
|
-
method: 'POST',
|
|
78
|
-
headers: jsonHeaders(token),
|
|
79
|
-
body: JSON.stringify({ appleSessionId }),
|
|
80
|
-
});
|
|
81
|
-
if (!response.ok) {
|
|
82
|
-
throw new Error(`Apple 2FA trigger failed: HTTP ${response.status} ${await response.text()}`);
|
|
83
|
-
}
|
|
84
|
-
return (await response.json()) as AppleRelayResponse;
|
|
124
|
+
export async function triggerTrustedDeviceTwoFactor(relay: AppleRelayWebSocketClient) {
|
|
125
|
+
return relay.request('triggerTrustedDevice2FA');
|
|
85
126
|
}
|
|
86
127
|
|
|
87
128
|
export async function triggerPhoneTwoFactor(
|
|
88
|
-
|
|
89
|
-
appleSessionId: string,
|
|
129
|
+
relay: AppleRelayWebSocketClient,
|
|
90
130
|
phoneNumberId: number,
|
|
91
131
|
mode = 'sms',
|
|
92
|
-
token?: string,
|
|
93
132
|
) {
|
|
94
|
-
|
|
95
|
-
method: 'POST',
|
|
96
|
-
headers: jsonHeaders(token),
|
|
97
|
-
body: JSON.stringify({ appleSessionId, phoneNumberId, mode }),
|
|
98
|
-
});
|
|
99
|
-
if (!response.ok) {
|
|
100
|
-
throw new Error(`Apple phone 2FA trigger failed: HTTP ${response.status} ${await response.text()}`);
|
|
101
|
-
}
|
|
102
|
-
return (await response.json()) as AppleRelayResponse;
|
|
133
|
+
return relay.request('triggerPhone2FA', { phoneNumberId, mode });
|
|
103
134
|
}
|
|
104
135
|
|
|
105
|
-
export async function proxyTwoFactorCode(
|
|
106
|
-
|
|
107
|
-
appleSessionId: string,
|
|
108
|
-
code: string,
|
|
109
|
-
token?: string,
|
|
110
|
-
) {
|
|
111
|
-
const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/2fa', token), {
|
|
112
|
-
method: 'POST',
|
|
113
|
-
headers: jsonHeaders(token),
|
|
114
|
-
body: JSON.stringify({ appleSessionId, code }),
|
|
115
|
-
});
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
throw new Error(`Apple 2FA proxy failed: HTTP ${response.status} ${await response.text()}`);
|
|
118
|
-
}
|
|
119
|
-
return (await response.json()) as AppleRelayResponse;
|
|
136
|
+
export async function proxyTwoFactorCode(relay: AppleRelayWebSocketClient, code: string) {
|
|
137
|
+
return relay.request('submitTrustedDevice2FA', { code });
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
export async function proxyPhoneTwoFactorCode(
|
|
123
|
-
|
|
124
|
-
appleSessionId: string,
|
|
141
|
+
relay: AppleRelayWebSocketClient,
|
|
125
142
|
phoneNumberId: number,
|
|
126
143
|
code: string,
|
|
127
144
|
mode = 'sms',
|
|
128
|
-
token?: string,
|
|
129
145
|
) {
|
|
130
|
-
|
|
131
|
-
method: 'POST',
|
|
132
|
-
headers: jsonHeaders(token),
|
|
133
|
-
body: JSON.stringify({ appleSessionId, phoneNumberId, mode, code }),
|
|
134
|
-
});
|
|
135
|
-
if (!response.ok) {
|
|
136
|
-
throw new Error(`Apple phone 2FA proxy failed: HTTP ${response.status} ${await response.text()}`);
|
|
137
|
-
}
|
|
138
|
-
return (await response.json()) as AppleRelayResponse;
|
|
146
|
+
return relay.request('submitPhone2FA', { phoneNumberId, mode, code });
|
|
139
147
|
}
|
|
140
148
|
|
|
141
|
-
export async function fetchAppleAccountSession(
|
|
142
|
-
|
|
143
|
-
appleSessionId: string,
|
|
144
|
-
token?: string,
|
|
145
|
-
) {
|
|
146
|
-
const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/finalize', token), {
|
|
147
|
-
method: 'POST',
|
|
148
|
-
headers: jsonHeaders(token),
|
|
149
|
-
body: JSON.stringify({ appleSessionId }),
|
|
150
|
-
});
|
|
151
|
-
if (!response.ok) {
|
|
152
|
-
throw new Error(`Apple session finalization failed: HTTP ${response.status} ${await response.text()}`);
|
|
153
|
-
}
|
|
154
|
-
return (await response.json()) as AppleRelayResponse;
|
|
149
|
+
export async function fetchAppleAccountSession(relay: AppleRelayWebSocketClient) {
|
|
150
|
+
return relay.request('finalize');
|
|
155
151
|
}
|
|
156
152
|
|
|
157
153
|
export async function proxyProvisioningRequest<T = unknown>(
|
|
158
|
-
|
|
159
|
-
appleSessionId: string,
|
|
154
|
+
relay: AppleRelayWebSocketClient,
|
|
160
155
|
request: AppleProvisioningRequest,
|
|
161
|
-
token?: string,
|
|
162
|
-
) {
|
|
163
|
-
const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/provisioning', token), {
|
|
164
|
-
method: 'POST',
|
|
165
|
-
headers: jsonHeaders(token),
|
|
166
|
-
body: JSON.stringify({ appleSessionId, ...request }),
|
|
167
|
-
});
|
|
168
|
-
if (!response.ok) {
|
|
169
|
-
throw new Error(`Apple provisioning proxy failed: HTTP ${response.status} ${await response.text()}`);
|
|
170
|
-
}
|
|
171
|
-
return normalizeAppleProxyResponse<T>((await response.json()) as AppleRelayResponse<T>);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function postAppleProxy<T>(
|
|
175
|
-
limbuildApiUrl: string,
|
|
176
|
-
path: string,
|
|
177
|
-
appleSessionId: string,
|
|
178
|
-
payload: unknown,
|
|
179
|
-
token?: string,
|
|
180
156
|
) {
|
|
181
|
-
|
|
182
|
-
method: 'POST',
|
|
183
|
-
headers: jsonHeaders(token),
|
|
184
|
-
body: JSON.stringify({ appleSessionId, payload }),
|
|
185
|
-
});
|
|
186
|
-
if (!response.ok) {
|
|
187
|
-
throw new Error(`Apple proxy ${path} failed: HTTP ${response.status} ${await response.text()}`);
|
|
188
|
-
}
|
|
189
|
-
return normalizeAppleProxyResponse<T>((await response.json()) as AppleRelayResponse<T>);
|
|
157
|
+
return relay.request<T>('provisioning', request);
|
|
190
158
|
}
|
|
191
159
|
|
|
192
160
|
function normalizeAppleProxyResponse<T>(response: AppleRelayResponse<T>) {
|
|
@@ -203,23 +171,15 @@ function normalizeAppleProxyResponse<T>(response: AppleRelayResponse<T>) {
|
|
|
203
171
|
}
|
|
204
172
|
}
|
|
205
173
|
|
|
206
|
-
function
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
174
|
+
function appleRelayWebSocketURL(apiUrl: string, token?: string, organizationId?: string) {
|
|
175
|
+
const url = new URL(apiUrl);
|
|
176
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
177
|
+
url.pathname = `${url.pathname.replace(/\/$/, '')}/ios/appstoreconnect/ws`;
|
|
210
178
|
if (token) {
|
|
211
179
|
url.searchParams.set('token', token);
|
|
212
180
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
'Content-Type': 'application/json',
|
|
219
|
-
...authHeaders(token),
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function authHeaders(token?: string): Record<string, string> {
|
|
224
|
-
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
181
|
+
if (organizationId) {
|
|
182
|
+
url.searchParams.set('organization', organizationId);
|
|
183
|
+
}
|
|
184
|
+
return url.toString();
|
|
225
185
|
}
|
|
@@ -14,6 +14,7 @@ export type StartSignedDeviceBuildOptions = {
|
|
|
14
14
|
certificateP12Base64: string;
|
|
15
15
|
certificatePassword?: string;
|
|
16
16
|
provisioningProfileBase64: string;
|
|
17
|
+
signedUploadUrl?: string;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export type BuildLogEventsOptions = {
|
|
@@ -61,6 +62,7 @@ export async function startSignedDeviceBuild({
|
|
|
61
62
|
certificateP12Base64,
|
|
62
63
|
certificatePassword,
|
|
63
64
|
provisioningProfileBase64,
|
|
65
|
+
signedUploadUrl,
|
|
64
66
|
}: StartSignedDeviceBuildOptions) {
|
|
65
67
|
const url = new URL(`${limbuildApiUrl}/exec`);
|
|
66
68
|
if (token) {
|
|
@@ -75,6 +77,7 @@ export async function startSignedDeviceBuild({
|
|
|
75
77
|
body: JSON.stringify({
|
|
76
78
|
command: 'xcodebuild',
|
|
77
79
|
xcodebuild: { sdk: 'iphoneos' },
|
|
80
|
+
...(signedUploadUrl ? { signedUploadUrl } : {}),
|
|
78
81
|
signing: {
|
|
79
82
|
certificateP12Base64,
|
|
80
83
|
...(certificatePassword ? { certificatePassword } : {}),
|
|
@@ -18,14 +18,21 @@ export type RequestUSBAccessOptions = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export type StartPairingRelayOptions = {
|
|
21
|
-
|
|
21
|
+
registryApiUrl: string;
|
|
22
22
|
token?: string;
|
|
23
|
+
organizationId?: string;
|
|
23
24
|
log: DeviceInstallLog;
|
|
24
25
|
target: DeviceRelayTarget;
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
export type InstallSource =
|
|
29
|
+
| { assetId: string; assetName?: never; downloadUrl?: never }
|
|
30
|
+
| { assetId?: never; assetName: string; downloadUrl?: never }
|
|
31
|
+
| { assetId?: never; assetName?: never; downloadUrl: string };
|
|
32
|
+
|
|
27
33
|
export type StartInstallRelayOptions = StartPairingRelayOptions & {
|
|
28
34
|
pairRecord: PairRecordPayload;
|
|
35
|
+
installSource: InstallSource;
|
|
29
36
|
};
|
|
30
37
|
|
|
31
38
|
export async function requestUSBAccess({ log }: RequestUSBAccessOptions) {
|
|
@@ -39,8 +46,8 @@ export async function requestUSBAccess({ log }: RequestUSBAccessOptions) {
|
|
|
39
46
|
return target;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
export async function startPairingRelay({
|
|
43
|
-
const deviceRelayUrl = deviceRelayWebSocketUrl(
|
|
49
|
+
export async function startPairingRelay({ registryApiUrl, token, organizationId, log, target }: StartPairingRelayOptions) {
|
|
50
|
+
const deviceRelayUrl = deviceRelayWebSocketUrl(registryApiUrl, token, organizationId);
|
|
44
51
|
let relay: RelayClient | undefined;
|
|
45
52
|
try {
|
|
46
53
|
relay = await connectRelay(deviceRelayUrl, target, log);
|
|
@@ -53,17 +60,19 @@ export async function startPairingRelay({ limbuildApiUrl, token, log, target }:
|
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
export async function startInstallRelay({
|
|
56
|
-
|
|
63
|
+
registryApiUrl,
|
|
57
64
|
token,
|
|
65
|
+
organizationId,
|
|
58
66
|
log,
|
|
59
67
|
target,
|
|
60
68
|
pairRecord,
|
|
69
|
+
installSource,
|
|
61
70
|
}: StartInstallRelayOptions) {
|
|
62
|
-
const deviceRelayUrl = deviceRelayWebSocketUrl(
|
|
71
|
+
const deviceRelayUrl = deviceRelayWebSocketUrl(registryApiUrl, token, organizationId);
|
|
63
72
|
let relay: RelayClient | undefined;
|
|
64
73
|
try {
|
|
65
74
|
relay = await connectRelay(deviceRelayUrl, target, log);
|
|
66
|
-
await relay.startInstall(pairRecord);
|
|
75
|
+
await relay.startInstall(pairRecord, installSource);
|
|
67
76
|
return relay;
|
|
68
77
|
} catch (error) {
|
|
69
78
|
relay?.close();
|
|
@@ -182,13 +191,16 @@ function orderedUsbmuxCandidates(device: USBDevice) {
|
|
|
182
191
|
];
|
|
183
192
|
}
|
|
184
193
|
|
|
185
|
-
export function deviceRelayWebSocketUrl(
|
|
186
|
-
const url = new URL(
|
|
194
|
+
export function deviceRelayWebSocketUrl(registryApiUrl: string, token?: string, organizationId?: string) {
|
|
195
|
+
const url = new URL(registryApiUrl);
|
|
187
196
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
188
|
-
url.pathname = `${url.pathname.replace(/\/$/, '')}/device/ws`;
|
|
197
|
+
url.pathname = `${url.pathname.replace(/\/$/, '')}/ios/device/ws`;
|
|
189
198
|
if (token) {
|
|
190
199
|
url.searchParams.set('token', token);
|
|
191
200
|
}
|
|
201
|
+
if (organizationId) {
|
|
202
|
+
url.searchParams.set('organization', organizationId);
|
|
203
|
+
}
|
|
192
204
|
return url.toString();
|
|
193
205
|
}
|
|
194
206
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DeviceHello, DeviceInstallLog, PairRecordPayload } from '../types';
|
|
2
2
|
import { decodeFrame, decodeJson, encodeFrame, encodeJson, RelayMessageType } from './relay-protocol';
|
|
3
|
+
import type { InstallSource } from './operations';
|
|
3
4
|
import {
|
|
4
5
|
openStream,
|
|
5
6
|
receiveStreamData,
|
|
@@ -83,12 +84,12 @@ export class RelayClient {
|
|
|
83
84
|
return recordPromise;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
async startInstall(pairRecord: PairRecordPayload) {
|
|
87
|
+
async startInstall(pairRecord: PairRecordPayload, installSource: InstallSource) {
|
|
87
88
|
await this.send({
|
|
88
89
|
type: RelayMessageType.StartInstall,
|
|
89
90
|
requestId: 0,
|
|
90
91
|
streamId: 0,
|
|
91
|
-
payload: encodeJson(pairRecord),
|
|
92
|
+
payload: encodeJson({ ...pairRecord, ...installSource }),
|
|
92
93
|
});
|
|
93
94
|
this.log('Installation requested');
|
|
94
95
|
}
|
|
@@ -18,6 +18,7 @@ export type UseDeviceBuildOptions = {
|
|
|
18
18
|
|
|
19
19
|
export type StartDeviceBuildInput = {
|
|
20
20
|
signingAssets?: StoredSigningAssets;
|
|
21
|
+
signedUploadUrl?: string;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export type UseDeviceBuildResult = {
|
|
@@ -82,6 +83,7 @@ export function useDeviceBuild({
|
|
|
82
83
|
certificateP12Base64: activeSigningAssets.certificateP12Base64,
|
|
83
84
|
certificatePassword: activeSigningAssets.certificatePassword,
|
|
84
85
|
provisioningProfileBase64: activeSigningAssets.provisioningProfileBase64,
|
|
86
|
+
signedUploadUrl: input.signedUploadUrl,
|
|
85
87
|
});
|
|
86
88
|
if (!result.execId) {
|
|
87
89
|
throw new Error('Build request did not return an exec ID.');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Build a signed iOS app on Limrun and install it onto a physical iPhone that's
|
|
4
4
|
plugged into the user's own computer — straight from the browser, no Mac and no
|
|
5
5
|
Xcode on their machine. The browser talks to the iPhone over WebUSB; Limrun runs
|
|
6
|
-
the native pairing and install
|
|
6
|
+
the native pairing and install on its side and relays the
|
|
7
7
|
USB traffic over a WebSocket.
|
|
8
8
|
|
|
9
9
|
`@limrun/ui` ships the browser primitives and React hooks; your app owns the UI.
|
|
@@ -83,7 +83,7 @@ user only taps **Trust** once per device.
|
|
|
83
83
|
```tsx
|
|
84
84
|
import { useDeviceInstallRelay } from '@limrun/ui/device-install/react';
|
|
85
85
|
|
|
86
|
-
const install = useDeviceInstallRelay({ apiUrl, token, log: (m, d) => console.log(m, d) });
|
|
86
|
+
const install = useDeviceInstallRelay({ apiUrl, token, organizationId, log: (m, d) => console.log(m, d) });
|
|
87
87
|
|
|
88
88
|
// 1. Pick the iPhone (shows Chrome's WebUSB chooser).
|
|
89
89
|
await install.requestUSBAccess();
|
|
@@ -156,13 +156,13 @@ import {
|
|
|
156
156
|
putAppleGeneratedSigningAssets,
|
|
157
157
|
} from '@limrun/ui/device-build';
|
|
158
158
|
|
|
159
|
-
const appleLogin = useAppleIDLogin({
|
|
159
|
+
const appleLogin = useAppleIDLogin({ registryApiUrl: apiUrl, token, organizationId });
|
|
160
160
|
const session = await appleLogin.startLogin({ accountName, password });
|
|
161
161
|
if (session?.requiresTwoFactor) await appleLogin.submitTwoFactorCode(code);
|
|
162
162
|
|
|
163
163
|
// Resolve the team id across all three fields Apple may use.
|
|
164
164
|
const teamId = team.teamId ?? String(team.providerId ?? team.publicProviderId ?? '');
|
|
165
|
-
const base = {
|
|
165
|
+
const base = { relay: appleLogin.session!.relay, teamId };
|
|
166
166
|
|
|
167
167
|
// Register the paired iPhone (no-op if already registered).
|
|
168
168
|
await registerAppleDevice({ ...base, deviceUDID, name: 'My iPhone' });
|
|
@@ -298,7 +298,7 @@ export function InstallToIPhone({
|
|
|
298
298
|
const [log, setLog] = useState<string[]>([]);
|
|
299
299
|
const append = (m: string, d?: string) => setLog((l) => [d ? `${m}: ${d}` : m, ...l]);
|
|
300
300
|
|
|
301
|
-
const install = useDeviceInstallRelay({ apiUrl, token, log: append });
|
|
301
|
+
const install = useDeviceInstallRelay({ apiUrl, token, organizationId, log: append });
|
|
302
302
|
const build = useDeviceBuild({ apiUrl, token, signingAssets });
|
|
303
303
|
|
|
304
304
|
async function prepare() {
|
|
@@ -64,6 +64,7 @@ const emptyAppleResources: AppleResourceState = {
|
|
|
64
64
|
const storageKeys = {
|
|
65
65
|
apiUrl: 'limrun-device-demo-api-url',
|
|
66
66
|
token: 'limrun-device-demo-token',
|
|
67
|
+
ipaDownloadUrl: 'limrun-device-demo-ipa-download-url',
|
|
67
68
|
bundleId: 'limrun-device-demo-bundle-id',
|
|
68
69
|
certificatePassword: 'limrun-device-demo-certificate-password',
|
|
69
70
|
appleAccount: 'limrun-device-demo-apple-account',
|
|
@@ -72,6 +73,7 @@ const storageKeys = {
|
|
|
72
73
|
function App() {
|
|
73
74
|
const [apiUrl, setApiUrl] = useLocalStorage(storageKeys.apiUrl, 'http://127.0.0.1:8080');
|
|
74
75
|
const [token, setToken] = useLocalStorage(storageKeys.token, '');
|
|
76
|
+
const [ipaDownloadUrl, setIPADownloadUrl] = useLocalStorage(storageKeys.ipaDownloadUrl, '');
|
|
75
77
|
const [bundleId, setBundleId] = useLocalStorage(storageKeys.bundleId, '');
|
|
76
78
|
const [appleAccount, setAppleAccount] = useLocalStorage(storageKeys.appleAccount, '');
|
|
77
79
|
const [certificatePassword, setCertificatePassword] = useLocalStorage(storageKeys.certificatePassword, '');
|
|
@@ -131,7 +133,7 @@ function App() {
|
|
|
131
133
|
});
|
|
132
134
|
|
|
133
135
|
const appleLogin = useAppleIDLogin({
|
|
134
|
-
|
|
136
|
+
registryApiUrl: apiUrl.trim(),
|
|
135
137
|
token: token.trim() || undefined,
|
|
136
138
|
});
|
|
137
139
|
|
|
@@ -148,10 +150,10 @@ function App() {
|
|
|
148
150
|
const selectedProfile = resources.profiles.find(
|
|
149
151
|
(profile) => stringField(profile, 'provisioningProfileId') === selectedProfileId,
|
|
150
152
|
);
|
|
151
|
-
const canUseApple = !!apiUrl.trim() && !!appleLogin.session?.
|
|
153
|
+
const canUseApple = !!apiUrl.trim() && !!appleLogin.session?.relay && !!developerTeamId;
|
|
152
154
|
const canPrepareSigning = !!certificateFile && !!profileFile;
|
|
153
155
|
const canStartBuild = !!signingAssets && build.status !== 'queued' && build.status !== 'running';
|
|
154
|
-
const canInstall = install.canInstall && build.status === 'succeeded';
|
|
156
|
+
const canInstall = install.canInstall && build.status === 'succeeded' && !!ipaDownloadUrl.trim();
|
|
155
157
|
|
|
156
158
|
// Live per-step status used by the overview stepper and the section pills.
|
|
157
159
|
const pairStep: StepView =
|
|
@@ -182,7 +184,7 @@ function App() {
|
|
|
182
184
|
|
|
183
185
|
async function runInstall() {
|
|
184
186
|
setInstallPhase('installing');
|
|
185
|
-
const relay = await install.startInstallation();
|
|
187
|
+
const relay = await install.startInstallation({ downloadUrl: ipaDownloadUrl.trim() });
|
|
186
188
|
if (!relay) setInstallPhase('error');
|
|
187
189
|
}
|
|
188
190
|
|
|
@@ -246,7 +248,7 @@ function App() {
|
|
|
246
248
|
});
|
|
247
249
|
setApplePassword('');
|
|
248
250
|
if (session && !session.requiresTwoFactor) {
|
|
249
|
-
await loadAppleTeams(session.
|
|
251
|
+
await loadAppleTeams(session.relay);
|
|
250
252
|
}
|
|
251
253
|
} catch (error) {
|
|
252
254
|
setPrepareError(errorMessage(error));
|
|
@@ -262,7 +264,7 @@ function App() {
|
|
|
262
264
|
try {
|
|
263
265
|
await appleLogin.submitTwoFactorCode(twoFactorCode);
|
|
264
266
|
setTwoFactorCode('');
|
|
265
|
-
await loadAppleTeams(appleLogin.session.
|
|
267
|
+
await loadAppleTeams(appleLogin.session.relay);
|
|
266
268
|
} catch (error) {
|
|
267
269
|
setPrepareError(errorMessage(error));
|
|
268
270
|
} finally {
|
|
@@ -270,17 +272,13 @@ function App() {
|
|
|
270
272
|
}
|
|
271
273
|
}
|
|
272
274
|
|
|
273
|
-
async function loadAppleTeams(
|
|
274
|
-
if (!apiUrl.trim() || !
|
|
275
|
+
async function loadAppleTeams(relay = appleLogin.session?.relay) {
|
|
276
|
+
if (!apiUrl.trim() || !relay) return;
|
|
275
277
|
setAppleBusy('teams');
|
|
276
278
|
setPrepareError(undefined);
|
|
277
279
|
try {
|
|
278
280
|
await appleLogin.finalize().catch(() => undefined);
|
|
279
|
-
const teams = await listAppleTeams({
|
|
280
|
-
apiUrl: apiUrl.trim(),
|
|
281
|
-
token: token.trim() || undefined,
|
|
282
|
-
appleSessionId,
|
|
283
|
-
});
|
|
281
|
+
const teams = await listAppleTeams({ relay });
|
|
284
282
|
setResources((current) => ({ ...current, teams }));
|
|
285
283
|
const firstTeamId = teams.map(appleTeamSelectionId).find(Boolean) ?? '';
|
|
286
284
|
setSelectedTeamId(firstTeamId);
|
|
@@ -288,7 +286,7 @@ function App() {
|
|
|
288
286
|
teams.find((team) => appleTeamSelectionId(team) === firstTeamId),
|
|
289
287
|
);
|
|
290
288
|
if (firstDeveloperTeamId) {
|
|
291
|
-
await loadAppleResources(firstDeveloperTeamId,
|
|
289
|
+
await loadAppleResources(firstDeveloperTeamId, relay);
|
|
292
290
|
}
|
|
293
291
|
} catch (error) {
|
|
294
292
|
setPrepareError(errorMessage(error));
|
|
@@ -299,13 +297,11 @@ function App() {
|
|
|
299
297
|
|
|
300
298
|
async function loadAppleResources(
|
|
301
299
|
teamId = developerTeamId,
|
|
302
|
-
|
|
300
|
+
relay = appleLogin.session?.relay,
|
|
303
301
|
) {
|
|
304
|
-
if (!apiUrl.trim() || !
|
|
302
|
+
if (!apiUrl.trim() || !relay || !teamId) return;
|
|
305
303
|
const base = {
|
|
306
|
-
|
|
307
|
-
token: token.trim() || undefined,
|
|
308
|
-
appleSessionId,
|
|
304
|
+
relay,
|
|
309
305
|
teamId,
|
|
310
306
|
};
|
|
311
307
|
const [appIds, devices, certificates, profiles] = await Promise.all([
|
|
@@ -336,9 +332,7 @@ function App() {
|
|
|
336
332
|
setPrepareError(undefined);
|
|
337
333
|
try {
|
|
338
334
|
const app = await createAppleBundleID({
|
|
339
|
-
|
|
340
|
-
token: token.trim() || undefined,
|
|
341
|
-
appleSessionId: appleLogin.session.appleSessionId,
|
|
335
|
+
relay: appleLogin.session.relay,
|
|
342
336
|
teamId: developerTeamId,
|
|
343
337
|
bundleId: bundleId.trim(),
|
|
344
338
|
});
|
|
@@ -359,9 +353,7 @@ function App() {
|
|
|
359
353
|
setPrepareError(undefined);
|
|
360
354
|
try {
|
|
361
355
|
await registerAppleDevice({
|
|
362
|
-
|
|
363
|
-
token: token.trim() || undefined,
|
|
364
|
-
appleSessionId: appleLogin.session.appleSessionId,
|
|
356
|
+
relay: appleLogin.session.relay,
|
|
365
357
|
teamId: developerTeamId,
|
|
366
358
|
deviceUDID: selectedUDID,
|
|
367
359
|
name: install.device?.hello.productName ?? 'Limrun iPhone',
|
|
@@ -393,9 +385,7 @@ function App() {
|
|
|
393
385
|
setPrepareError(undefined);
|
|
394
386
|
try {
|
|
395
387
|
const base = {
|
|
396
|
-
|
|
397
|
-
token: token.trim() || undefined,
|
|
398
|
-
appleSessionId: appleLogin.session.appleSessionId,
|
|
388
|
+
relay: appleLogin.session.relay,
|
|
399
389
|
teamId: developerTeamId,
|
|
400
390
|
};
|
|
401
391
|
let certificateId = storedCertificate?.certificateID;
|
|
@@ -599,6 +589,14 @@ function App() {
|
|
|
599
589
|
<p className="hint">
|
|
600
590
|
Run this page on <code>localhost</code> or HTTPS. WebUSB is available in Chromium browsers only.
|
|
601
591
|
</p>
|
|
592
|
+
<label>
|
|
593
|
+
IPA download URL
|
|
594
|
+
<input
|
|
595
|
+
value={ipaDownloadUrl}
|
|
596
|
+
onChange={(event) => setIPADownloadUrl(event.currentTarget.value)}
|
|
597
|
+
placeholder="https://asset-storage.example/app.ipa"
|
|
598
|
+
/>
|
|
599
|
+
</label>
|
|
602
600
|
</section>
|
|
603
601
|
|
|
604
602
|
<section className="card">
|
|
@@ -14,6 +14,7 @@ export {
|
|
|
14
14
|
startInstallRelay as startDeviceInstallRelay,
|
|
15
15
|
startPairingRelay as startDevicePairingRelay,
|
|
16
16
|
type DeviceRelayTarget,
|
|
17
|
+
type InstallSource,
|
|
17
18
|
type StartInstallRelayOptions,
|
|
18
19
|
type StartPairingRelayOptions,
|
|
19
20
|
} from '../core/device-install/operations';
|