@limrun/ui 0.9.0-rc.5 → 0.9.0-rc.7

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 (73) hide show
  1. package/README.md +9 -0
  2. package/dist/components/device-install/device-install-dialog.d.ts +5 -0
  3. package/dist/components/device-install/index.d.ts +2 -0
  4. package/dist/components/inspect-overlay.d.ts +1 -0
  5. package/dist/components/remote-control.d.ts +13 -2
  6. package/dist/core/ax-tree.d.ts +2 -0
  7. package/dist/core/device-install/apple/client.d.ts +17 -0
  8. package/dist/core/device-install/apple/crypto.d.ts +20 -0
  9. package/dist/core/device-install/apple/gsa-srp.d.ts +26 -0
  10. package/dist/core/device-install/apple/index.d.ts +5 -0
  11. package/dist/core/device-install/apple/provisioning.d.ts +161 -0
  12. package/dist/core/device-install/apple/relay.d.ts +29 -0
  13. package/dist/core/device-install/index.d.ts +4 -0
  14. package/dist/core/device-install/operations/index.d.ts +6 -0
  15. package/dist/core/device-install/operations/limbuild-client.d.ts +28 -0
  16. package/dist/core/device-install/operations/operations.d.ts +32 -0
  17. package/dist/core/device-install/operations/relay-client.d.ts +25 -0
  18. package/dist/core/device-install/operations/relay-protocol.d.ts +27 -0
  19. package/dist/core/device-install/operations/usbmux.d.ts +32 -0
  20. package/dist/core/device-install/operations/webusb.d.ts +21 -0
  21. package/dist/core/device-install/storage/browser-storage.d.ts +44 -0
  22. package/dist/core/device-install/storage/index.d.ts +1 -0
  23. package/dist/core/device-install/types.d.ts +48 -0
  24. package/dist/device-install/index.cjs +1 -0
  25. package/dist/device-install/index.d.ts +3 -0
  26. package/dist/device-install/index.js +78 -0
  27. package/dist/device-install/react.cjs +1 -0
  28. package/dist/device-install/react.d.ts +1 -0
  29. package/dist/device-install/react.js +4 -0
  30. package/dist/device-install-dialog-86RDdoK9.js +2 -0
  31. package/dist/device-install-dialog-CnyDWf0q.mjs +462 -0
  32. package/dist/device-install-dialog.css +1 -0
  33. package/dist/hooks/index.d.ts +1 -0
  34. package/dist/hooks/use-device-install.d.ts +73 -0
  35. package/dist/index.cjs +1 -1
  36. package/dist/index.css +1 -1
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.js +737 -703
  39. package/dist/use-device-install-CbGVvwPp.js +31 -0
  40. package/dist/use-device-install-j1Gekpl4.mjs +13623 -0
  41. package/package.json +15 -2
  42. package/src/components/device-install/device-install-dialog.css +325 -0
  43. package/src/components/device-install/device-install-dialog.tsx +513 -0
  44. package/src/components/device-install/index.ts +2 -0
  45. package/src/components/inspect-overlay.css +6 -0
  46. package/src/components/inspect-overlay.tsx +46 -15
  47. package/src/components/remote-control.tsx +16 -2
  48. package/src/core/ax-tree.test.ts +124 -0
  49. package/src/core/ax-tree.ts +107 -0
  50. package/src/core/device-install/apple/client.ts +152 -0
  51. package/src/core/device-install/apple/crypto.ts +202 -0
  52. package/src/core/device-install/apple/gsa-srp.ts +127 -0
  53. package/src/core/device-install/apple/index.ts +5 -0
  54. package/src/core/device-install/apple/provisioning.ts +298 -0
  55. package/src/core/device-install/apple/relay.ts +221 -0
  56. package/src/core/device-install/index.ts +4 -0
  57. package/src/core/device-install/operations/index.ts +6 -0
  58. package/src/core/device-install/operations/limbuild-client.ts +104 -0
  59. package/src/core/device-install/operations/operations.ts +217 -0
  60. package/src/core/device-install/operations/relay-client.ts +255 -0
  61. package/src/core/device-install/operations/relay-protocol.ts +71 -0
  62. package/src/core/device-install/operations/usbmux.ts +270 -0
  63. package/src/core/device-install/operations/webusb-dom.d.ts +54 -0
  64. package/src/core/device-install/operations/webusb.ts +105 -0
  65. package/src/core/device-install/storage/browser-storage.ts +263 -0
  66. package/src/core/device-install/storage/index.ts +1 -0
  67. package/src/core/device-install/types.ts +65 -0
  68. package/src/device-install/index.ts +3 -0
  69. package/src/device-install/react.ts +1 -0
  70. package/src/hooks/index.ts +1 -0
  71. package/src/hooks/use-device-install.ts +1210 -0
  72. package/src/index.ts +4 -0
  73. package/vite.config.ts +6 -2
