@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.
Files changed (44) hide show
  1. package/dist/app-store-relay/index.cjs +3 -3
  2. package/dist/app-store-relay/index.d.ts +3 -5
  3. package/dist/app-store-relay/index.js +56 -58
  4. package/dist/app-store-relay/react.cjs +1 -1
  5. package/dist/app-store-relay/react.d.ts +2 -2
  6. package/dist/app-store-relay/react.js +35 -34
  7. package/dist/client-BtC4n_qf.js +1 -0
  8. package/dist/client-CBX-hLnL.mjs +189 -0
  9. package/dist/core/device-install/apple/client.d.ts +5 -4
  10. package/dist/core/device-install/apple/relay.d.ts +24 -13
  11. package/dist/core/device-install/operations/limbuild-client.d.ts +2 -1
  12. package/dist/core/device-install/operations/operations.d.ts +19 -4
  13. package/dist/core/device-install/operations/relay-client.d.ts +2 -1
  14. package/dist/device-build/index.cjs +1 -1
  15. package/dist/device-build/index.js +1 -1
  16. package/dist/device-build/react.cjs +1 -1
  17. package/dist/device-build/react.d.ts +1 -0
  18. package/dist/device-build/react.js +34 -33
  19. package/dist/device-install/index.cjs +1 -1
  20. package/dist/device-install/index.d.ts +1 -1
  21. package/dist/device-install/index.js +121 -119
  22. package/dist/device-install/react.cjs +1 -1
  23. package/dist/device-install/react.d.ts +4 -3
  24. package/dist/device-install/react.js +58 -54
  25. package/dist/limbuild-client-BagK_FWE.mjs +83 -0
  26. package/dist/limbuild-client-CVDwq54z.js +1 -0
  27. package/package.json +1 -1
  28. package/src/app-store-relay/index.test.ts +19 -29
  29. package/src/app-store-relay/index.ts +6 -9
  30. package/src/app-store-relay/react.ts +5 -4
  31. package/src/core/device-install/apple/client.ts +22 -33
  32. package/src/core/device-install/apple/relay.ts +108 -148
  33. package/src/core/device-install/operations/limbuild-client.ts +3 -0
  34. package/src/core/device-install/operations/operations.ts +21 -9
  35. package/src/core/device-install/operations/relay-client.ts +3 -2
  36. package/src/device-build/react.ts +2 -0
  37. package/src/device-install/README.md +5 -5
  38. package/src/device-install/demo/demo.tsx +26 -28
  39. package/src/device-install/index.ts +1 -0
  40. package/src/device-install/react.ts +12 -6
  41. package/dist/client-ASLtHFLh.js +0 -1
  42. package/dist/client-CGdh8yoJ.mjs +0 -234
  43. package/dist/limbuild-client-CgEncrgx.js +0 -1
  44. package/dist/limbuild-client-D7U7w342.mjs +0 -81
@@ -15,178 +15,146 @@ export type AppleProvisioningRequest = {
15
15
  payload?: unknown;
16
16
  };
17
17
 
18
- export async function createAppleRelaySession(limbuildApiUrl: string, token?: string) {
19
- const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/session', token), {
20
- method: 'POST',
21
- headers: authHeaders(token),
22
- });
23
- if (!response.ok) {
24
- throw new Error(`Apple relay session failed: HTTP ${response.status} ${await response.text()}`);
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
- export async function deleteAppleRelaySession(
30
- limbuildApiUrl: string,
31
- appleSessionId: string,
32
- token?: string,
33
- ) {
34
- const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/session/delete', token), {
35
- method: 'POST',
36
- headers: jsonHeaders(token),
37
- body: JSON.stringify({ appleSessionId }),
38
- });
39
- if (!response.ok) {
40
- throw new Error(`Apple relay session delete failed: HTTP ${response.status} ${await response.text()}`);
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
- limbuildApiUrl: string,
46
- appleSessionId: string,
108
+ relay: AppleRelayWebSocketClient,
47
109
  payload: AppleSRPInitRequest,
48
- token?: string,
49
110
  ) {
50
- return postAppleProxy<AppleSRPInitResponse>(
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
- limbuildApiUrl: string,
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 postAppleProxy(limbuildApiUrl, '/apple/auth/srp/complete', appleSessionId, payload, token);
121
+ return relay.request('srpComplete', payload);
69
122
  }
70
123
 
71
- export async function triggerTrustedDeviceTwoFactor(
72
- limbuildApiUrl: string,
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
- limbuildApiUrl: string,
89
- appleSessionId: string,
129
+ relay: AppleRelayWebSocketClient,
90
130
  phoneNumberId: number,
91
131
  mode = 'sms',
92
- token?: string,
93
132
  ) {
94
- const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/2fa/phone/trigger', token), {
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
- limbuildApiUrl: string,
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
- limbuildApiUrl: string,
124
- appleSessionId: string,
141
+ relay: AppleRelayWebSocketClient,
125
142
  phoneNumberId: number,
126
143
  code: string,
127
144
  mode = 'sms',
128
- token?: string,
129
145
  ) {
130
- const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/2fa/phone', token), {
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
- limbuildApiUrl: string,
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
- limbuildApiUrl: string,
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
- const response = await fetch(limbuildURL(limbuildApiUrl, path, token), {
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 limbuildURL(limbuildApiUrl: string, path: string, token?: string) {
207
- const base = limbuildApiUrl.replace(/\/$/, '');
208
- const suffix = path.startsWith('/') ? path : `/${path}`;
209
- const url = new URL(`${base}${suffix}`);
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
- return url;
214
- }
215
-
216
- function jsonHeaders(token?: string): Record<string, string> {
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
- limbuildApiUrl: string;
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({ limbuildApiUrl, token, log, target }: StartPairingRelayOptions) {
43
- const deviceRelayUrl = deviceRelayWebSocketUrl(limbuildApiUrl, token);
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
- limbuildApiUrl,
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(limbuildApiUrl, token);
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(limbuildApiUrl: string, token?: string) {
186
- const url = new URL(limbuildApiUrl);
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 (`libimobiledevice`) on its side and relays the
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({ limbuildApiUrl: apiUrl, token });
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 = { apiUrl, token, appleSessionId: appleLogin.session!.appleSessionId, teamId };
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
- limbuildApiUrl: apiUrl.trim(),
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?.appleSessionId && !!developerTeamId;
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.appleSessionId);
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.appleSessionId);
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(appleSessionId = appleLogin.session?.appleSessionId) {
274
- if (!apiUrl.trim() || !appleSessionId) return;
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, appleSessionId);
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
- appleSessionId = appleLogin.session?.appleSessionId,
300
+ relay = appleLogin.session?.relay,
303
301
  ) {
304
- if (!apiUrl.trim() || !appleSessionId || !teamId) return;
302
+ if (!apiUrl.trim() || !relay || !teamId) return;
305
303
  const base = {
306
- apiUrl: apiUrl.trim(),
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
- apiUrl: apiUrl.trim(),
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
- apiUrl: apiUrl.trim(),
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
- apiUrl: apiUrl.trim(),
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';