@limrun/ui 0.14.0-rc.4 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +9 -26
  2. package/dist/components/remote-control.d.ts +36 -0
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1189 -1089
  6. package/package.json +10 -51
  7. package/src/components/remote-control.tsx +267 -0
  8. package/src/index.ts +1 -1
  9. package/vite.config.ts +0 -9
  10. package/dist/app-store-relay/index.cjs +0 -9
  11. package/dist/app-store-relay/index.d.ts +0 -100
  12. package/dist/app-store-relay/index.js +0 -484
  13. package/dist/app-store-relay/react.cjs +0 -1
  14. package/dist/app-store-relay/react.d.ts +0 -15
  15. package/dist/app-store-relay/react.js +0 -69
  16. package/dist/apple-credentials/index.cjs +0 -1
  17. package/dist/apple-credentials/index.d.ts +0 -59
  18. package/dist/apple-credentials/index.js +0 -227
  19. package/dist/browser-storage-BnO-Ma-e.mjs +0 -219
  20. package/dist/browser-storage-DLxIqZk5.js +0 -1
  21. package/dist/client-D1drDXxE.js +0 -1
  22. package/dist/client-DSuNtWDv.mjs +0 -184
  23. package/dist/core/device-install/apple/client.d.ts +0 -18
  24. package/dist/core/device-install/apple/crypto.d.ts +0 -20
  25. package/dist/core/device-install/apple/gsa-srp.d.ts +0 -26
  26. package/dist/core/device-install/apple/index.d.ts +0 -5
  27. package/dist/core/device-install/apple/provisioning.d.ts +0 -212
  28. package/dist/core/device-install/apple/relay.d.ts +0 -40
  29. package/dist/core/device-install/index.d.ts +0 -4
  30. package/dist/core/device-install/operations/index.d.ts +0 -6
  31. package/dist/core/device-install/operations/limbuild-client.d.ts +0 -43
  32. package/dist/core/device-install/operations/operations.d.ts +0 -47
  33. package/dist/core/device-install/operations/relay-client.d.ts +0 -26
  34. package/dist/core/device-install/operations/relay-protocol.d.ts +0 -27
  35. package/dist/core/device-install/operations/usbmux.d.ts +0 -32
  36. package/dist/core/device-install/operations/webusb.d.ts +0 -21
  37. package/dist/core/device-install/storage/browser-storage.d.ts +0 -56
  38. package/dist/core/device-install/storage/index.d.ts +0 -2
  39. package/dist/core/device-install/storage/secret-store.d.ts +0 -101
  40. package/dist/core/device-install/types.d.ts +0 -57
  41. package/dist/core/errors.d.ts +0 -1
  42. package/dist/core/play-publish/google.d.ts +0 -13
  43. package/dist/core/play-publish/publish.d.ts +0 -34
  44. package/dist/device-build/index.cjs +0 -1
  45. package/dist/device-build/index.d.ts +0 -4
  46. package/dist/device-build/index.js +0 -85
  47. package/dist/device-build/react.cjs +0 -1
  48. package/dist/device-build/react.d.ts +0 -21
  49. package/dist/device-build/react.js +0 -68
  50. package/dist/device-build/signing.d.ts +0 -20
  51. package/dist/device-install/demo/demo.d.ts +0 -0
  52. package/dist/device-install/index.cjs +0 -1
  53. package/dist/device-install/index.d.ts +0 -18
  54. package/dist/device-install/index.js +0 -591
  55. package/dist/device-install/react.cjs +0 -1
  56. package/dist/device-install/react.d.ts +0 -24
  57. package/dist/device-install/react.js +0 -105
  58. package/dist/errors-BSkqS7U-.js +0 -1
  59. package/dist/errors-gy8yQJos.mjs +0 -6
  60. package/dist/gsa-srp-Bme3JLEi.mjs +0 -266
  61. package/dist/gsa-srp-CCIfUz7Y.js +0 -1
  62. package/dist/index-ChhtuQ7e.js +0 -22
  63. package/dist/index-YDu8K9a4.mjs +0 -11279
  64. package/dist/limbuild-client-BagK_FWE.mjs +0 -83
  65. package/dist/limbuild-client-CVDwq54z.js +0 -1
  66. package/dist/play-publish/index.cjs +0 -1
  67. package/dist/play-publish/index.d.ts +0 -2
  68. package/dist/play-publish/index.js +0 -109
  69. package/dist/play-publish/react.cjs +0 -1
  70. package/dist/play-publish/react.d.ts +0 -31
  71. package/dist/play-publish/react.js +0 -91
  72. package/dist/provisioning-B_DefBEp.mjs +0 -239
  73. package/dist/provisioning-BlQMccX_.js +0 -1
  74. package/src/app-store-relay/index.test.ts +0 -60
  75. package/src/app-store-relay/index.ts +0 -511
  76. package/src/app-store-relay/react.ts +0 -131
  77. package/src/apple-credentials/index.test.ts +0 -158
  78. package/src/apple-credentials/index.ts +0 -241
  79. package/src/core/device-install/apple/client.ts +0 -143
  80. package/src/core/device-install/apple/crypto.ts +0 -205
  81. package/src/core/device-install/apple/gsa-srp.ts +0 -123
  82. package/src/core/device-install/apple/index.ts +0 -5
  83. package/src/core/device-install/apple/provisioning.test.ts +0 -84
  84. package/src/core/device-install/apple/provisioning.ts +0 -380
  85. package/src/core/device-install/apple/relay.ts +0 -182
  86. package/src/core/device-install/index.ts +0 -4
  87. package/src/core/device-install/operations/index.ts +0 -6
  88. package/src/core/device-install/operations/limbuild-client.ts +0 -144
  89. package/src/core/device-install/operations/operations.ts +0 -234
  90. package/src/core/device-install/operations/relay-client.ts +0 -286
  91. package/src/core/device-install/operations/relay-protocol.ts +0 -71
  92. package/src/core/device-install/operations/usbmux.ts +0 -270
  93. package/src/core/device-install/operations/webusb-dom.d.ts +0 -54
  94. package/src/core/device-install/operations/webusb.ts +0 -110
  95. package/src/core/device-install/storage/browser-storage.test.ts +0 -65
  96. package/src/core/device-install/storage/browser-storage.ts +0 -325
  97. package/src/core/device-install/storage/index.ts +0 -2
  98. package/src/core/device-install/storage/secret-store.ts +0 -298
  99. package/src/core/device-install/types.ts +0 -69
  100. package/src/core/errors.ts +0 -3
  101. package/src/core/play-publish/google.ts +0 -113
  102. package/src/core/play-publish/publish.test.ts +0 -68
  103. package/src/core/play-publish/publish.ts +0 -89
  104. package/src/device-build/index.ts +0 -42
  105. package/src/device-build/react.ts +0 -130
  106. package/src/device-build/signing.ts +0 -94
  107. package/src/device-install/README.md +0 -423
  108. package/src/device-install/demo/demo.css +0 -493
  109. package/src/device-install/demo/demo.tsx +0 -1061
  110. package/src/device-install/demo/index.html +0 -12
  111. package/src/device-install/index.ts +0 -50
  112. package/src/device-install/react.ts +0 -195
  113. package/src/play-publish/README.md +0 -66
  114. package/src/play-publish/index.ts +0 -12
  115. package/src/play-publish/react.ts +0 -208
