@limrun/ui 0.13.3-rc.1 → 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 (50) hide show
  1. package/dist/app-store-relay/index.cjs +4 -4
  2. package/dist/app-store-relay/index.js +175 -119
  3. package/dist/app-store-relay/react.cjs +1 -1
  4. package/dist/app-store-relay/react.js +2 -2
  5. package/dist/{browser-storage-BJ__DGJ6.mjs → browser-storage-Cx9eW-bm.mjs} +48 -45
  6. package/dist/browser-storage-jJaIht4K.js +1 -0
  7. package/dist/{client-CnbCWvCs.js → client-ASLtHFLh.js} +1 -1
  8. package/dist/{client-Bk1N3XIF.mjs → client-CGdh8yoJ.mjs} +33 -27
  9. package/dist/core/device-install/apple/provisioning.d.ts +4 -4
  10. package/dist/device-build/index.cjs +1 -1
  11. package/dist/device-build/index.js +5 -5
  12. package/dist/device-build/react.cjs +1 -1
  13. package/dist/device-build/react.js +15 -13
  14. package/dist/device-install/demo/demo.d.ts +0 -0
  15. package/dist/device-install/index.cjs +1 -1
  16. package/dist/device-install/index.d.ts +1 -1
  17. package/dist/device-install/index.js +134 -121
  18. package/dist/device-install/react.cjs +1 -1
  19. package/dist/device-install/react.js +55 -50
  20. package/dist/{index-BXg7HdIs.mjs → index-DFdCvi2_.mjs} +3 -7
  21. package/dist/limbuild-client-CgEncrgx.js +1 -0
  22. package/dist/{limbuild-client-CFJhYsRx.mjs → limbuild-client-D7U7w342.mjs} +4 -2
  23. package/dist/{provisioning-CdseoMJQ.mjs → provisioning-DKbkBO-p.mjs} +11 -11
  24. package/dist/{provisioning-D2ZZQeyX.js → provisioning-Va0PH4Kk.js} +1 -1
  25. package/package.json +1 -1
  26. package/src/app-store-relay/index.test.ts +1 -5
  27. package/src/app-store-relay/index.ts +109 -42
  28. package/src/app-store-relay/react.ts +6 -2
  29. package/src/core/device-install/apple/client.ts +29 -22
  30. package/src/core/device-install/apple/crypto.ts +9 -6
  31. package/src/core/device-install/apple/gsa-srp.ts +3 -7
  32. package/src/core/device-install/apple/provisioning.ts +16 -18
  33. package/src/core/device-install/apple/relay.ts +5 -1
  34. package/src/core/device-install/operations/limbuild-client.ts +8 -1
  35. package/src/core/device-install/operations/operations.ts +3 -1
  36. package/src/core/device-install/operations/relay-client.ts +57 -27
  37. package/src/core/device-install/operations/usbmux.ts +3 -3
  38. package/src/core/device-install/operations/webusb.ts +9 -4
  39. package/src/core/device-install/storage/browser-storage.ts +20 -6
  40. package/src/core/device-install/types.ts +1 -7
  41. package/src/device-build/react.ts +4 -1
  42. package/src/device-install/README.md +423 -0
  43. package/src/device-install/demo/demo.css +493 -0
  44. package/src/device-install/demo/demo.tsx +1069 -0
  45. package/src/device-install/demo/index.html +12 -0
  46. package/src/device-install/index.ts +7 -7
  47. package/src/device-install/react.ts +26 -17
  48. package/src/index.ts +1 -1
  49. package/dist/browser-storage-C1jQLXat.js +0 -1
  50. package/dist/limbuild-client-C_CMNLYV.js +0 -1
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Real Device Install Demo - @limrun/ui</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./demo.tsx"></script>
11
+ </body>
12
+ </html>
@@ -17,11 +17,7 @@ export {
17
17
  type StartInstallRelayOptions,
18
18
  type StartPairingRelayOptions,
19
19
  } from '../core/device-install/operations';
