@limrun/ui 0.9.0-rc.9 → 0.13.3

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 (80) hide show
  1. package/README.md +25 -8
  2. package/dist/app-store-relay/index.cjs +9 -0
  3. package/dist/app-store-relay/index.d.ts +102 -0
  4. package/dist/app-store-relay/index.js +486 -0
  5. package/dist/app-store-relay/react.cjs +1 -0
  6. package/dist/app-store-relay/react.d.ts +15 -0
  7. package/dist/app-store-relay/react.js +65 -0
  8. package/dist/browser-storage-Cx9eW-bm.mjs +205 -0
  9. package/dist/browser-storage-jJaIht4K.js +1 -0
  10. package/dist/client-ASLtHFLh.js +1 -0
  11. package/dist/client-CGdh8yoJ.mjs +234 -0
  12. package/dist/components/device-install/index.d.ts +1 -2
  13. package/dist/core/device-install/apple/provisioning.d.ts +58 -7
  14. package/dist/core/device-install/operations/limbuild-client.d.ts +15 -1
  15. package/dist/core/device-install/storage/browser-storage.d.ts +9 -5
  16. package/dist/core/device-install/types.d.ts +4 -1
  17. package/dist/device-build/index.cjs +1 -0
  18. package/dist/device-build/index.d.ts +4 -0
  19. package/dist/device-build/index.js +84 -0
  20. package/dist/device-build/react.cjs +1 -0
  21. package/dist/device-build/react.d.ts +20 -0
  22. package/dist/device-build/react.js +68 -0
  23. package/dist/device-build/signing.d.ts +20 -0
  24. package/dist/device-install/demo/demo.d.ts +0 -0
  25. package/dist/device-install/index.cjs +1 -1
  26. package/dist/device-install/index.d.ts +18 -3
  27. package/dist/device-install/index.js +583 -76
  28. package/dist/device-install/react.cjs +1 -1
  29. package/dist/device-install/react.d.ts +23 -1
  30. package/dist/device-install/react.js +98 -2
  31. package/dist/hooks/index.d.ts +1 -1
  32. package/dist/index-CMeQfhYy.js +22 -0
  33. package/dist/index-DFdCvi2_.mjs +11543 -0
  34. package/dist/index.cjs +1 -1
  35. package/dist/index.d.ts +0 -2
  36. package/dist/index.js +502 -495
  37. package/dist/limbuild-client-CgEncrgx.js +1 -0
  38. package/dist/limbuild-client-D7U7w342.mjs +81 -0
  39. package/dist/provisioning-DKbkBO-p.mjs +239 -0
  40. package/dist/provisioning-Va0PH4Kk.js +1 -0
  41. package/package.json +21 -1
  42. package/src/app-store-relay/index.test.ts +70 -0
  43. package/src/app-store-relay/index.ts +514 -0
  44. package/src/app-store-relay/react.ts +129 -0
  45. package/src/components/device-install/index.ts +1 -2
  46. package/src/core/device-install/apple/client.ts +29 -22
  47. package/src/core/device-install/apple/crypto.ts +9 -6
  48. package/src/core/device-install/apple/gsa-srp.ts +3 -7
  49. package/src/core/device-install/apple/provisioning.test.ts +84 -0
  50. package/src/core/device-install/apple/provisioning.ts +98 -16
  51. package/src/core/device-install/apple/relay.ts +5 -1
  52. package/src/core/device-install/operations/limbuild-client.ts +41 -4
  53. package/src/core/device-install/operations/operations.ts +3 -1
  54. package/src/core/device-install/operations/relay-client.ts +57 -27
  55. package/src/core/device-install/operations/usbmux.ts +3 -3
  56. package/src/core/device-install/operations/webusb.ts +9 -4
  57. package/src/core/device-install/storage/browser-storage.ts +43 -14
  58. package/src/core/device-install/types.ts +6 -8
  59. package/src/device-build/index.ts +42 -0
  60. package/src/device-build/react.ts +131 -0
  61. package/src/device-build/signing.ts +94 -0
  62. package/src/device-install/README.md +423 -0
  63. package/src/device-install/demo/demo.css +493 -0
  64. package/src/device-install/demo/demo.tsx +1069 -0
  65. package/src/device-install/demo/index.html +12 -0
  66. package/src/device-install/index.ts +49 -3
  67. package/src/device-install/react.ts +189 -1
  68. package/src/hooks/index.ts +1 -1
  69. package/src/index.ts +0 -3
  70. package/vite.config.ts +4 -0
  71. package/dist/components/device-install/device-install-dialog.d.ts +0 -5
  72. package/dist/device-install-dialog-CqxDEH85.mjs +0 -443
  73. package/dist/device-install-dialog-jvqQ-fuo.js +0 -2
  74. package/dist/device-install-dialog.css +0 -1
  75. package/dist/hooks/use-device-install.d.ts +0 -73
  76. package/dist/use-device-install-CAPli9MR.js +0 -31
  77. package/dist/use-device-install-H8dqqvbR.mjs +0 -13624
  78. package/src/components/device-install/device-install-dialog.css +0 -325
  79. package/src/components/device-install/device-install-dialog.tsx +0 -495
  80. package/src/hooks/use-device-install.ts +0 -1219