@@ -1,1061 +0,0 @@
1
- import { StrictMode, useEffect, useMemo, useState, type ChangeEvent, type ReactNode } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import {
4
- createAppleBundleID,
5
- createAppleCertificate,
6
- createAppleProfile,
7
- downloadAppleCertificate,
8
- downloadAppleProfile,
9
- exportAppleCertificateP12,
10
- generateAppleSigningKeyAndCSR,
11
- listAppleBundleIDs,
12
- listAppleCertificates,
13
- listAppleDevices,
14
- listAppleProfiles,
15
- listAppleTeams,
16
- registerAppleDevice,
17
- type AppleDeveloperPortalAppID,
18
- type AppleDeveloperPortalDevice,
19
- type AppleDeveloperPortalTeam,
20
- } from '../../app-store-relay';
21
- import { useAppleIDLogin } from '../../app-store-relay/react';
22
- import {
23
- getLatestSigningAssetsWithCertificate,
24
- importSigningAssetsFromFiles,
25
- parseProvisioningProfileBase64,
26
- putAppleGeneratedSigningAssets,
27
- type StoredSigningAssets,
28
- } from '../../device-build';
29
- import { useDeviceBuild } from '../../device-build/react';
30
- import { useDeviceInstallRelay } from '../react';
31
- import './demo.css';
32
- import { errorMessage } from '../../core/errors';
33
-
34
- type ActivityLine = {
35
- id: number;
36
- time: string;
37
- message: string;
38
- detail?: string;
39
- };
40
-
41
- type StepState = 'pending' | 'active' | 'done' | 'error';
42
- type StepView = { state: StepState; label: string };
43
- type PillTone = 'neutral' | 'active' | 'success' | 'danger';
44
-
45
- type SigningSource = 'apple' | 'upload';
46
- type CertificateChoice = 'stored' | 'create';
47
- type ProfileChoice = 'create' | 'existing';
48
-
49
- type AppleResourceState = {
50
- teams: AppleDeveloperPortalTeam[];
51
- appIds: AppleDeveloperPortalAppID[];
52
- devices: AppleDeveloperPortalDevice[];
53
- certificates: Array<Record<string, unknown>>;
54
- profiles: Array<Record<string, unknown>>;
55
- };
56
-
57
- const emptyAppleResources: AppleResourceState = {
58
- teams: [],
59
- appIds: [],
60
- devices: [],
61
- certificates: [],
62
- profiles: [],
63
- };
64
-
65
- const storageKeys = {
66
- apiUrl: 'limrun-device-demo-api-url',
67
- token: 'limrun-device-demo-token',
68
- ipaDownloadUrl: 'limrun-device-demo-ipa-download-url',
69
- bundleId: 'limrun-device-demo-bundle-id',
70
- certificatePassword: 'limrun-device-demo-certificate-password',
71
- appleAccount: 'limrun-device-demo-apple-account',
72
- };
73
-
74
- function App() {
75
- const [apiUrl, setApiUrl] = useLocalStorage(storageKeys.apiUrl, 'http://127.0.0.1:8080');
76
- const [token, setToken] = useLocalStorage(storageKeys.token, '');
77
- const [ipaDownloadUrl, setIPADownloadUrl] = useLocalStorage(storageKeys.ipaDownloadUrl, '');
78
- const [bundleId, setBundleId] = useLocalStorage(storageKeys.bundleId, '');
79
- const [appleAccount, setAppleAccount] = useLocalStorage(storageKeys.appleAccount, '');
80
- const [certificatePassword, setCertificatePassword] = useLocalStorage(storageKeys.certificatePassword, '');
81
- const [signingSource, setSigningSource] = useState<SigningSource>('apple');
82
- const [applePassword, setApplePassword] = useState('');
83
- const [twoFactorCode, setTwoFactorCode] = useState('');
84
- const [resources, setResources] = useState<AppleResourceState>(emptyAppleResources);
85
- const [selectedTeamId, setSelectedTeamId] = useState('');
86
- const [selectedAppIdId, setSelectedAppIdId] = useState('');
87
- const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>([]);
88
- const [certificateChoice, setCertificateChoice] = useState<CertificateChoice>('create');
89
- const [profileChoice, setProfileChoice] = useState<ProfileChoice>('create');
90
- const [selectedProfileId, setSelectedProfileId] = useState('');
91
- const [storedCertificate, setStoredCertificate] = useState<StoredSigningAssets>();
92
- const [appleBusy, setAppleBusy] = useState<string>();
93
- const [certificateFile, setCertificateFile] = useState<File>();
94
- const [profileFile, setProfileFile] = useState<File>();
95
- const [signingAssets, setSigningAssets] = useState<StoredSigningAssets>();
96
- const [activity, setActivity] = useState<ActivityLine[]>([]);
97
- const [prepareError, setPrepareError] = useState<string>();
98
- const [dismissedError, setDismissedError] = useState<string>();
99
- const [installPhase, setInstallPhase] = useState<'idle' | 'installing' | 'done' | 'error'>('idle');
100
-
101
- const addActivity = (message: string, detail?: string) => {
102
- setActivity((current) =>
103
- [
104
- {
105
- id: Date.now() + Math.random(),
106
- time: new Date().toLocaleTimeString(),
107
- message,
108
- detail,
109
- },
110
- ...current,
111
- ].slice(0, 120),
112
- );
113
- // Derive install completion from relay progress messages (the hook has no
114
- // explicit "installed" signal). Only act while an install is in flight so
115
- // pairing's "completed" messages don't flip this.
116
- setInstallPhase((phase) => {
117
- if (phase !== 'installing') return phase;
118
- if (/100% complete/i.test(message) || /install completed/i.test(message)) return 'done';
119
- if (/^server error/i.test(message) || /install failed/i.test(message)) return 'error';
120
- return phase;
121
- });
122
- };
123
-
124
- const install = useDeviceInstallRelay({
125
- apiUrl: apiUrl.trim() || undefined,
126
- token: token.trim() || undefined,
127
- log: addActivity,
128
- });
129
-
130
- const build = useDeviceBuild({
131
- apiUrl: apiUrl.trim() || undefined,
132
- token: token.trim() || undefined,
133
- signingAssets,
134
- });
135
-
136
- const appleLogin = useAppleIDLogin({
137
- registryApiUrl: apiUrl.trim(),
138
- token: token.trim() || undefined,
139
- });
140
-
141
- const combinedError = prepareError ?? appleLogin.error ?? install.error ?? build.error;
142
- // Show the error as a floating toast (so it's visible without scrolling up)
143
- // until it's dismissed; a different error re-shows it.
144
- const visibleError = combinedError && combinedError !== dismissedError ? combinedError : undefined;
145
- const selectedUDID = install.device?.hello.serialNumber;
146
- const selectedTeam = resources.teams.find((team) => appleTeamSelectionId(team) === selectedTeamId);
147
- // Resolve the team id the same way the <select> value is computed, so teams
148
- // whose portal id lives in providerId/publicProviderId (not teamId) still
149
- // drive the Apple Developer flow instead of leaving developerTeamId undefined.
150
- const developerTeamId = appleTeamSelectionId(selectedTeam);
151
- const selectedProfile = resources.profiles.find(
152
- (profile) => stringField(profile, 'provisioningProfileId') === selectedProfileId,
153
- );
154
- const canUseApple = !!apiUrl.trim() && !!appleLogin.session?.relay && !!developerTeamId;
155
- const canPrepareSigning = !!certificateFile && !!profileFile;
156
- const canStartBuild = !!signingAssets && build.status !== 'queued' && build.status !== 'running';
157
- const canInstall = install.canInstall && build.status === 'succeeded' && !!ipaDownloadUrl.trim();
158
-
159
- // Live per-step status used by the overview stepper and the section pills.
160
- const pairStep: StepView =
161
- install.busyAction === 'usb' ? { state: 'active', label: 'Selecting device…' }
162
- : install.busyAction === 'pair' ? { state: 'active', label: 'Pairing…' }
163
- : install.hasPairRecord ? { state: 'done', label: 'Paired' }
164
- : install.device ? { state: 'active', label: 'Device selected' }
165
- : { state: 'pending', label: 'Not started' };
166
- const signStep: StepView =
167
- signingAssets ? { state: 'done', label: 'Assets ready' }
168
- : appleBusy === 'signing' ? { state: 'active', label: 'Preparing assets…' }
169
- : appleBusy === 'login' || appleLogin.status === 'authenticating' ?
170
- { state: 'active', label: 'Signing in…' }
171
- : appleLogin.status === 'two-factor-required' ? { state: 'active', label: '2FA required' }
172
- : { state: 'pending', label: 'Missing' };
173
- const buildStep: StepView =
174
- build.status === 'succeeded' ? { state: 'done', label: 'Succeeded' }
175
- : build.status === 'failed' || build.status === 'cancelled' ? { state: 'error', label: build.status }
176
- : build.status === 'queued' || build.status === 'running' ? { state: 'active', label: build.status }
177
- : { state: 'pending', label: 'Idle' };
178
- const installStep: StepView =
179
- installPhase === 'done' ? { state: 'done', label: 'Installed' }
180
- : installPhase === 'error' ? { state: 'error', label: 'Failed' }
181
- : install.busyAction === 'install' || installPhase === 'installing' ?
182
- { state: 'active', label: 'Installing…' }
183
- : canInstall ? { state: 'active', label: 'Ready to install' }
184
- : { state: 'pending', label: 'Waiting' };
185
-
186
- async function runInstall() {
187
- setInstallPhase('installing');
188
- const relay = await install.startInstallation({ downloadUrl: ipaDownloadUrl.trim() });
189
- if (!relay) setInstallPhase('error');
190
- }
191
-
192
- const buildLogText = useMemo(
193
- () =>
194
- build.logs
195
- .slice(-80)
196
- .map((line) => line.data)
197
- .join('\n'),
198
- [build.logs],
199
- );
200
-
201
- useEffect(() => {
202
- let cancelled = false;
203
- if (!developerTeamId) {
204
- setStoredCertificate(undefined);
205
- setCertificateChoice('create');
206
- return;
207
- }
208
- void getLatestSigningAssetsWithCertificate(developerTeamId, 'development').then((assets) => {
209
- if (cancelled) return;
210
- setStoredCertificate(assets);
211
- setCertificateChoice(assets ? 'stored' : 'create');
212
- });
213
- return () => {
214
- cancelled = true;
215
- };
216
- }, [developerTeamId]);
217
-
218
- async function prepareSigningAssets() {
219
- if (!certificateFile || !profileFile) return;
220
- setPrepareError(undefined);
221
- try {
222
- const assets = await importSigningAssetsFromFiles({
223
- certificateFile,
224
- provisioningProfileFile: profileFile,
225
- certificatePassword,
226
- bundleId: bundleId.trim() || undefined,
227
- deviceUDID: selectedUDID,
228
- signingMode: 'development',
229
- });
230
- setSigningAssets(assets);
231
- if (!bundleId.trim()) {
232
- setBundleId(assets.bundleID);
233
- }
234
- addActivity('Signing assets ready', assets.bundleID);
235
- } catch (error) {
236
- setSigningAssets(undefined);
237
- setPrepareError(errorMessage(error));
238
- }
239
- }
240
-
241
- async function signInWithApple() {
242
- if (!apiUrl.trim()) return;
243
- setAppleBusy('login');
244
- setPrepareError(undefined);
245
- try {
246
- const session = await appleLogin.startLogin({
247
- accountName: appleAccount,
248
- password: applePassword,
249
- });
250
- setApplePassword('');
251
- if (session && !session.requiresTwoFactor) {
252
- await loadAppleTeams(session.relay);
253
- }
254
- } catch (error) {
255
- setPrepareError(errorMessage(error));
256
- } finally {
257
- setAppleBusy(undefined);
258
- }
259
- }
260
-
261
- async function submitAppleTwoFactor() {
262
- if (!appleLogin.session) return;
263
- setAppleBusy('2fa');
264
- setPrepareError(undefined);
265
- try {
266
- await appleLogin.submitTwoFactorCode(twoFactorCode);
267
- setTwoFactorCode('');
268
- await loadAppleTeams(appleLogin.session.relay);
269
- } catch (error) {
270
- setPrepareError(errorMessage(error));
271
- } finally {
272
- setAppleBusy(undefined);
273
- }
274
- }
275
-
276
- async function loadAppleTeams(relay = appleLogin.session?.relay) {
277
- if (!apiUrl.trim() || !relay) return;
278
- setAppleBusy('teams');
279
- setPrepareError(undefined);
280
- try {
281
- await appleLogin.finalize().catch(() => undefined);
282
- const teams = await listAppleTeams({ relay });
283
- setResources((current) => ({ ...current, teams }));
284
- const firstTeamId = teams.map(appleTeamSelectionId).find(Boolean) ?? '';
285
- setSelectedTeamId(firstTeamId);
286
- const firstDeveloperTeamId = appleTeamSelectionId(
287
- teams.find((team) => appleTeamSelectionId(team) === firstTeamId),
288
- );
289
- if (firstDeveloperTeamId) {
290
- await loadAppleResources(firstDeveloperTeamId, relay);
291
- }
292
- } catch (error) {
293
- setPrepareError(errorMessage(error));
294
- } finally {
295
- setAppleBusy(undefined);
296
- }
297
- }
298
-
299
- async function loadAppleResources(teamId = developerTeamId, relay = appleLogin.session?.relay) {
300
- if (!apiUrl.trim() || !relay || !teamId) return;
301
- const base = {
302
- relay,
303
- teamId,
304
- };
305
- const [appIds, devices, certificates, profiles] = await Promise.all([
306
- listAppleBundleIDs(base),
307
- listAppleDevices(base),
308
- listAppleCertificates({ ...base, certificateKind: 'development' }),
309
- listAppleProfiles({ ...base, profileKind: 'development' }),
310
- ]);
311
- setResources((current) => ({ ...current, appIds, devices, certificates, profiles }));
312
- const firstApp = appIds[0];
313
- const firstAppId = appIdIdentifier(firstApp) ?? '';
314
- const firstBundleId = appIdBundleId(firstApp);
315
- setSelectedAppIdId(firstAppId);
316
- if (!bundleId.trim() && firstBundleId) setBundleId(firstBundleId);
317
- const matchingDevice =
318
- selectedUDID ? devices.find((device) => sameUDID(device.deviceNumber, selectedUDID)) : undefined;
319
- const firstDeviceId = matchingDevice?.deviceId ?? devices.find((device) => !!device.deviceId)?.deviceId;
320
- setSelectedDeviceIds(firstDeviceId ? [firstDeviceId] : []);
321
- setSelectedProfileId(
322
- profiles.map((profile) => stringField(profile, 'provisioningProfileId')).find(Boolean) ?? '',
323
- );
324
- addActivity('Apple Developer resources loaded', `${devices.length} devices, ${profiles.length} profiles`);
325
- }
326
-
327
- async function createBundleIdResource() {
328
- if (!canUseApple || !bundleId.trim() || !appleLogin.session || !developerTeamId) return;
329
- setAppleBusy('bundle');
330
- setPrepareError(undefined);
331
- try {
332
- const app = await createAppleBundleID({
333
- relay: appleLogin.session.relay,
334
- teamId: developerTeamId,
335
- bundleId: bundleId.trim(),
336
- });
337
- const appIdId = stringField(app, 'appIdId') ?? stringField(app, 'appId');
338
- if (appIdId) setSelectedAppIdId(appIdId);
339
- await loadAppleResources(developerTeamId);
340
- addActivity('Apple bundle ID created', bundleId.trim());
341
- } catch (error) {
342
- setPrepareError(errorMessage(error));
343
- } finally {
344
- setAppleBusy(undefined);
345
- }
346
- }
347
-
348
- async function registerSelectedIPhone() {
349
- if (!canUseApple || !appleLogin.session || !developerTeamId || !selectedUDID) return;
350
- setAppleBusy('register-device');
351
- setPrepareError(undefined);
352
- try {
353
- await registerAppleDevice({
354
- relay: appleLogin.session.relay,
355
- teamId: developerTeamId,
356
- deviceUDID: selectedUDID,
357
- name: install.device?.hello.productName ?? 'Limrun iPhone',
358
- });
359
- await loadAppleResources(developerTeamId);
360
- addActivity('Registered connected iPhone', selectedUDID);
361
- } catch (error) {
362
- setPrepareError(errorMessage(error));
363
- } finally {
364
- setAppleBusy(undefined);
365
- }
366
- }
367
-
368
- async function prepareAppleSigningAssets() {
369
- if (!canUseApple || !appleLogin.session || !developerTeamId) return;
370
- if (!bundleId.trim()) {
371
- setPrepareError('Enter a bundle ID.');
372
- return;
373
- }
374
- if (!selectedAppIdId) {
375
- setPrepareError('Select or create a Bundle ID resource.');
376
- return;
377
- }
378
- if (selectedDeviceIds.length === 0) {
379
- setPrepareError('Select at least one Apple Developer device.');
380
- return;
381
- }
382
- setAppleBusy('signing');
383
- setPrepareError(undefined);
384
- try {
385
- const base = {
386
- relay: appleLogin.session.relay,
387
- teamId: developerTeamId,
388
- };
389
- let certificateId = storedCertificate?.certificateID;
390
- let certificateP12Base64 = storedCertificate?.certificateP12Base64;
391
- let storedCertificatePassword = storedCertificate?.certificatePassword;
392
-
393
- // Reuse the stored private key, but resolve the id the profile API
394
- // actually expects. createProvisioningProfile needs the certificate's
395
- // canonical `certificateId`; the stored value may be a `certRequestId`,
396
- // so match against the live cert list and use the matched certificateId.
397
- // If the cert is no longer on the team (revoked), regenerate instead.
398
- let reuseStoredCertificate = false;
399
- if (certificateChoice === 'stored' && certificateId && certificateP12Base64) {
400
- const currentCertificates = await listAppleCertificates({
401
- ...base,
402
- certificateKind: 'development',
403
- });
404
- const matched = currentCertificates.find(
405
- (item) =>
406
- stringField(item, 'certificateId') === certificateId ||
407
- stringField(item, 'certRequestId') === certificateId,
408
- );
409
- if (matched) {
410
- certificateId = stringField(matched, 'certificateId') ?? certificateId;
411
- reuseStoredCertificate = true;
412
- addActivity('Reusing stored Apple certificate', certificateId);
413
- } else {
414
- addActivity('Stored certificate is no longer on the team', 'Generating a new certificate.');
415
- }
416
- }
417
-
418
- if (!reuseStoredCertificate) {
419
- const keyMaterial = await generateAppleSigningKeyAndCSR({
420
- commonName: `Limrun ${bundleId.trim()}`,
421
- });
422
- const certificate = await createAppleCertificate({
423
- ...base,
424
- certificateKind: 'development',
425
- csrPEM: keyMaterial.csrPEM,
426
- });
427
- certificateId =
428
- stringField(certificate, 'certificateId') ?? stringField(certificate, 'certRequestId');
429
- if (!certificateId) throw new Error('Apple certificate creation did not return a certificate ID.');
430
- const downloadedCertificate = await downloadAppleCertificate({
431
- ...base,
432
- certificateKind: 'development',
433
- certificateId,
434
- });
435
- if (!downloadedCertificate.rawBodyBase64) {
436
- throw new Error('Apple certificate download did not return certificate bytes.');
437
- }
438
- certificateP12Base64 = exportAppleCertificateP12({
439
- privateKeyPKCS8Base64: keyMaterial.privateKeyPKCS8Base64,
440
- certificateBase64: downloadedCertificate.rawBodyBase64,
441
- password: certificatePassword,
442
- friendlyName: `Apple Development ${bundleId.trim()}`,
443
- });
444
- storedCertificatePassword = certificatePassword || undefined;
445
- }
446
-
447
- if (certificateChoice === 'stored' && !reuseStoredCertificate) {
448
- throw new Error(
449
- 'The stored certificate is no longer current on this team (it may have been revoked). ' +
450
- 'Choose "Create new certificate" (needs a free slot in Apple\'s 2-cert limit), or use the ' +
451
- '"Upload files" tab with a .p12.',
452
- );
453
- }
454
- if (!certificateId || !certificateP12Base64) {
455
- throw new Error('Select a stored certificate or create a new certificate.');
456
- }
457
-
458
- let profileId = selectedProfileId;
459
- if (profileChoice === 'create') {
460
- const profile = await createAppleProfile({
461
- ...base,
462
- profileKind: 'development',
463
- bundleId: bundleId.trim(),
464
- appIdId: selectedAppIdId,
465
- certificateIds: [certificateId],
466
- deviceIds: selectedDeviceIds,
467
- name: `Limrun ${bundleId.trim()} ${Date.now()}`,
468
- });
469
- profileId = stringField(profile, 'provisioningProfileId') ?? stringField(profile, 'profileId') ?? '';
470
- }
471
- if (!profileId) throw new Error('Select or create a provisioning profile.');
472
- const downloadedProfile = await downloadAppleProfile({ ...base, profileId });
473
- if (!downloadedProfile.rawBodyBase64) {
474
- throw new Error('Apple provisioning profile download did not return profile bytes.');
475
- }
476
- const profile = parseProvisioningProfileBase64(downloadedProfile.rawBodyBase64);
477
- const assets = await putAppleGeneratedSigningAssets({
478
- bundleID: bundleId.trim(),
479
- deviceUDID: selectedUDID,
480
- teamID: developerTeamId,
481
- signingMode: 'development',
482
- certificateID: certificateId,
483
- certificateP12Base64,
484
- certificatePassword: storedCertificatePassword,
485
- provisioningProfileBase64: downloadedProfile.rawBodyBase64,
486
- profile,
487
- });
488
- setSigningAssets(assets);
489
- await loadAppleResources(developerTeamId);
490
- addActivity('Apple signing assets ready', assets.bundleID);
491
- } catch (error) {
492
- setSigningAssets(undefined);
493
- const message = errorMessage(error);
494
- const capHit =
495
- /current Development certificate|pending certificate request|Maximum number of certificates/i.test(
496
- message,
497
- );
498
- setPrepareError(
499
- capHit ?
500
- `${message}\n\nApple caps development certificates at 2. Either pick "Use stored local certificate" (if this browser already created one), upload a .p12 instead, or revoke an existing certificate at developer.apple.com and retry.`
501
- : message,
502
- );
503
- } finally {
504
- setAppleBusy(undefined);
505
- }
506
- }
507
-
508
- async function startBuild() {
509
- const execId = await build.startBuild({ signingAssets });
510
- if (execId) {
511
- addActivity('Signed device build started', execId);
512
- }
513
- }
514
-
515
- return (
516
- <main className="page">
517
- <header className="hero">
518
- <p className="eyebrow">Limrun WebUSB Demo</p>
519
- <h1>Install a signed iOS build onto a physical iPhone</h1>
520
- <p>
521
- This page demonstrates the upload-signing path from
522
- <code> REAL_DEVICE_INSTALL_README.md</code>: select an iPhone, pair it, upload signing assets, start
523
- a signed <code>iphoneos</code> build, and install over WebUSB.
524
- </p>
525
- </header>
526
-
527
- <section className="stepper">
528
- {[
529
- { n: 1, title: 'Pair', step: pairStep },
530
- { n: 2, title: 'Sign', step: signStep },
531
- { n: 3, title: 'Build', step: buildStep },
532
- { n: 4, title: 'Install', step: installStep },
533
- ].map(({ n, title, step }) => (
534
- <div key={n} className={`step ${step.state}`}>
535
- <span className="stepDot">
536
- {step.state === 'done' ?
537
- '✓'
538
- : step.state === 'error' ?
539
- '!'
540
- : n}
541
- </span>
542
- <div className="stepText">
543
- <span className="stepTitle">{title}</span>
544
- <span className="stepLabel">{step.label}</span>
545
- </div>
546
- </div>
547
- ))}
548
- </section>
549
-
550
- {visibleError && (
551
- <div className="errorToast" role="alert">
552
- <div className="errorToastBody">
553
- <strong>Something went wrong</strong>
554
- <pre>{visibleError}</pre>
555
- </div>
556
- <button
557
- type="button"
558
- className="errorToastClose"
559
- aria-label="Dismiss"
560
- onClick={() => setDismissedError(combinedError)}
561
- >
562
- ✕
563
- </button>
564
- </div>
565
- )}
566
-
567
- <section className="card">
568
- <h2>Connection</h2>
569
- <div className="grid two">
570
- <label>
571
- limbuild API URL
572
- <input
573
- value={apiUrl}
574
- onChange={(event) => setApiUrl(event.currentTarget.value)}
575
- placeholder="http://127.0.0.1:8080"
576
- />
577
- </label>
578
- <label>
579
- Token (optional)
580
- <input
581
- value={token}
582
- onChange={(event) => setToken(event.currentTarget.value)}
583
- placeholder="Instance token, if required"
584
- />
585
- </label>
586
- </div>
587
- <p className="hint">
588
- Run this page on <code>localhost</code> or HTTPS. WebUSB is available in Chromium browsers only.
589
- </p>
590
- <label>
591
- IPA download URL
592
- <input
593
- value={ipaDownloadUrl}
594
- onChange={(event) => setIPADownloadUrl(event.currentTarget.value)}
595
- placeholder="https://asset-storage.example/app.ipa"
596
- />
597
- </label>
598
- </section>
599
-
600
- <section className="card">
601
- <div className="sectionHeader">
602
- <div>
603
- <h2>1. Pair iPhone</h2>
604
- <p>Select the USB device, then pair. Unlock the iPhone and tap Trust.</p>
605
- </div>
606
- <StatusPill tone={pillTone(pairStep.state)}>{pairStep.label}</StatusPill>
607
- </div>
608
- <div className="actions">
609
- <button
610
- type="button"
611
- disabled={!apiUrl.trim() || !!install.busyAction}
612
- onClick={() => void install.requestUSBAccess()}
613
- >
614
- Select iPhone
615
- </button>
616
- <button type="button" disabled={!install.canPair} onClick={() => void install.pairBrowser()}>
617
- Pair
618
- </button>
619
- <button type="button" className="secondary" onClick={install.stopRelay}>
620
- Stop relay
621
- </button>
622
- </div>
623
- <dl className="facts">
624
- <div>
625
- <dt>Device</dt>
626
- <dd>{install.device?.hello.productName ?? 'Not selected'}</dd>
627
- </div>
628
- <div>
629
- <dt>UDID</dt>
630
- <dd>{selectedUDID ?? 'Not selected'}</dd>
631
- </div>
632
- <div>
633
- <dt>Busy action</dt>
634
- <dd>{install.busyAction ?? 'idle'}</dd>
635
- </div>
636
- </dl>
637
- </section>
638
-
639
- <section className="card">
640
- <div className="sectionHeader">
641
- <div>
642
- <h2>2. Prepare signing assets</h2>
643
- <p>
644
- Use the Apple Developer relay or upload a <code>.p12</code> and
645
- <code> .mobileprovision</code> directly.
646
- </p>
647
- </div>
648
- <StatusPill tone={pillTone(signStep.state)}>{signStep.label}</StatusPill>
649
- </div>
650
-
651
- <div className="tabs" role="tablist" aria-label="Signing source">
652
- <button
653
- type="button"
654
- className={signingSource === 'apple' ? 'tab active' : 'tab'}
655
- onClick={() => setSigningSource('apple')}
656
- >
657
- Apple Developer
658
- </button>
659
- <button
660
- type="button"
661
- className={signingSource === 'upload' ? 'tab active' : 'tab'}
662
- onClick={() => setSigningSource('upload')}
663
- >
664
- Upload files
665
- </button>
666
- </div>
667
-
668
- <div className="grid two">
669
- <label>
670
- Bundle ID
671
- <input
672
- value={bundleId}
673
- onChange={(event) => setBundleId(event.currentTarget.value)}
674
- placeholder="com.example.MyApp"
675
- />
676
- </label>
677
- <label>
678
- Certificate password
679
- <input
680
- type="password"
681
- value={certificatePassword}
682
- onChange={(event) => setCertificatePassword(event.currentTarget.value)}
683
- />
684
- </label>
685
- </div>
686
-
687
- {signingSource === 'apple' ?
688
- <div className="subPanel">
689
- <div className="sectionHeader compact">
690
- <div>
691
- <h3>Apple ID</h3>
692
- <p>Status: {appleLogin.status}</p>
693
- </div>
694
- <button
695
- type="button"
696
- disabled={!appleAccount || !applePassword || appleBusy === 'login'}
697
- onClick={() => void signInWithApple()}
698
- >
699
- Sign in
700
- </button>
701
- </div>
702
- <div className="grid two">
703
- <label>
704
- Apple ID
705
- <input
706
- type="email"
707
- autoComplete="username"
708
- value={appleAccount}
709
- onChange={(event) => setAppleAccount(event.currentTarget.value)}
710
- />
711
- </label>
712
- <label>
713
- Apple ID password
714
- <input
715
- type="password"
716
- autoComplete="current-password"
717
- value={applePassword}
718
- onChange={(event) => setApplePassword(event.currentTarget.value)}
719
- />
720
- </label>
721
- </div>
722
- {appleLogin.status === 'two-factor-required' && (
723
- <div className="inlineForm">
724
- <input
725
- inputMode="numeric"
726
- autoComplete="one-time-code"
727
- placeholder="Two-factor code"
728
- value={twoFactorCode}
729
- onChange={(event) => setTwoFactorCode(event.currentTarget.value)}
730
- />
731
- <button
732
- type="button"
733
- disabled={!twoFactorCode || appleBusy === '2fa'}
734
- onClick={() => void submitAppleTwoFactor()}
735
- >
736
- Submit code
737
- </button>
738
- </div>
739
- )}
740
-
741
- <div className="grid two">
742
- <label>
743
- Team
744
- <select
745
- value={selectedTeamId}
746
- disabled={resources.teams.length === 0}
747
- onChange={(event) => {
748
- const value = event.currentTarget.value;
749
- setSelectedTeamId(value);
750
- const team = resources.teams.find((item) => appleTeamSelectionId(item) === value);
751
- const teamId = appleTeamSelectionId(team);
752
- if (teamId) void loadAppleResources(teamId);
753
- }}
754
- >
755
- <option value="">Sign in to load teams</option>
756
- {resources.teams.map((team, index) => {
757
- const value = appleTeamSelectionId(team) ?? `team-${index}`;
758
- return (
759
- <option key={value} value={value}>
760
- {team.name ?? value}
761
- </option>
762
- );
763
- })}
764
- </select>
765
- </label>
766
- <label>
767
- Bundle ID resource
768
- <select
769
- value={selectedAppIdId}
770
- disabled={resources.appIds.length === 0}
771
- onChange={(event) => setSelectedAppIdId(event.currentTarget.value)}
772
- >
773
- <option value="">Select or create a Bundle ID resource</option>
774
- {resources.appIds.map((appId, index) => {
775
- const value = appIdIdentifier(appId) ?? `app-${index}`;
776
- return (
777
- <option key={value} value={value}>
778
- {appIdBundleId(appId) ?? appId.name ?? value}
779
- </option>
780
- );
781
- })}
782
- </select>
783
- </label>
784
- </div>
785
- <div className="actions">
786
- <button
787
- type="button"
788
- disabled={!canUseApple || !bundleId.trim() || appleBusy === 'bundle'}
789
- onClick={() => void createBundleIdResource()}
790
- >
791
- Create bundle ID
792
- </button>
793
- <button
794
- type="button"
795
- className="secondary"
796
- disabled={!canUseApple || !selectedUDID || appleBusy === 'register-device'}
797
- onClick={() => void registerSelectedIPhone()}
798
- >
799
- Register selected iPhone
800
- </button>
801
- <button
802
- type="button"
803
- className="secondary"
804
- disabled={!appleLogin.session || appleBusy === 'teams'}
805
- onClick={() => void loadAppleTeams()}
806
- >
807
- Reload Apple resources
808
- </button>
809
- </div>
810
- <div className="grid two">
811
- <label>
812
- Apple devices
813
- <select
814
- multiple
815
- className="multiSelect"
816
- value={selectedDeviceIds}
817
- onChange={(event) =>
818
- setSelectedDeviceIds(
819
- Array.from(event.currentTarget.selectedOptions).map((option) => option.value),
820
- )
821
- }
822
- >
823
- {resources.devices.map((device, index) => {
824
- const value = device.deviceId ?? `device-${index}`;
825
- return (
826
- <option key={value} value={value}>
827
- {device.name ?? device.deviceNumber ?? value}
828
- </option>
829
- );
830
- })}
831
- </select>
832
- </label>
833
- <label>
834
- Existing profile
835
- <select
836
- value={selectedProfileId}
837
- disabled={resources.profiles.length === 0}
838
- onChange={(event) => setSelectedProfileId(event.currentTarget.value)}
839
- >
840
- <option value="">Select profile</option>
841
- {resources.profiles.map((profile, index) => {
842
- const value = stringField(profile, 'provisioningProfileId') ?? `profile-${index}`;
843
- return (
844
- <option key={value} value={value}>
845
- {stringField(profile, 'name') ?? value}
846
- </option>
847
- );
848
- })}
849
- </select>
850
- </label>
851
- <label>
852
- Certificate
853
- <select
854
- value={certificateChoice}
855
- onChange={(event) => setCertificateChoice(event.currentTarget.value as CertificateChoice)}
856
- >
857
- <option value="stored" disabled={!storedCertificate}>
858
- Use stored local certificate
859
- </option>
860
- <option value="create">Create new certificate</option>
861
- </select>
862
- </label>
863
- <label>
864
- Provisioning profile
865
- <select
866
- value={profileChoice}
867
- onChange={(event) => setProfileChoice(event.currentTarget.value as ProfileChoice)}
868
- >
869
- <option value="create">Create new profile</option>
870
- <option value="existing" disabled={!selectedProfile}>
871
- Use selected existing profile
872
- </option>
873
- </select>
874
- </label>
875
- </div>
876
- <p className="hint">
877
- Apple account has {resources.certificates.length} matching development certificates. Existing
878
- Apple certs can only be reused when this browser already has the private key.
879
- </p>
880
- <div className="actions">
881
- <button
882
- type="button"
883
- disabled={!canUseApple || appleBusy === 'signing'}
884
- onClick={() => void prepareAppleSigningAssets()}
885
- >
886
- Prepare Apple signing assets
887
- </button>
888
- </div>
889
- </div>
890
- : <div className="subPanel">
891
- <p className="hint">
892
- Use a development <code>.mobileprovision</code> that includes the selected iPhone UDID and
893
- covers the app bundle ID.
894
- </p>
895
- <div className="grid two">
896
- <label>
897
- .p12 certificate
898
- <input
899
- type="file"
900
- accept=".p12,application/x-pkcs12"
901
- onChange={(event) => updateFile(setCertificateFile, event)}
902
- />
903
- </label>
904
- <label>
905
- .mobileprovision profile
906
- <input
907
- type="file"
908
- accept=".mobileprovision"
909
- onChange={(event) => updateFile(setProfileFile, event)}
910
- />
911
- </label>
912
- </div>
913
- <div className="actions">
914
- <button type="button" disabled={!canPrepareSigning} onClick={() => void prepareSigningAssets()}>
915
- Prepare uploaded signing assets
916
- </button>
917
- </div>
918
- </div>
919
- }
920
-
921
- {signingAssets && (
922
- <dl className="facts">
923
- <div>
924
- <dt>Profile bundle</dt>
925
- <dd>{signingAssets.profile.bundleID ?? signingAssets.bundleID}</dd>
926
- </div>
927
- <div>
928
- <dt>Team</dt>
929
- <dd>{signingAssets.teamID ?? signingAssets.profile.teamID ?? 'unknown'}</dd>
930
- </div>
931
- <div>
932
- <dt>Devices in profile</dt>
933
- <dd>{signingAssets.profile.provisionedDevices.length}</dd>
934
- </div>
935
- </dl>
936
- )}
937
- </section>
938
-
939
- <section className="card">
940
- <div className="sectionHeader">
941
- <div>
942
- <h2>3. Build</h2>
943
- <p>Starts a signed device build on limbuild and streams xcodebuild logs.</p>
944
- </div>
945
- <StatusPill tone={pillTone(buildStep.state)}>{buildStep.label}</StatusPill>
946
- </div>
947
- <div className="actions">
948
- <button type="button" disabled={!canStartBuild} onClick={() => void startBuild()}>
949
- Start signed build
950
- </button>
951
- </div>
952
- <pre className="logBox">{buildLogText || 'Build logs will appear here.'}</pre>
953
- </section>
954
-
955
- <section className="card">
956
- <div className="sectionHeader">
957
- <div>
958
- <h2>4. Install</h2>
959
- <p>Installs the latest successful signed build onto the paired iPhone.</p>
960
- </div>
961
- <StatusPill tone={pillTone(installStep.state)}>{installStep.label}</StatusPill>
962
- </div>
963
- <div className="actions">
964
- <button
965
- type="button"
966
- disabled={!canInstall || installPhase === 'installing'}
967
- onClick={() => void runInstall()}
968
- >
969
- Install to iPhone
970
- </button>
971
- </div>
972
- </section>
973
-
974
- <section className="card">
975
- <div className="sectionHeader">
976
- <div>
977
- <h2>Activity</h2>
978
- <p>Device, relay, signing, build, and install events.</p>
979
- </div>
980
- <button type="button" className="secondary" onClick={() => setActivity([])}>
981
- Clear
982
- </button>
983
- </div>
984
- <div className="activity">
985
- {activity.length === 0 ?
986
- <p className="hint">No activity yet.</p>
987
- : activity.map((line) => (
988
- <div key={line.id} className="activityLine">
989
- <span>{line.time}</span>
990
- <strong>{line.message}</strong>
991
- {line.detail && <pre>{line.detail}</pre>}
992
- </div>
993
- ))
994
- }
995
- </div>
996
- </section>
997
- </main>
998
- );
999
- }
1000
-
1001
- function pillTone(state: StepState): PillTone {
1002
- return (
1003
- state === 'done' ? 'success'
1004
- : state === 'error' ? 'danger'
1005
- : state === 'active' ? 'active'
1006
- : 'neutral'
1007
- );
1008
- }
1009
-
1010
- function StatusPill({ tone, children }: { tone: PillTone; children: ReactNode }) {
1011
- return <span className={`pill ${tone}`}>{children}</span>;
1012
- }
1013
-
1014
- function updateFile(setter: (file: File | undefined) => void, event: ChangeEvent<HTMLInputElement>) {
1015
- setter(event.currentTarget.files?.[0]);
1016
- }
1017
-
1018
- function appleTeamSelectionId(team?: AppleDeveloperPortalTeam) {
1019
- const value = team?.teamId ?? team?.providerId ?? team?.publicProviderId;
1020
- return value === undefined || value === '' ? undefined : String(value);
1021
- }
1022
-
1023
- function appIdIdentifier(appId?: AppleDeveloperPortalAppID) {
1024
- return appId?.appIdId ?? appId?.appId ?? appId?.identifier ?? appId?.bundleId;
1025
- }
1026
-
1027
- function appIdBundleId(appId?: AppleDeveloperPortalAppID) {
1028
- return appId?.identifier ?? appId?.bundleId;
1029
- }
1030
-
1031
- function stringField(record: Record<string, unknown> | undefined, key: string) {
1032
- const value = record?.[key];
1033
- if (typeof value === 'string') return value;
1034
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
1035
- return undefined;
1036
- }
1037
-
1038
- function sameUDID(left?: string, right?: string) {
1039
- return normalizeUDID(left) === normalizeUDID(right);
1040
- }
1041
-
1042
- function normalizeUDID(udid?: string) {
1043
- return (udid ?? '')
1044
- .replace(/-/g, '')
1045
- .replace(/[^a-fA-F0-9]/g, '')
1046
- .toUpperCase();
1047
- }
1048
-
1049
- function useLocalStorage(key: string, initialValue: string) {
1050
- const [value, setValue] = useState(() => localStorage.getItem(key) ?? initialValue);
1051
- useEffect(() => {
1052
- localStorage.setItem(key, value);
1053
- }, [key, value]);
1054
- return [value, setValue] as const;
1055
- }
1056
-
1057
- createRoot(document.getElementById('root')!).render(
1058
- <StrictMode>
1059
- <App />
1060
- </StrictMode>,
1061
- );