20
- export {
21
- getPairRecord,
22
- normalizeUDID,
23
- putPairRecord,
24
- } from '../core/device-install/storage';
20
+ export { getPairRecord, normalizeUDID, putPairRecord } from '../core/device-install/storage';
25
21
  export type {
26
22
  DeviceHello,
27
23
  DeviceInstallLog,
@@ -36,11 +32,15 @@ export async function requestUSBAccess(options: RequestUSBAccessOptions = {}) {
36
32
  return requestCoreUSBAccess({ log: options.log ?? noopLog });
37
33
  }
38
34
 
39
- export async function pairDevice(options: Omit<StartPairingRelayOptions, 'log'> & { log?: DeviceInstallLog }) {
35
+ export async function pairDevice(
36
+ options: Omit<StartPairingRelayOptions, 'log'> & { log?: DeviceInstallLog },
37
+ ) {
40
38
  return startPairingRelay({ ...options, log: options.log ?? noopLog });
41
39
  }
42
40
 
43
- export async function startDeviceInstall(options: Omit<StartInstallRelayOptions, 'log'> & { log?: DeviceInstallLog }) {
41
+ export async function startDeviceInstall(
42
+ options: Omit<StartInstallRelayOptions, 'log'> & { log?: DeviceInstallLog },
43
+ ) {
44
44
  return startInstallRelay({ ...options, log: options.log ?? noopLog });
45
45
  }
46
46
 
@@ -48,12 +48,18 @@ export function useDeviceInstallRelay({
48
48
  const [pairConfirmationRequired, setPairConfirmationRequired] = useState(false);
49
49
  const relayRef = useRef<RelayClient | undefined>(undefined);
50
50
  const deviceRef = useRef<DeviceRelayTarget | undefined>(undefined);
51
+ // Keep the logger in a ref so callbacks below don't change identity when the
52
+ // consumer passes an unmemoized `log`. Otherwise every render would recreate
53
+ // cleanupDeviceAccess, re-run the unmount effect, and close the USB device
54
+ // mid-claim (surfacing as "Unable to claim interface" / "operation in progress").
55
+ const logRef = useRef(log);
56
+ logRef.current = log;
51
57
 
52
58
  const cleanupDeviceAccess = useCallback(async () => {
53
59
  relayRef.current?.close();
54
60
  relayRef.current = undefined;
55
- await closeDeviceRelayTarget(deviceRef.current, log);
56
- }, [log]);
61
+ await closeDeviceRelayTarget(deviceRef.current, logRef.current);
62
+ }, []);
57
63
 
58
64
  useEffect(() => {
59
65
  deviceRef.current = device;
@@ -72,14 +78,17 @@ export function useDeviceInstallRelay({
72
78
  let target: DeviceRelayTarget | undefined;
73
79
  try {
74
80
  await cleanupDeviceAccess();
75
- target = await requestUSBAccess({ log });
81
+ target = await requestUSBAccess({ log: logRef.current });
76
82
  const storedPairRecord = await getPairRecord(target.hello.serialNumber);
77
83
  setDevice(target);
78
84
  setPairRecord(storedPairRecord);
79
- log(storedPairRecord ? 'Pair record found' : 'No pair record found', target.hello.serialNumber);
85
+ logRef.current(
86
+ storedPairRecord ? 'Pair record found' : 'No pair record found',
87
+ target.hello.serialNumber,
88
+ );
80
89
  return target;
81
90
  } catch (caught) {
82
- await closeDeviceRelayTarget(target, log);
91
+ await closeDeviceRelayTarget(target, logRef.current);
83
92
  setDevice(undefined);
84
93
  setPairRecord(undefined);
85
94
  setError(errorMessage(caught));
@@ -87,7 +96,7 @@ export function useDeviceInstallRelay({
87
96
  } finally {
88
97
  setBusyAction(undefined);
89
98
  }
90
- }, [cleanupDeviceAccess, log]);
99
+ }, [cleanupDeviceAccess]);
91
100
 
92
101
  const pairBrowser = useCallback(async () => {
93
102
  if (!apiUrl || !device) {
@@ -101,27 +110,27 @@ export function useDeviceInstallRelay({
101
110
  const result = await pairDevice({
102
111
  limbuildApiUrl: apiUrl,
103
112
  token,
104
- log,
113
+ log: logRef.current,
105
114
  target: device,
106
115
  });
107
116
  const stored = await putPairRecord(result.pairRecord, {
108
117
  productName: device.hello.productName,
109
118
  });
110
119
  result.relay.close();
111
- await closeDeviceRelayTarget(device, log);
120
+ await closeDeviceRelayTarget(device, logRef.current);
112
121
  setPairRecord(stored);
113
122
  setPairConfirmationRequired(false);
114
- log('Device paired', 'The pair record was stored locally in this browser.');
123
+ logRef.current('Device paired', 'The pair record was stored locally in this browser.');
115
124
  return stored;
116
125
  } catch (caught) {
117
- await closeDeviceRelayTarget(device, log);
126
+ await closeDeviceRelayTarget(device, logRef.current);
118
127
  setPairConfirmationRequired(true);
119
128
  setError(errorMessage(caught));
120
129
  return undefined;
121
130
  } finally {
122
131
  setBusyAction(undefined);
123
132
  }
124
- }, [apiUrl, cleanupDeviceAccess, device, log, token]);
133
+ }, [apiUrl, cleanupDeviceAccess, device, token]);
125
134
 
126
135
  const startInstallation = useCallback(async () => {
127
136
  if (!apiUrl || !device || !pairRecord) {
@@ -134,25 +143,25 @@ export function useDeviceInstallRelay({
134
143
  relayRef.current = await startDeviceInstall({
135
144
  limbuildApiUrl: apiUrl,
136
145
  token,
137
- log,
146
+ log: logRef.current,
138
147
  target: device,
139
148
  pairRecord,
140
149
  });
141
- log('Device install started', 'Installation will continue through the connected iPhone.');
150
+ logRef.current('Device install started', 'Installation will continue through the connected iPhone.');
142
151
  return relayRef.current;
143
152
  } catch (caught) {
144
- await closeDeviceRelayTarget(device, log);
153
+ await closeDeviceRelayTarget(device, logRef.current);
145
154
  setError(errorMessage(caught));
146
155
  return undefined;
147
156
  } finally {
148
157
  setBusyAction(undefined);
149
158
  }
150
- }, [apiUrl, cleanupDeviceAccess, device, log, pairRecord, token]);
159
+ }, [apiUrl, cleanupDeviceAccess, device, pairRecord, token]);
151
160
 
152
161
  const stopRelay = useCallback(() => {
153
162
  void cleanupDeviceAccess();
154
- log('Device relay stopped');
155
- }, [cleanupDeviceAccess, log]);
163
+ logRef.current('Device relay stopped');
164
+ }, [cleanupDeviceAccess]);
156
165
 
157
166
  return {
158
167
  device,
package/src/index.ts CHANGED
@@ -16,4 +16,4 @@ export {
16
16
  normalizeAndroidTree,
17
17
  normalizeIosTree,
18
18
  AX_UNAVAILABLE_ERROR,
19
- } from './core/ax-tree';
19
+ } from './core/ax-tree';
@@ -1 +0,0 @@
1
- "use strict";const b="limbuild-device-pairing";const l="pairRecords",m="limbuild-device-signing";const c="signingAssets";function d(e){return(e??"").replace(/-/g,"").replace(/[^a-fA-F0-9]/g,"")}function u(e){return(e??"").trim()}async function h(e){const n=d(e);if(!n)return;const r=await g(b,1,l,"udid");return p(r.transaction(l,"readonly").objectStore(l).get(n))}async function E(e,n={}){const r=d(e.udid);if(!r)throw new Error("Cannot store pair record without a UDID.");const t={...e,udid:r,productName:n.productName,updatedAt:new Date().toISOString()},i=await g(b,1,l,"udid");return await p(i.transaction(l,"readwrite").objectStore(l).put(t)),t}async function x({deviceUDID:e,bundleID:n,signingMode:r="development"}){const t=u(n);if(!t)return;const i=d(e),s=await v(I("bundle",t,r));if(s)return s;if(i){const o=await v(I(i,t,r));if(o)return o}return(await O(t,r))[0]}async function _(e="development"){return(await A()).filter(r=>(r.signingMode??"development")===e).sort((r,t)=>new Date(t.updatedAt).getTime()-new Date(r.updatedAt).getTime())[0]}async function R(e,n="development"){return(await A()).filter(t=>!t.certificateID||!t.certificateP12Base64||(t.signingMode??"development")!==n?!1:!e||!t.teamID||t.teamID===e).sort((t,i)=>new Date(i.updatedAt).getTime()-new Date(t.updatedAt).getTime())[0]}async function G(e){const n=u(e.bundleID);if(!n)throw new Error("Cannot store signing assets without a bundle ID.");const r=d(e.deviceUDID),t=e.signingMode??"development",i=I("bundle",n,t),s={...e,id:i,deviceUDID:r||void 0,signingMode:t,bundleID:n,updatedAt:new Date().toISOString()},a=await g(m,1,c,"id");return await p(a.transaction(c,"readwrite").objectStore(c).put(s)),s}async function O(e,n="development"){const r=u(e);return r?(await A()).filter(i=>i.bundleID===r&&(i.signingMode??"development")===n):[]}function z(e,n){const r=d(n);return!!r&&e.provisionedDevices.some(t=>d(t)===r)}function U(e,n){const r=u(n),t=u(e.bundleID);if(!r||!t)return!1;if(t===r||t==="*")return!0;if(!t.endsWith(".*"))return!1;const i=t.slice(0,-1);return r.startsWith(i)}async function V(e){return w(new Uint8Array(await e.arrayBuffer()))}function C(e){const n=atob(e),r=new Uint8Array(n.length);for(let t=0;t<n.length;t+=1)r[t]=n.charCodeAt(t);return w(r)}function w(e){const n=new TextDecoder("latin1").decode(e),r=n.indexOf("<?xml"),t=n.indexOf("</plist>");if(r<0||t<r)throw new Error("Provisioning profile plist not found.");const i=n.slice(r,t+8),s=new DOMParser().parseFromString(i,"application/xml");if(s.querySelector("parsererror"))throw new Error("Provisioning profile plist could not be parsed.");const a=s.querySelector("plist > dict");if(!a)throw new Error("Provisioning profile plist dictionary not found.");const o=S(a);if(!P(o))throw new Error("Provisioning profile plist has an unexpected shape.");const D=P(o.Entitlements)?o.Entitlements:{},y=f(D["application-identifier"]),B=M(y);return{name:f(o.Name),uuid:f(o.UUID),teamID:f(D["com.apple.developer.team-identifier"])??N(o.TeamIdentifier)[0],applicationIdentifier:y,bundleID:B,provisionedDevices:N(o.ProvisionedDevices),getTaskAllow:j(D["get-task-allow"]),expirationDate:f(o.ExpirationDate)}}async function v(e){if(!e)return;const n=await g(m,1,c,"id");return p(n.transaction(c,"readonly").objectStore(c).get(e))}async function A(){const e=await g(m,1,c,"id");return p(e.transaction(c,"readonly").objectStore(c).getAll())}function I(e,n,r){return`${r}:${e}:${n}`}function M(e){if(!e)return;const n=e.indexOf(".");return n>=0?e.slice(n+1):void 0}function S(e){switch(e.tagName){case"dict":return T(e);case"array":return Array.from(e.children).map(S);case"string":case"date":return e.textContent??"";default:return e.textContent??""}}function T(e){const n={},r=Array.from(e.children);for(let t=0;t<r.length;t+=2){const i=r[t],s=r[t+1];!i||i.tagName!=="key"||!s||(n[i.textContent??""]=S(s))}return n}function f(e){return typeof e=="string"&&e?e:void 0}function N(e){return Array.isArray(e)?e.filter(n=>typeof n=="string"):[]}function j(e){return typeof e=="boolean"?e:void 0}function P(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function g(e,n,r,t){return new Promise((i,s)=>{const a=indexedDB.open(e,n);a.onupgradeneeded=()=>{const o=a.result;o.objectStoreNames.contains(r)||o.createObjectStore(r,{keyPath:t})},a.onsuccess=()=>i(a.result),a.onerror=()=>s(a.error??new Error("Open IndexedDB failed"))})}function p(e){return new Promise((n,r)=>{e.onsuccess=()=>n(e.result),e.onerror=()=>r(e.error??new Error("IndexedDB request failed"))})}exports.getLatestSigningAssets=_;exports.getLatestSigningAssetsWithCertificate=R;exports.getPairRecord=h;exports.getSigningAssets=x;exports.normalizeBundleID=u;exports.normalizeUDID=d;exports.parseProvisioningProfile=V;exports.parseProvisioningProfileBase64=C;exports.parseProvisioningProfileBytes=w;exports.profileContainsDevice=z;exports.profileMatchesBundleID=U;exports.putPairRecord=E;exports.putSigningAssets=G;
@@ -1 +0,0 @@
1
- "use strict";async function u(o,e){const a=new URL(`${o}/info`);e&&a.searchParams.set("token",e);const t=await fetch(a.toString(),{headers:e?{Authorization:`Bearer ${e}`}:void 0});if(!t.ok){const n=await t.text();throw new Error(`Info request failed: HTTP ${t.status} ${n}`)}return await t.json()}async function l({limbuildApiUrl:o,token:e,certificateP12Base64:a,certificatePassword:t,provisioningProfileBase64:n}){const d=new URL(`${o}/exec`);e&&d.searchParams.set("token",e);const r=await fetch(d.toString(),{method:"POST",headers:{"Content-Type":"application/json",...e?{Authorization:`Bearer ${e}`}:{}},body:JSON.stringify({command:"xcodebuild",xcodebuild:{sdk:"iphoneos"},signing:{certificateP12Base64:a,...t?{certificatePassword:t}:{},provisioningProfileBase64:n}})});if(!r.ok){const s=await r.text();throw new Error(`Build request failed: HTTP ${r.status} ${s}`)}return await r.json()}async function f({limbuildApiUrl:o,execId:e,token:a}){const t=new URL(`${o}/exec/${encodeURIComponent(e)}/ios/ota`);a&&t.searchParams.set("token",a);const n=await fetch(t.toString(),{headers:a?{Authorization:`Bearer ${a}`}:void 0});if(!n.ok){const d=await n.text();throw new Error(`OTA install metadata failed: HTTP ${n.status} ${d}`)}return await n.json()}function h({limbuildApiUrl:o,execId:e,token:a,onLine:t,onStatus:n,onError:d}){const r=new URL(`${o}/exec/${e}/events`);a&&r.searchParams.set("token",a);const s=new EventSource(r.toString());return n("running"),s.addEventListener("command",i=>t({type:"command",data:i.data})),s.addEventListener("stdout",i=>t({type:"stdout",data:i.data})),s.addEventListener("stderr",i=>t({type:"stderr",data:i.data})),s.addEventListener("exitCode",i=>{const c=parseInt(i.data,10);n(c===0?"succeeded":c<0?"cancelled":"failed"),s.close()}),s.onerror=()=>{s.close(),d?.(new Error("Build log stream closed before completion."))},()=>s.close()}exports.fetchLimbuildInfo=u;exports.getIOSOTAInstall=f;exports.startSignedDeviceBuild=l;exports.watchBuildLogEvents=h;