@@ -0,0 +1,270 @@
1
+ /// <reference path="./webusb-dom.d.ts" />
2
+
3
+ import type { UsbmuxCandidate } from './webusb';
4
+ import { getBulkEndpoints, transferIn, transferOutWithZlp } from './webusb';
5
+
6
+ const PROTO_VERSION = 0;
7
+ const PROTO_SETUP = 2;
8
+ const PROTO_TCP = 6;
9
+ const MAGIC = 0xfeedface;
10
+ const FLAG_SYN = 0x02;
11
+ const FLAG_RST = 0x04;
12
+ const FLAG_ACK = 0x10;
13
+ const FIRST_SPORT = 49152;
14
+
15
+ export type UsbmuxSession = {
16
+ device: USBDevice;
17
+ candidate: UsbmuxCandidate;
18
+ inEndpoint: ReturnType<typeof getBulkEndpoints>['inEndpoint'];
19
+ outEndpoint: ReturnType<typeof getBulkEndpoints>['outEndpoint'];
20
+ muxVersion: number;
21
+ txSeq: number;
22
+ rxSeq: number;
23
+ nextSport: number;
24
+ streams: Map<string, UsbmuxStream>;
25
+ writeChain: Promise<void>;
26
+ closed: boolean;
27
+ };
28
+
29
+ export type UsbmuxStream = {
30
+ session: UsbmuxSession;
31
+ sport: number;
32
+ dport: number;
33
+ seq: number;
34
+ ack: number;
35
+ queue: Uint8Array[];
36
+ waiters: Array<(value: Uint8Array) => void>;
37
+ error?: Error;
38
+ opened: Promise<void>;
39
+ resolveOpened: () => void;
40
+ rejectOpened: (error: Error) => void;
41
+ };
42
+
43
+ export async function createUsbmuxSession(device: USBDevice, candidate: UsbmuxCandidate) {
44
+ const { inEndpoint, outEndpoint } = getBulkEndpoints(candidate);
45
+ const versionPayload = new Uint8Array(12);
46
+ const versionView = new DataView(versionPayload.buffer);
47
+ versionView.setUint32(0, 2);
48
+ versionView.setUint32(4, 0);
49
+ versionView.setUint32(8, 0);
50
+ await transferOutWithZlp(device, outEndpoint, buildV1Packet(PROTO_VERSION, versionPayload));
51
+ const versionPacket = await readPacket(device, inEndpoint, 1);
52
+ const version = new DataView(versionPacket.payload.buffer, versionPacket.payload.byteOffset).getUint32(0);
53
+ const session: UsbmuxSession = {
54
+ device,
55
+ candidate,
56
+ inEndpoint,
57
+ outEndpoint,
58
+ muxVersion: version,
59
+ txSeq: 0,
60
+ rxSeq: 0xffff,
61
+ nextSport: FIRST_SPORT,
62
+ streams: new Map(),
63
+ writeChain: Promise.resolve(),
64
+ closed: false,
65
+ };
66
+ if (version >= 2) {
67
+ await sendMux(session, PROTO_SETUP, new Uint8Array([0x07]));
68
+ }
69
+ void readLoop(session);
70
+ return session;
71
+ }
72
+
73
+ export async function openStream(session: UsbmuxSession, port: number) {
74
+ if (session.closed) {
75
+ throw new Error('usbmux session is closed.');
76
+ }
77
+ let resolveOpened!: () => void;
78
+ let rejectOpened!: (error: Error) => void;
79
+ const stream: UsbmuxStream = {
80
+ session,
81
+ sport: session.nextSport++,
82
+ dport: port,
83
+ seq: 0,
84
+ ack: 0,
85
+ queue: [],
86
+ waiters: [],
87
+ opened: new Promise<void>((resolve, reject) => {
88
+ resolveOpened = resolve;
89
+ rejectOpened = reject;
90
+ }),
91
+ resolveOpened,
92
+ rejectOpened,
93
+ };
94
+ session.streams.set(streamKey(port, stream.sport), stream);
95
+ await sendTcp(stream, FLAG_SYN, new Uint8Array());
96
+ await stream.opened;
97
+ return stream;
98
+ }
99
+
100
+ export async function sendStreamData(stream: UsbmuxStream, bytes: Uint8Array) {
101
+ if (stream.session.closed) {
102
+ throw new Error('usbmux session is closed.');
103
+ }
104
+ await sendTcp(stream, FLAG_ACK, bytes);
105
+ stream.seq += bytes.byteLength;
106
+ }
107
+
108
+ export async function receiveStreamData(stream: UsbmuxStream) {
109
+ if (stream.queue.length > 0) {
110
+ return stream.queue.shift()!;
111
+ }
112
+ if (stream.error) {
113
+ throw stream.error;
114
+ }
115
+ return new Promise<Uint8Array>((resolve) => {
116
+ stream.waiters.push(resolve);
117
+ });
118
+ }
119
+
120
+ export function closeUsbmuxSession(session: UsbmuxSession) {
121
+ session.closed = true;
122
+ for (const stream of session.streams.values()) {
123
+ stream.error = new Error('usbmux session closed');
124
+ stream.rejectOpened(stream.error);
125
+ while (stream.waiters.length > 0) {
126
+ stream.waiters.shift()!(new Uint8Array());
127
+ }
128
+ }
129
+ session.streams.clear();
130
+ }
131
+
132
+ async function sendTcp(stream: UsbmuxStream, flags: number, payload: Uint8Array) {
133
+ const tcp = new Uint8Array(20 + payload.byteLength);
134
+ const view = new DataView(tcp.buffer);
135
+ view.setUint16(0, stream.sport);
136
+ view.setUint16(2, stream.dport);
137
+ view.setUint32(4, stream.seq);
138
+ view.setUint32(8, stream.ack);
139
+ view.setUint8(12, 0x50);
140
+ view.setUint8(13, flags);
141
+ view.setUint16(14, 512);
142
+ tcp.set(payload, 20);
143
+ await sendMux(stream.session, PROTO_TCP, tcp);
144
+ }
145
+
146
+ async function sendMux(session: UsbmuxSession, protocol: number, payload: Uint8Array) {
147
+ if (session.closed) {
148
+ throw new Error('usbmux session is closed.');
149
+ }
150
+ session.writeChain = session.writeChain.then(async () => {
151
+ if (session.closed) return;
152
+ const packet =
153
+ session.muxVersion >= 2
154
+ ? buildV2Packet(protocol, payload, session.txSeq++, session.rxSeq)
155
+ : buildV1Packet(protocol, payload);
156
+ await transferOutWithZlp(session.device, session.outEndpoint, packet);
157
+ });
158
+ return session.writeChain;
159
+ }
160
+
161
+ async function readLoop(session: UsbmuxSession) {
162
+ try {
163
+ for (;;) {
164
+ if (session.closed) return;
165
+ const packet = await readPacket(session.device, session.inEndpoint, session.muxVersion);
166
+ if (session.closed) return;
167
+ if (packet.rxSeq !== undefined) {
168
+ session.rxSeq = packet.rxSeq;
169
+ }
170
+ if (packet.protocol !== PROTO_TCP) {
171
+ continue;
172
+ }
173
+ const tcp = parseTcp(packet.payload);
174
+ const stream = session.streams.get(streamKey(tcp.sport, tcp.dport));
175
+ if (!stream) {
176
+ continue;
177
+ }
178
+ if (tcp.flags & FLAG_RST) {
179
+ stream.error = new Error(`Device reset stream ${stream.dport}`);
180
+ stream.rejectOpened(stream.error);
181
+ while (stream.waiters.length > 0) {
182
+ stream.waiters.shift()!(new Uint8Array());
183
+ }
184
+ session.streams.delete(streamKey(stream.dport, stream.sport));
185
+ continue;
186
+ }
187
+ if ((tcp.flags & (FLAG_SYN | FLAG_ACK)) === (FLAG_SYN | FLAG_ACK)) {
188
+ stream.seq += 1;
189
+ stream.ack = tcp.seq + 1;
190
+ await sendTcp(stream, FLAG_ACK, new Uint8Array());
191
+ stream.resolveOpened();
192
+ continue;
193
+ }
194
+ if (tcp.payload.byteLength === 0) {
195
+ continue;
196
+ }
197
+ stream.ack = tcp.seq + tcp.payload.byteLength;
198
+ await sendTcp(stream, FLAG_ACK, new Uint8Array());
199
+ if (stream.waiters.length > 0) {
200
+ stream.waiters.shift()!(tcp.payload);
201
+ } else {
202
+ stream.queue.push(tcp.payload);
203
+ }
204
+ }
205
+ } catch (error) {
206
+ if (!session.closed) {
207
+ closeUsbmuxSession(session);
208
+ }
209
+ }
210
+ }
211
+
212
+ async function readPacket(device: USBDevice, endpoint: UsbmuxSession['inEndpoint'], version: number) {
213
+ for (;;) {
214
+ const bytes = await transferIn(device, endpoint);
215
+ const packet = parseMux(bytes, version);
216
+ if (version === 1 && packet.protocol !== PROTO_VERSION) continue;
217
+ return packet;
218
+ }
219
+ }
220
+
221
+ function buildV1Packet(protocol: number, payload: Uint8Array) {
222
+ const bytes = new Uint8Array(8 + payload.byteLength);
223
+ const view = new DataView(bytes.buffer);
224
+ view.setUint32(0, protocol);
225
+ view.setUint32(4, bytes.byteLength);
226
+ bytes.set(payload, 8);
227
+ return bytes;
228
+ }
229
+
230
+ function buildV2Packet(protocol: number, payload: Uint8Array, txSeq: number, rxSeq: number) {
231
+ const bytes = new Uint8Array(16 + payload.byteLength);
232
+ const view = new DataView(bytes.buffer);
233
+ view.setUint32(0, protocol);
234
+ view.setUint32(4, bytes.byteLength);
235
+ view.setUint32(8, MAGIC);
236
+ view.setUint16(12, txSeq);
237
+ view.setUint16(14, rxSeq);
238
+ bytes.set(payload, 16);
239
+ return bytes;
240
+ }
241
+
242
+ function parseMux(bytes: Uint8Array, version: number) {
243
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
244
+ const protocol = view.getUint32(0);
245
+ const length = view.getUint32(4);
246
+ const headerSize = version >= 2 ? 16 : 8;
247
+ return {
248
+ protocol,
249
+ length,
250
+ rxSeq: version >= 2 ? view.getUint16(14) : undefined,
251
+ payload: bytes.slice(headerSize, length),
252
+ };
253
+ }
254
+
255
+ function parseTcp(bytes: Uint8Array) {
256
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
257
+ const dataOffset = (view.getUint8(12) >> 4) * 4;
258
+ return {
259
+ sport: view.getUint16(0),
260
+ dport: view.getUint16(2),
261
+ seq: view.getUint32(4),
262
+ ack: view.getUint32(8),
263
+ flags: view.getUint8(13),
264
+ payload: bytes.slice(dataOffset),
265
+ };
266
+ }
267
+
268
+ function streamKey(devicePort: number, hostPort: number) {
269
+ return `${devicePort}:${hostPort}`;
270
+ }
@@ -0,0 +1,54 @@
1
+ type USBTransferStatus = 'ok' | 'stall' | 'babble';
2
+
3
+ interface USBInTransferResult {
4
+ data?: DataView;
5
+ status: USBTransferStatus;
6
+ }
7
+
8
+ interface USBOutTransferResult {
9
+ bytesWritten: number;
10
+ status: USBTransferStatus;
11
+ }
12
+
13
+ interface USBDevice {
14
+ vendorId: number;
15
+ productId: number;
16
+ productName?: string;
17
+ manufacturerName?: string;
18
+ serialNumber?: string;
19
+ opened: boolean;
20
+ configuration: { configurationValue: number } | null;
21
+ configurations: Array<{
22
+ configurationValue: number;
23
+ interfaces: Array<{
24
+ interfaceNumber: number;
25
+ alternates: Array<{
26
+ alternateSetting: number;
27
+ interfaceClass: number;
28
+ interfaceSubclass: number;
29
+ interfaceProtocol: number;
30
+ endpoints: Array<{
31
+ endpointNumber: number;
32
+ direction: 'in' | 'out';
33
+ type: string;
34
+ packetSize: number;
35
+ }>;
36
+ }>;
37
+ }>;
38
+ }>;
39
+ open(): Promise<void>;
40
+ close(): Promise<void>;
41
+ selectConfiguration(configurationValue: number): Promise<void>;
42
+ claimInterface(interfaceNumber: number): Promise<void>;
43
+ releaseInterface(interfaceNumber: number): Promise<void>;
44
+ selectAlternateInterface(interfaceNumber: number, alternateSetting: number): Promise<void>;
45
+ transferIn(endpointNumber: number, length: number): Promise<USBInTransferResult>;
46
+ transferOut(endpointNumber: number, data: Uint8Array): Promise<USBOutTransferResult>;
47
+ }
48
+
49
+ interface Navigator {
50
+ usb?: {
51
+ requestDevice(options: { filters: Array<{ vendorId: number }> }): Promise<USBDevice>;
52
+ getDevices(): Promise<USBDevice[]>;
53
+ };
54
+ }
@@ -0,0 +1,105 @@
1
+ /// <reference path="./webusb-dom.d.ts" />
2
+
3
+ export type UsbEndpoint = {
4
+ endpointNumber: number;
5
+ direction: 'in' | 'out';
6
+ type: string;
7
+ packetSize: number;
8
+ };
9
+
10
+ export type UsbmuxCandidate = {
11
+ configurationValue: number;
12
+ interfaceNumber: number;
13
+ alternateSetting: number;
14
+ endpoints: UsbEndpoint[];
15
+ };
16
+
17
+ export async function requestAppleDevice() {
18
+ if (!navigator.usb) {
19
+ throw new Error('WebUSB is not available in this browser.');
20
+ }
21
+ return navigator.usb.requestDevice({ filters: [{ vendorId: 0x05ac }] });
22
+ }
23
+
24
+ export function findUsbmuxCandidates(device: USBDevice): UsbmuxCandidate[] {
25
+ const candidates: UsbmuxCandidate[] = [];
26
+ const activeConfigurationValue = device.configuration?.configurationValue;
27
+ const activeConfiguration = device.configurations.find(
28
+ (configuration) => configuration.configurationValue === activeConfigurationValue,
29
+ );
30
+ if (activeConfiguration) {
31
+ candidates.push(...findConfigurationUsbmuxCandidates(activeConfiguration));
32
+ }
33
+ for (const configuration of device.configurations) {
34
+ if (configuration.configurationValue === activeConfigurationValue) {
35
+ continue;
36
+ }
37
+ candidates.push(...findConfigurationUsbmuxCandidates(configuration));
38
+ }
39
+ return candidates;
40
+ }
41
+
42
+ function findConfigurationUsbmuxCandidates(configuration: USBDevice['configurations'][number]): UsbmuxCandidate[] {
43
+ const candidates: UsbmuxCandidate[] = [];
44
+ for (const usbInterface of configuration.interfaces) {
45
+ for (const alternate of usbInterface.alternates) {
46
+ if (
47
+ alternate.interfaceClass === 0xff &&
48
+ alternate.interfaceSubclass === 0xfe &&
49
+ alternate.interfaceProtocol === 0x02
50
+ ) {
51
+ candidates.push({
52
+ configurationValue: configuration.configurationValue,
53
+ interfaceNumber: usbInterface.interfaceNumber,
54
+ alternateSetting: alternate.alternateSetting,
55
+ endpoints: alternate.endpoints,
56
+ });
57
+ }
58
+ }
59
+ }
60
+ return candidates;
61
+ }
62
+
63
+ export async function claimUsbmux(device: USBDevice, candidate: UsbmuxCandidate) {
64
+ if (!device.opened) {
65
+ await device.open();
66
+ }
67
+ if (!device.configuration || device.configuration.configurationValue !== candidate.configurationValue) {
68
+ await device.selectConfiguration(candidate.configurationValue);
69
+ }
70
+ await device.claimInterface(candidate.interfaceNumber);
71
+ if (candidate.alternateSetting !== 0) {
72
+ await device.selectAlternateInterface(candidate.interfaceNumber, candidate.alternateSetting);
73
+ }
74
+ }
75
+
76
+ export function getBulkEndpoints(candidate: UsbmuxCandidate) {
77
+ const outEndpoint = candidate.endpoints.find(
78
+ (endpoint) => endpoint.direction === 'out' && endpoint.type === 'bulk',
79
+ );
80
+ const inEndpoint = candidate.endpoints.find(
81
+ (endpoint) => endpoint.direction === 'in' && endpoint.type === 'bulk',
82
+ );
83
+ if (!outEndpoint || !inEndpoint) {
84
+ throw new Error('Could not find usbmux bulk endpoints.');
85
+ }
86
+ return { outEndpoint, inEndpoint };
87
+ }
88
+
89
+ export async function transferOutWithZlp(device: USBDevice, endpoint: UsbEndpoint, bytes: Uint8Array) {
90
+ const result = await device.transferOut(endpoint.endpointNumber, bytes);
91
+ if (result.status !== 'ok') {
92
+ throw new Error(`USB transferOut failed: ${result.status}`);
93
+ }
94
+ if (bytes.byteLength % endpoint.packetSize === 0) {
95
+ await device.transferOut(endpoint.endpointNumber, new Uint8Array());
96
+ }
97
+ }
98
+
99
+ export async function transferIn(device: USBDevice, endpoint: UsbEndpoint, size = 16384) {
100
+ const result = await device.transferIn(endpoint.endpointNumber, size);
101
+ if (result.status !== 'ok' || !result.data) {
102
+ throw new Error(`USB transferIn failed: ${result.status}`);
103
+ }
104
+ return new Uint8Array(result.data.buffer, result.data.byteOffset, result.data.byteLength);
105
+ }
@@ -0,0 +1,263 @@
1
+ import type {
2
+ ProvisioningProfileInfo,
3
+ PutSigningAssetsInput,
4
+ StoredPairRecord,
5
+ StoredSigningAssets,
6
+ } from '../types';
7
+ import type { PairRecordPayload } from '../types';
8
+
9
+ const PAIRING_DB_NAME = 'limbuild-device-pairing';
10
+ const PAIRING_DB_VERSION = 1;
11
+ const PAIRING_STORE_NAME = 'pairRecords';
12
+ const SIGNING_DB_NAME = 'limbuild-device-signing';
13
+ const SIGNING_DB_VERSION = 1;
14
+ const SIGNING_STORE_NAME = 'signingAssets';
15
+
16
+ export function normalizeUDID(udid?: string) {
17
+ return (udid ?? '').replace(/-/g, '').replace(/[^a-fA-F0-9]/g, '');
18
+ }
19
+
20
+ export function normalizeBundleID(bundleID?: string) {
21
+ return (bundleID ?? '').trim();
22
+ }
23
+
24
+ export async function getPairRecord(udid?: string) {
25
+ const normalized = normalizeUDID(udid);
26
+ if (!normalized) return undefined;
27
+ const db = await openDB(PAIRING_DB_NAME, PAIRING_DB_VERSION, PAIRING_STORE_NAME, 'udid');
28
+ return requestToPromise<StoredPairRecord | undefined>(
29
+ db.transaction(PAIRING_STORE_NAME, 'readonly').objectStore(PAIRING_STORE_NAME).get(normalized),
30
+ );
31
+ }
32
+
33
+ export async function putPairRecord(record: PairRecordPayload, metadata: { productName?: string } = {}) {
34
+ const normalized = normalizeUDID(record.udid);
35
+ if (!normalized) throw new Error('Cannot store pair record without a UDID.');
36
+ const stored: StoredPairRecord = {
37
+ ...record,
38
+ udid: normalized,
39
+ productName: metadata.productName,
40
+ updatedAt: new Date().toISOString(),
41
+ };
42
+ const db = await openDB(PAIRING_DB_NAME, PAIRING_DB_VERSION, PAIRING_STORE_NAME, 'udid');
43
+ await requestToPromise(
44
+ db.transaction(PAIRING_STORE_NAME, 'readwrite').objectStore(PAIRING_STORE_NAME).put(stored),
45
+ );
46
+ return stored;
47
+ }
48
+
49
+ export async function getSigningAssets({
50
+ deviceUDID,
51
+ bundleID,
52
+ }: {
53
+ deviceUDID?: string;
54
+ bundleID?: string;
55
+ }) {
56
+ const normalizedBundleID = normalizeBundleID(bundleID);
57
+ if (!normalizedBundleID) return undefined;
58
+ const normalizedUDID = normalizeUDID(deviceUDID);
59
+ const bundleScoped = await getSigningAssetsByID(signingAssetID('bundle', normalizedBundleID));
60
+ if (bundleScoped) return bundleScoped;
61
+ if (normalizedUDID) {
62
+ const exact = await getSigningAssetsByID(signingAssetID(normalizedUDID, normalizedBundleID));
63
+ if (exact) return exact;
64
+ }
65
+ const candidates = await findSigningAssetsForBundle(normalizedBundleID);
66
+ return candidates[0];
67
+ }
68
+
69
+ export async function getLatestSigningAssets() {
70
+ const all = await getAllSigningAssets();
71
+ return all.sort(
72
+ (left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime(),
73
+ )[0];
74
+ }
75
+
76
+ export async function getLatestSigningAssetsWithCertificate(teamID?: string) {
77
+ const all = await getAllSigningAssets();
78
+ return all
79
+ .filter((asset) => {
80
+ if (!asset.certificateID || !asset.certificateP12Base64 || !asset.certificatePassword) {
81
+ return false;
82
+ }
83
+ return !teamID || !asset.teamID || asset.teamID === teamID;
84
+ })
85
+ .sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime())[0];
86
+ }
87
+
88
+ export async function putSigningAssets(input: PutSigningAssetsInput) {
89
+ const normalizedBundleID = normalizeBundleID(input.bundleID);
90
+ if (!normalizedBundleID) {
91
+ throw new Error('Cannot store signing assets without a bundle ID.');
92
+ }
93
+ const normalizedUDID = normalizeUDID(input.deviceUDID);
94
+ const id = signingAssetID('bundle', normalizedBundleID);
95
+ const stored: StoredSigningAssets = {
96
+ ...input,
97
+ id,
98
+ deviceUDID: normalizedUDID || undefined,
99
+ bundleID: normalizedBundleID,
100
+ updatedAt: new Date().toISOString(),
101
+ };
102
+ const db = await openDB(SIGNING_DB_NAME, SIGNING_DB_VERSION, SIGNING_STORE_NAME, 'id');
103
+ await requestToPromise(
104
+ db.transaction(SIGNING_STORE_NAME, 'readwrite').objectStore(SIGNING_STORE_NAME).put(stored),
105
+ );
106
+ return stored;
107
+ }
108
+
109
+ export async function findSigningAssetsForBundle(bundleID?: string) {
110
+ const normalized = normalizeBundleID(bundleID);
111
+ if (!normalized) return [];
112
+ const all = await getAllSigningAssets();
113
+ return all.filter((asset) => asset.bundleID === normalized);
114
+ }
115
+
116
+ export function profileContainsDevice(profile: ProvisioningProfileInfo, deviceUDID?: string) {
117
+ const normalized = normalizeUDID(deviceUDID);
118
+ return !!normalized && profile.provisionedDevices.some((device) => normalizeUDID(device) === normalized);
119
+ }
120
+
121
+ export function profileMatchesBundleID(profile: ProvisioningProfileInfo, bundleID?: string) {
122
+ const expected = normalizeBundleID(bundleID);
123
+ const profileBundleID = normalizeBundleID(profile.bundleID);
124
+ if (!expected || !profileBundleID) return false;
125
+ if (profileBundleID === expected) return true;
126
+ if (profileBundleID === '*') return true;
127
+ if (!profileBundleID.endsWith('.*')) return false;
128
+ const prefix = profileBundleID.slice(0, -1);
129
+ return expected.startsWith(prefix);
130
+ }
131
+
132
+ export async function parseProvisioningProfile(file: File) {
133
+ return parseProvisioningProfileBytes(new Uint8Array(await file.arrayBuffer()));
134
+ }
135
+
136
+ export function parseProvisioningProfileBase64(base64: string) {
137
+ const binary = atob(base64);
138
+ const bytes = new Uint8Array(binary.length);
139
+ for (let index = 0; index < binary.length; index += 1) {
140
+ bytes[index] = binary.charCodeAt(index);
141
+ }
142
+ return parseProvisioningProfileBytes(bytes);
143
+ }
144
+
145
+ export function parseProvisioningProfileBytes(bytes: Uint8Array) {
146
+ const text = new TextDecoder('latin1').decode(bytes);
147
+ const start = text.indexOf('<?xml');
148
+ const end = text.indexOf('</plist>');
149
+ if (start < 0 || end < start) {
150
+ throw new Error('Provisioning profile plist not found.');
151
+ }
152
+ const xml = text.slice(start, end + '</plist>'.length);
153
+ const doc = new DOMParser().parseFromString(xml, 'application/xml');
154
+ if (doc.querySelector('parsererror')) {
155
+ throw new Error('Provisioning profile plist could not be parsed.');
156
+ }
157
+ const dict = doc.querySelector('plist > dict');
158
+ if (!dict) {
159
+ throw new Error('Provisioning profile plist dictionary not found.');
160
+ }
161
+ const value = readPlistValue(dict);
162
+ if (!isRecord(value)) {
163
+ throw new Error('Provisioning profile plist has an unexpected shape.');
164
+ }
165
+ const entitlements = isRecord(value.Entitlements) ? value.Entitlements : {};
166
+ const applicationIdentifier = stringValue(entitlements['application-identifier']);
167
+ const bundleID = bundleIDFromApplicationIdentifier(applicationIdentifier);
168
+ return {
169
+ name: stringValue(value.Name),
170
+ uuid: stringValue(value.UUID),
171
+ teamID:
172
+ stringValue(entitlements['com.apple.developer.team-identifier']) ??
173
+ stringArrayValue(value.TeamIdentifier)[0],
174
+ applicationIdentifier,
175
+ bundleID,
176
+ provisionedDevices: stringArrayValue(value.ProvisionedDevices),
177
+ expirationDate: stringValue(value.ExpirationDate),
178
+ } satisfies ProvisioningProfileInfo;
179
+ }
180
+
181
+ async function getSigningAssetsByID(id?: string) {
182
+ if (!id) return undefined;
183
+ const db = await openDB(SIGNING_DB_NAME, SIGNING_DB_VERSION, SIGNING_STORE_NAME, 'id');
184
+ return requestToPromise<StoredSigningAssets | undefined>(
185
+ db.transaction(SIGNING_STORE_NAME, 'readonly').objectStore(SIGNING_STORE_NAME).get(id),
186
+ );
187
+ }
188
+
189
+ async function getAllSigningAssets() {
190
+ const db = await openDB(SIGNING_DB_NAME, SIGNING_DB_VERSION, SIGNING_STORE_NAME, 'id');
191
+ return requestToPromise<StoredSigningAssets[]>(
192
+ db.transaction(SIGNING_STORE_NAME, 'readonly').objectStore(SIGNING_STORE_NAME).getAll(),
193
+ );
194
+ }
195
+
196
+ function signingAssetID(deviceUDID: string, bundleID: string) {
197
+ return `${deviceUDID}:${bundleID}`;
198
+ }
199
+
200
+ function bundleIDFromApplicationIdentifier(applicationIdentifier?: string) {
201
+ if (!applicationIdentifier) return undefined;
202
+ const dot = applicationIdentifier.indexOf('.');
203
+ return dot >= 0 ? applicationIdentifier.slice(dot + 1) : undefined;
204
+ }
205
+
206
+ function readPlistValue(element: Element): unknown {
207
+ switch (element.tagName) {
208
+ case 'dict':
209
+ return readPlistDict(element);
210
+ case 'array':
211
+ return Array.from(element.children).map(readPlistValue);
212
+ case 'string':
213
+ case 'date':
214
+ return element.textContent ?? '';
215
+ default:
216
+ return element.textContent ?? '';
217
+ }
218
+ }
219
+
220
+ function readPlistDict(dict: Element) {
221
+ const result: Record<string, unknown> = {};
222
+ const children = Array.from(dict.children);
223
+ for (let index = 0; index < children.length; index += 2) {
224
+ const key = children[index];
225
+ const value = children[index + 1];
226
+ if (!key || key.tagName !== 'key' || !value) continue;
227
+ result[key.textContent ?? ''] = readPlistValue(value);
228
+ }
229
+ return result;
230
+ }
231
+
232
+ function stringValue(value: unknown) {
233
+ return typeof value === 'string' && value ? value : undefined;
234
+ }
235
+
236
+ function stringArrayValue(value: unknown) {
237
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
238
+ }
239
+
240
+ function isRecord(value: unknown): value is Record<string, unknown> {
241
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
242
+ }
243
+
244
+ function openDB(dbName: string, dbVersion: number, storeName: string, keyPath: string) {
245
+ return new Promise<IDBDatabase>((resolve, reject) => {
246
+ const request = indexedDB.open(dbName, dbVersion);
247
+ request.onupgradeneeded = () => {
248
+ const db = request.result;
249
+ if (!db.objectStoreNames.contains(storeName)) {
250
+ db.createObjectStore(storeName, { keyPath });
251
+ }
252
+ };
253
+ request.onsuccess = () => resolve(request.result);
254
+ request.onerror = () => reject(request.error ?? new Error('Open IndexedDB failed'));
255
+ });
256
+ }
257
+
258
+ function requestToPromise<T = unknown>(request: IDBRequest<T>) {
259
+ return new Promise<T>((resolve, reject) => {
260
+ request.onsuccess = () => resolve(request.result);
261
+ request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed'));
262
+ });
263
+ }
@@ -0,0 +1 @@
1
+ export * from './browser-storage';