@@ -0,0 +1,423 @@
1
+ # Install to a real iPhone over WebUSB
2
+
3
+ Build a signed iOS app on Limrun and install it onto a physical iPhone that's
4
+ plugged into the user's own computer — straight from the browser, no Mac and no
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
7
+ USB traffic over a WebSocket.
8
+
9
+ `@limrun/ui` ships the browser primitives and React hooks; your app owns the UI.
10
+ There is no prebuilt wizard, so you decide how much of the Apple Developer flow
11
+ to expose and how the steps look in your product.
12
+
13
+ The flow:
14
+
15
+ 1. **Provision** an Xcode build sandbox and get its `apiUrl` + `token`.
16
+ 2. **Pair** the iPhone over WebUSB (the user taps _Trust_ once).
17
+ 3. **Get signing assets** — upload a `.p12` + `.mobileprovision`, or sign in with
18
+ an Apple ID and let Limrun fetch them.
19
+ 4. **Build** a signed `iphoneos` IPA on the sandbox; logs stream back.
20
+ 5. **Install** the IPA onto the paired iPhone over the WebUSB relay.
21
+
22
+ > **Want to see it working first?** A complete, runnable reference app — with the
23
+ > upload and Apple Developer signing flows, a status stepper, and error handling —
24
+ > lives in [`src/device-install/demo`](./demo). Read its
25
+ > [`demo.tsx`](./demo/demo.tsx) end to end to see how every hook and helper in this
26
+ > guide fits together, or run it locally (see [Run the demo](#run-the-demo)).
27
+
28
+ ## Requirements
29
+
30
+ - A **Chromium** browser (Chrome or Edge). WebUSB is not available in Safari or
31
+ Firefox.
32
+ - A **secure context** — your app must be served over `https://` or `localhost`.
33
+ - A physical iPhone connected over USB; the user unlocks it and taps **Trust**
34
+ during pairing.
35
+ - An Apple signing identity: a development `.p12` (with its private key) and a
36
+ `.mobileprovision` that covers the app's bundle ID **and** the target device's
37
+ UDID. See [Apple signing](#get-signing-assets).
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ npm install @limrun/ui
43
+ ```
44
+
45
+ The feature is split across three subpath entry points so you only pull in what
46
+ you use:
47
+
48
+ | Import | Provides |
49
+ | --------------------------------------- | --------------------------------------------------------------------------------------------------- |
50
+ | `@limrun/ui/device-install` + `/react` | WebUSB device selection, pairing, install relay, pair-record storage (`useDeviceInstallRelay`). |
51
+ | `@limrun/ui/device-build` + `/react` | Signing-asset import/validation, signed-build trigger, log streaming (`useDeviceBuild`). |
52
+ | `@limrun/ui/app-store-relay` + `/react` | Apple ID login (SRP + 2FA) and Apple Developer Portal calls (`useAppleIDLogin`, `list*`/`create*`). |
53
+
54
+ ## Get a build sandbox
55
+
56
+ Every call below takes an `apiUrl` and `token` that point at a Limrun Xcode
57
+ sandbox. Provision one with the `@limrun/api` SDK or the CLI, then read them off
58
+ the instance status — see [Build with remote Xcode](https://docs.limrun.com/docs/ios/build-with-xcode).
59
+
60
+ ```ts
61
+ import Limrun from '@limrun/api';
62
+
63
+ const lim = new Limrun({ apiKey: process.env['LIM_API_KEY'] });
64
+ const xcode = await lim.xcodeInstances.create({ wait: true, reuseIfExists: true });
65
+
66
+ const apiUrl = xcode.status.apiUrl; // HTTP base for the sandbox
67
+ const token = xcode.status.token; // per-instance bearer
68
+ ```
69
+
70
+ The sandbox must have your project **synced** before a build runs (`lim xcode build .`
71
+ or `lim xcode sync .`). A build against an empty sandbox returns
72
+ `no synced folder found; call /sync first`.
73
+
74
+ The `token` is an **instance-scoped** bearer, not your org API key. It's safe to hand to the browser for this one sandbox; it can't touch the rest of your account.
75
+
76
+ ## Pair the iPhone
77
+
78
+ `useDeviceInstallRelay` drives the WebUSB side. `requestUSBAccess` opens the
79
+ browser's device picker; `pairBrowser` runs the pairing handshake through the
80
+ relay and stores the resulting pair record in the browser's IndexedDB, so the
81
+ user only taps **Trust** once per device.
82
+
83
+ ```tsx
84
+ import { useDeviceInstallRelay } from '@limrun/ui/device-install/react';
85
+
86
+ const install = useDeviceInstallRelay({ apiUrl, token, log: (m, d) => console.log(m, d) });
87
+
88
+ // 1. Pick the iPhone (shows Chrome's WebUSB chooser).
89
+ await install.requestUSBAccess();
90
+ // install.device?.hello.serialNumber is the UDID.
91
+
92
+ // 2. Pair (user unlocks + taps Trust). Stored for next time.
93
+ await install.pairBrowser();
94
+ // install.hasPairRecord === true
95
+ ```
96
+
97
+ | Field / method | Use |
98
+ | ------------------------------------------ | ------------------------------------------------------------------ |
99
+ | `requestUSBAccess()` | Open the WebUSB picker and select the iPhone. |
100
+ | `pairBrowser()` | Pair through the relay; persists the pair record. |
101
+ | `startInstallation()` | Install the latest signed build onto the paired device. |
102
+ | `device` | Selected device; `device.hello.serialNumber` is the UDID. |
103
+ | `hasPairRecord` / `canPair` / `canInstall` | Gating flags for your buttons. |
104
+ | `busyAction` | `'usb'`, `'pair'`, or `'install'` while an operation is in flight. |
105
+ | `error` | Last error message, if any. |
106
+
107
+ Pairing is independent of the build — you can pair before signing or building.
108
+ Only `startInstallation()` needs both a stored pair record and a successful
109
+ build.
110
+
111
+ ## Get signing assets
112
+
113
+ A real-device install must be signed. You produce a `StoredSigningAssets` object
114
+ (persisted in IndexedDB) one of two ways, then hand it to the build step.
115
+
116
+ ### Option A — upload files (simplest)
117
+
118
+ Ask the user for a `.p12` (with its private key), its password, and a
119
+ `.mobileprovision`. `importSigningAssetsFromFiles` validates the profile against
120
+ the bundle ID and device, then stores it.
121
+
122
+ ```tsx
123
+ import { importSigningAssetsFromFiles } from '@limrun/ui/device-build';
124
+
125
+ const assets = await importSigningAssetsFromFiles({
126
+ certificateFile, // File (.p12)
127
+ provisioningProfileFile, // File (.mobileprovision)
128
+ certificatePassword,
129
+ bundleId, // e.g. 'com.example.MyApp'
130
+ deviceUDID: install.device?.hello.serialNumber,
131
+ signingMode: 'development',
132
+ });
133
+ ```
134
+
135
+ ### Option B — sign in with Apple ID
136
+
137
+ For a guided experience with no files to find, use the App Store Connect relay.
138
+ The user's password never leaves the browser: the SRP proof is computed
139
+ client-side and the relay only forwards Apple's auth and provisioning requests.
140
+
141
+ ```tsx
142
+ import { useAppleIDLogin } from '@limrun/ui/app-store-relay/react';
143
+ import {
144
+ registerAppleDevice,
145
+ listAppleCertificates,
146
+ createAppleCertificate,
147
+ downloadAppleCertificate,
148
+ exportAppleCertificateP12,
149
+ generateAppleSigningKeyAndCSR,
150
+ createAppleProfile,
151
+ downloadAppleProfile,
152
+ } from '@limrun/ui/app-store-relay';
153
+ import {
154
+ getLatestSigningAssetsWithCertificate,
155
+ parseProvisioningProfileBase64,
156
+ putAppleGeneratedSigningAssets,
157
+ } from '@limrun/ui/device-build';
158
+
159
+ const appleLogin = useAppleIDLogin({ limbuildApiUrl: apiUrl, token });
160
+ const session = await appleLogin.startLogin({ accountName, password });
161
+ if (session?.requiresTwoFactor) await appleLogin.submitTwoFactorCode(code);
162
+
163
+ // Resolve the team id across all three fields Apple may use.
164
+ const teamId = team.teamId ?? String(team.providerId ?? team.publicProviderId ?? '');
165
+ const base = { apiUrl, token, appleSessionId: appleLogin.session!.appleSessionId, teamId };
166
+
167
+ // Register the paired iPhone (no-op if already registered).
168
+ await registerAppleDevice({ ...base, deviceUDID, name: 'My iPhone' });
169
+
170
+ // Reuse a stored cert if its key is in this browser and it's still current;
171
+ // otherwise mint a new one.
172
+ let certificateId: string | undefined;
173
+ let certificateP12Base64: string | undefined;
174
+ const stored = await getLatestSigningAssetsWithCertificate(teamId, 'development');
175
+ if (stored?.certificateID && stored.certificateP12Base64) {
176
+ const current = await listAppleCertificates({ ...base, certificateKind: 'development' });
177
+ const match = current.find(
178
+ (c) => c.certificateId === stored.certificateID || c.certRequestId === stored.certificateID,
179
+ );
180
+ if (match) {
181
+ certificateId = (match.certificateId as string) ?? stored.certificateID; // canonical id
182
+ certificateP12Base64 = stored.certificateP12Base64;
183
+ }
184
+ }
185
+ if (!certificateId) {
186
+ const key = await generateAppleSigningKeyAndCSR({ commonName: 'My App' });
187
+ const created = await createAppleCertificate({
188
+ ...base,
189
+ certificateKind: 'development',
190
+ csrPEM: key.csrPEM,
191
+ });
192
+ certificateId = (created?.certificateId as string) ?? (created?.certRequestId as string);
193
+ const cer = await downloadAppleCertificate({ ...base, certificateKind: 'development', certificateId });
194
+ certificateP12Base64 = exportAppleCertificateP12({
195
+ privateKeyPKCS8Base64: key.privateKeyPKCS8Base64,
196
+ certificateBase64: cer.rawBodyBase64!,
197
+ password: certificatePassword,
198
+ });
199
+ }
200
+
201
+ // Create a profile covering the bundle ID + device (unique name avoids
202
+ // "multiple profiles with the same name"), download it, and store everything.
203
+ const profile = await createAppleProfile({
204
+ ...base,
205
+ profileKind: 'development',
206
+ bundleId,
207
+ appIdId,
208
+ certificateIds: [certificateId],
209
+ deviceIds: [appleDeviceId],
210
+ name: `MyApp ${Date.now()}`,
211
+ });
212
+ const profileId = (profile?.provisioningProfileId as string) ?? (profile?.profileId as string);
213
+ const downloaded = await downloadAppleProfile({ ...base, profileId });
214
+
215
+ const assets = await putAppleGeneratedSigningAssets({
216
+ bundleID: bundleId,
217
+ deviceUDID,
218
+ teamID: teamId,
219
+ signingMode: 'development',
220
+ certificateID: certificateId,
221
+ certificateP12Base64: certificateP12Base64!,
222
+ certificatePassword,
223
+ provisioningProfileBase64: downloaded.rawBodyBase64!,
224
+ profile: parseProvisioningProfileBase64(downloaded.rawBodyBase64!),
225
+ });
226
+ ```
227
+
228
+ These are Apple's rules, not Limrun's, and each one is a common dead end:
229
+
230
+ | Rule | Consequence if ignored |
231
+ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
232
+ | A cert is useless without its private key. | You can only sign with a cert this browser created (and stored) or one imported as `.p12`. Apple never returns private keys. |
233
+ | Max **2** development certs per team. | Creating one per build fails with _"you already have a current Development certificate"_. Reuse, or revoke an old one to free a slot. |
234
+ | Profile creation needs the canonical `certificateId`. | Passing a `certRequestId` fails with _"no current certificates matching the provided certificate IDs"_. Resolve it via `listAppleCertificates`. |
235
+ | A revoked cert still appears in lists. | Signing with it builds fine but the device rejects install with `ApplicationVerificationFailed`. Treat revoked certs as unusable. |
236
+ | The team id may live in `providerId`/`publicProviderId`. | Reading only `teamId` leaves the flow stuck after sign-in. |
237
+ | Profiles are device-scoped and immutable. | To authorize a new device you regenerate the profile (see [Adding a device](#add-a-device-to-a-profile)). |
238
+
239
+ ## Build a signed IPA
240
+
241
+ `useDeviceBuild` triggers a signed `iphoneos` build on the sandbox and streams
242
+ the logs. It builds for the device SDK and signs with the assets you pass.
243
+
244
+ ```tsx
245
+ import { useDeviceBuild } from '@limrun/ui/device-build/react';
246
+
247
+ const build = useDeviceBuild({ apiUrl, token, signingAssets: assets });
248
+
249
+ await build.startBuild({ signingAssets: assets });
250
+ // build.status: 'idle' | 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled'
251
+ // build.logs: BuildLogLine[] (render line.data; xcodebuild lines can be long — wrap them)
252
+ ```
253
+
254
+ | Field / method | Use |
255
+ | ------------------------------- | ------------------------------------------------------------------- |
256
+ | `startBuild({ signingAssets })` | Kick off the signed device build; returns the exec id. |
257
+ | `status` | Build lifecycle state. `succeeded` gates install. |
258
+ | `logs` | Streamed `command` / `stdout` / `stderr` lines. |
259
+ | `error` | Set (and `status` → `failed`) if the build or its log stream fails. |
260
+
261
+ ## Install over WebUSB
262
+
263
+ Once the build succeeds and the device is paired, install:
264
+
265
+ ```tsx
266
+ await install.startInstallation();
267
+ ```
268
+
269
+ Progress streams through your `log` callback. The relay surfaces the device's
270
+ real reason on failure (for example
271
+ `Install error: ApplicationVerificationFailed — The identity used to sign the executable is no longer valid.`),
272
+ so render the log — it's the fastest way to diagnose a signing/profile mismatch.
273
+
274
+ ## Full example
275
+
276
+ A single component covering pair → upload signing → build → install. It assumes
277
+ you already have `apiUrl`, `token`, and a `bundleId`.
278
+
279
+ ```tsx
280
+ import { useState } from 'react';
281
+ import { importSigningAssetsFromFiles, type StoredSigningAssets } from '@limrun/ui/device-build';
282
+ import { useDeviceBuild } from '@limrun/ui/device-build/react';
283
+ import { useDeviceInstallRelay } from '@limrun/ui/device-install/react';
284
+
285
+ export function InstallToIPhone({
286
+ apiUrl,
287
+ token,
288
+ bundleId,
289
+ }: {
290
+ apiUrl: string;
291
+ token?: string;
292
+ bundleId: string;
293
+ }) {
294
+ const [certificateFile, setCertificateFile] = useState<File>();
295
+ const [profileFile, setProfileFile] = useState<File>();
296
+ const [certificatePassword, setCertificatePassword] = useState('');
297
+ const [signingAssets, setSigningAssets] = useState<StoredSigningAssets>();
298
+ const [log, setLog] = useState<string[]>([]);
299
+ const append = (m: string, d?: string) => setLog((l) => [d ? `${m}: ${d}` : m, ...l]);
300
+
301
+ const install = useDeviceInstallRelay({ apiUrl, token, log: append });
302
+ const build = useDeviceBuild({ apiUrl, token, signingAssets });
303
+
304
+ async function prepare() {
305
+ if (!certificateFile || !profileFile) return;
306
+ setSigningAssets(
307
+ await importSigningAssetsFromFiles({
308
+ certificateFile,
309
+ provisioningProfileFile: profileFile,
310
+ certificatePassword,
311
+ bundleId,
312
+ deviceUDID: install.device?.hello.serialNumber,
313
+ signingMode: 'development',
314
+ }),
315
+ );
316
+ }
317
+
318
+ return (
319
+ <div>
320
+ {(install.error || build.error) && <pre>{install.error ?? build.error}</pre>}
321
+
322
+ <button disabled={!!install.busyAction} onClick={() => void install.requestUSBAccess()}>
323
+ Select iPhone
324
+ </button>
325
+ <button disabled={!install.canPair} onClick={() => void install.pairBrowser()}>
326
+ Pair
327
+ </button>
328
+
329
+ <input type="file" accept=".p12" onChange={(e) => setCertificateFile(e.currentTarget.files?.[0])} />
330
+ <input
331
+ type="file"
332
+ accept=".mobileprovision"
333
+ onChange={(e) => setProfileFile(e.currentTarget.files?.[0])}
334
+ />
335
+ <input
336
+ type="password"
337
+ value={certificatePassword}
338
+ onChange={(e) => setCertificatePassword(e.currentTarget.value)}
339
+ />
340
+ <button disabled={!certificateFile || !profileFile} onClick={() => void prepare()}>
341
+ Prepare signing assets
342
+ </button>
343
+
344
+ <button disabled={!signingAssets || build.status === 'running'} onClick={() => void build.startBuild()}>
345
+ Start signed build ({build.status})
346
+ </button>
347
+
348
+ <button
349
+ disabled={!install.canInstall || build.status !== 'succeeded'}
350
+ onClick={() => void install.startInstallation()}
351
+ >
352
+ Install
353
+ </button>
354
+
355
+ <pre>{log.join('\n')}</pre>
356
+ </div>
357
+ );
358
+ }
359
+ ```
360
+
361
+ A more complete, styled reference (with the Apple Developer flow, a status
362
+ stepper, and error handling) lives in
363
+ [`src/device-install/demo`](./demo) in this package — see
364
+ [`demo.tsx`](./demo/demo.tsx) for the full source.
365
+
366
+ ## Run the demo
367
+
368
+ The demo is a standalone Vite page you can run against any Xcode build sandbox:
369
+
370
+ ```bash
371
+ cd packages/ui
372
+ npm install
373
+ npx vite src/device-install/demo
374
+ ```
375
+
376
+ Open the printed `https://localhost` URL in Chrome or Edge, paste your sandbox's
377
+ `apiUrl` and `token`, then walk through pair → sign → build → install. The page
378
+ exercises the same hooks and helpers documented above, so it doubles as a
379
+ copy-paste reference for your own integration.
380
+
381
+ ## Add a device to a profile
382
+
383
+ Provisioning profiles can't be edited in place. To authorize a new iPhone,
384
+ **register the UDID, then regenerate the profile** with it included:
385
+
386
+ 1. `registerAppleDevice({ deviceUDID })` — add the UDID to the team.
387
+ 2. `createAppleProfile({ ..., deviceIds: [...existing, newDeviceId] })` — mint a
388
+ fresh profile (unique `name`).
389
+ 3. `downloadAppleProfile(...)` — fetch the new bytes.
390
+
391
+ This needs an Apple Developer session. A manually uploaded `.mobileprovision`
392
+ that's missing the device can't be amended locally — regenerate it through the
393
+ Apple flow.
394
+
395
+ ## Storage and security
396
+
397
+ - **Pair records** and **signing assets** (`.p12`, `.mobileprovision`, and the
398
+ `.p12` password when given) live in the browser's IndexedDB, written only when
399
+ you call the storage helpers. They stay on the device unless your app moves
400
+ them.
401
+ - The **Apple ID password never reaches Limrun** — only SRP proof material does.
402
+ - The sandbox `token` is instance-scoped; the Apple session is held in the
403
+ browser. Treat any `.p12` and password your app handles as secrets, and serve
404
+ the UI over HTTPS in production.
405
+
406
+ ## Troubleshooting
407
+
408
+ | Symptom | Cause and fix |
409
+ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
410
+ | `WebUSB is not available` | Use Chrome/Edge on HTTPS or `localhost`. Safari/Firefox are unsupported. |
411
+ | `Unable to claim interface` / "operation in progress" | Unplug/replug the iPhone, unlock it, close other WebUSB tabs, retry. Make sure you're on a current `@limrun/ui`. |
412
+ | First transfer fails with _"endpoint is not part of a claimed and selected alternate interface"_ | Update `@limrun/ui` — recent versions explicitly select the usbmux alternate (incl. alt 0). |
413
+ | Pairing seems to hang | The user must unlock the iPhone and tap **Trust**; the first attempt can fail if the prompt isn't accepted in time. Retry. |
414
+ | `no synced folder found; call /sync first` | Sync your project into the sandbox first (`lim xcode build .` / `lim xcode sync .`). |
415
+ | Install fails, log shows `ApplicationVerificationFailed` | Signed with a **revoked/invalid cert**, or the **device isn't in the profile**, or the **bundle ID isn't covered**. `zsign` doesn't rewrite the bundle ID — it must match (or be wildcarded by) the profile. |
416
+ | `no current certificates matching the provided certificate IDs` | You passed a `certRequestId`, or the cert was revoked. Resolve the canonical `certificateId` via `listAppleCertificates`. |
417
+ | `You already have a current Development certificate…` | Apple's cert cap. Reuse a stored cert, upload a `.p12`, or revoke one at developer.apple.com. |
418
+ | `Multiple profiles found with the name '…'` | Give created profiles a unique name. |
419
+ | Apple flow does nothing after sign-in | Resolve the team id across `teamId`/`providerId`/`publicProviderId`. |
420
+
421
+ ## Next steps
422
+
423
+ Provision an Xcode sandbox, sync source, and produce signed IPAs. Drive a simulator for the same app: taps, screenshots, logs, recordings. Build every PR and post a live preview link from a GitHub workflow.