@limrun/ui 0.9.0-rc.1 → 0.9.0-rc.4

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 (30) hide show
  1. package/dist/core/device-install/apple/client.d.ts +1 -0
  2. package/dist/core/device-install/apple/provisioning.d.ts +42 -31
  3. package/dist/core/device-install/apple/relay.d.ts +5 -9
  4. package/dist/core/device-install/storage/browser-storage.d.ts +19 -0
  5. package/dist/core/device-install/types.d.ts +2 -2
  6. package/dist/device-install/index.cjs +1 -9
  7. package/dist/device-install/index.js +76 -210
  8. package/dist/device-install/react.cjs +1 -1
  9. package/dist/device-install/react.js +1 -1
  10. package/dist/device-install-dialog-86RDdoK9.js +2 -0
  11. package/dist/device-install-dialog-CnyDWf0q.mjs +462 -0
  12. package/dist/device-install-dialog.css +1 -1
  13. package/dist/hooks/use-device-install.d.ts +21 -3
  14. package/dist/index.cjs +1 -1
  15. package/dist/index.js +3 -3
  16. package/dist/use-device-install-CbGVvwPp.js +31 -0
  17. package/dist/use-device-install-j1Gekpl4.mjs +13623 -0
  18. package/package.json +1 -1
  19. package/src/components/device-install/device-install-dialog.css +82 -1
  20. package/src/components/device-install/device-install-dialog.tsx +337 -187
  21. package/src/core/device-install/apple/client.ts +92 -4
  22. package/src/core/device-install/apple/provisioning.ts +67 -24
  23. package/src/core/device-install/apple/relay.ts +121 -205
  24. package/src/core/device-install/storage/browser-storage.ts +26 -1
  25. package/src/core/device-install/types.ts +2 -2
  26. package/src/hooks/use-device-install.ts +748 -60
  27. package/dist/device-install-dialog-CTwVViYY.js +0 -2
  28. package/dist/device-install-dialog-zzKJu7SM.mjs +0 -328
  29. package/dist/use-device-install-CgrOKKyi.mjs +0 -13042
  30. package/dist/use-device-install-DDKRf6IL.js +0 -23
@@ -6,14 +6,7 @@ export type AppleRelayResponse<T = unknown> = {
6
6
  headers?: Record<string, string>;
7
7
  body?: T;
8
8
  rawBody?: string;
9
- bodyBase64?: string;
10
- };
11
-
12
- export type AppleRelayRequest = {
13
- method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
14
- url: string;
15
- headers?: Record<string, string>;
16
- body?: BodyInit;
9
+ rawBodyBase64?: string;
17
10
  };
18
11
 
19
12
  export type AppleProvisioningRequest = {
@@ -33,11 +26,7 @@ export async function createAppleRelaySession(limbuildApiUrl: string, token?: st
33
26
  return (await response.json()) as { appleSessionId: string };
34
27
  }
35
28
 
36
- export async function deleteAppleRelaySession(
37
- limbuildApiUrl: string,
38
- appleSessionId: string,
39
- token?: string,
40
- ) {
29
+ export async function deleteAppleRelaySession(limbuildApiUrl: string, appleSessionId: string, token?: string) {
41
30
  const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/session/delete', token), {
42
31
  method: 'POST',
43
32
  headers: jsonHeaders(token),
@@ -48,38 +37,17 @@ export async function deleteAppleRelaySession(
48
37
  }
49
38
  }
50
39
 
51
- export async function relayAppleRequest<T = unknown>(
52
- limbuildApiUrl: string,
53
- appleSessionId: string,
54
- request: AppleRelayRequest,
55
- token?: string,
56
- ) {
57
- const response = await fetch(appleRelayURL(limbuildApiUrl, appleSessionId, request.url, token), {
58
- method: request.method ?? 'GET',
59
- headers: {
60
- ...(request.headers ?? {}),
61
- ...authHeaders(token),
62
- },
63
- body: request.body,
64
- });
65
- return responseToAppleRelayResponse<T>(response);
66
- }
67
-
68
40
  export async function proxySrpInit(
69
41
  limbuildApiUrl: string,
70
42
  appleSessionId: string,
71
43
  payload: AppleSRPInitRequest,
72
44
  token?: string,
73
45
  ) {
74
- return relayAppleRequest<AppleSRPInitResponse>(
46
+ return postAppleProxy<AppleSRPInitResponse>(
75
47
  limbuildApiUrl,
48
+ '/apple/auth/srp/init',
76
49
  appleSessionId,
77
- {
78
- method: 'POST',
79
- url: 'https://idmsa.apple.com/appleauth/auth/signin/init',
80
- headers: jsonContentHeaders(),
81
- body: JSON.stringify(payload),
82
- },
50
+ payload,
83
51
  token,
84
52
  );
85
53
  }
@@ -93,213 +61,161 @@ export async function proxySrpComplete(
93
61
  },
94
62
  token?: string,
95
63
  ) {
96
- const hashcash = await fetchAppleHashcash(limbuildApiUrl, appleSessionId, token);
97
- return relayAppleRequest(
98
- limbuildApiUrl,
99
- appleSessionId,
100
- {
101
- method: 'POST',
102
- url: 'https://idmsa.apple.com/appleauth/auth/signin/complete?isRememberMeEnabled=false',
103
- headers: {
104
- ...jsonContentHeaders(),
105
- ...(hashcash ? { 'X-Apple-HC': hashcash } : {}),
106
- },
107
- body: JSON.stringify(payload),
108
- },
109
- token,
110
- );
64
+ return postAppleProxy(limbuildApiUrl, '/apple/auth/srp/complete', appleSessionId, payload, token);
111
65
  }
112
66
 
113
- export async function proxyTwoFactorCode(
67
+ export async function triggerTrustedDeviceTwoFactor(
114
68
  limbuildApiUrl: string,
115
69
  appleSessionId: string,
116
- code: string,
117
70
  token?: string,
118
71
  ) {
119
- return relayAppleRequest(
120
- limbuildApiUrl,
121
- appleSessionId,
122
- {
123
- method: 'POST',
124
- url: 'https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode',
125
- headers: jsonContentHeaders(),
126
- body: JSON.stringify({ securityCode: { code } }),
127
- },
128
- token,
129
- );
72
+ const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/2fa/trigger', token), {
73
+ method: 'POST',
74
+ headers: jsonHeaders(token),
75
+ body: JSON.stringify({ appleSessionId }),
76
+ });
77
+ if (!response.ok) {
78
+ throw new Error(`Apple 2FA trigger failed: HTTP ${response.status} ${await response.text()}`);
79
+ }
80
+ return (await response.json()) as AppleRelayResponse;
130
81
  }
131
82
 
132
- export async function finalizeAppleRelaySession(
83
+ export async function triggerPhoneTwoFactor(
133
84
  limbuildApiUrl: string,
134
85
  appleSessionId: string,
86
+ phoneNumberId: number,
87
+ mode = 'sms',
135
88
  token?: string,
136
89
  ) {
137
- return relayAppleRequest(
138
- limbuildApiUrl,
139
- appleSessionId,
140
- {
141
- method: 'GET',
142
- url: 'https://appstoreconnect.apple.com/olympus/v1/session',
143
- headers: { Accept: 'application/json' },
144
- },
145
- token,
146
- );
90
+ const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/2fa/phone/trigger', token), {
91
+ method: 'POST',
92
+ headers: jsonHeaders(token),
93
+ body: JSON.stringify({ appleSessionId, phoneNumberId, mode }),
94
+ });
95
+ if (!response.ok) {
96
+ throw new Error(`Apple phone 2FA trigger failed: HTTP ${response.status} ${await response.text()}`);
97
+ }
98
+ return (await response.json()) as AppleRelayResponse;
147
99
  }
148
100
 
149
- export async function proxyProvisioningRequest<T = unknown>(
101
+ export async function proxyTwoFactorCode(
150
102
  limbuildApiUrl: string,
151
103
  appleSessionId: string,
152
- request: AppleProvisioningRequest,
104
+ code: string,
153
105
  token?: string,
154
106
  ) {
155
- return relayAppleRequest<T>(
156
- limbuildApiUrl,
157
- appleSessionId,
158
- {
159
- method: request.method ?? 'GET',
160
- url: `https://developer.apple.com/services-account/QH65B2${request.path}`,
161
- headers: {
162
- Accept: 'application/json',
163
- ...(request.payload ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {}),
164
- },
165
- body: request.payload ? formEncode(request.payload) : undefined,
166
- },
167
- token,
168
- );
169
- }
170
-
171
- function limbuildURL(limbuildApiUrl: string, path: string, token?: string) {
172
- const url = new URL(path, limbuildApiUrl);
173
- if (token) {
174
- url.searchParams.set('token', token);
107
+ const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/2fa', token), {
108
+ method: 'POST',
109
+ headers: jsonHeaders(token),
110
+ body: JSON.stringify({ appleSessionId, code }),
111
+ });
112
+ if (!response.ok) {
113
+ throw new Error(`Apple 2FA proxy failed: HTTP ${response.status} ${await response.text()}`);
175
114
  }
176
- return url;
177
- }
178
-
179
- function appleRelayURL(limbuildApiUrl: string, appleSessionId: string, appleURL: string, token?: string) {
180
- const url = limbuildURL(limbuildApiUrl, '/apple/relay', token);
181
- url.searchParams.set('appleSessionId', appleSessionId);
182
- url.searchParams.set('url', appleURL);
183
- return url;
115
+ return (await response.json()) as AppleRelayResponse;
184
116
  }
185
117
 
186
- function jsonHeaders(token?: string): Record<string, string> {
187
- const headers: Record<string, string> = {
188
- 'Content-Type': 'application/json',
189
- };
190
- if (token) {
191
- headers.Authorization = `Bearer ${token}`;
118
+ export async function proxyPhoneTwoFactorCode(
119
+ limbuildApiUrl: string,
120
+ appleSessionId: string,
121
+ phoneNumberId: number,
122
+ code: string,
123
+ mode = 'sms',
124
+ token?: string,
125
+ ) {
126
+ const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/2fa/phone', token), {
127
+ method: 'POST',
128
+ headers: jsonHeaders(token),
129
+ body: JSON.stringify({ appleSessionId, phoneNumberId, mode, code }),
130
+ });
131
+ if (!response.ok) {
132
+ throw new Error(`Apple phone 2FA proxy failed: HTTP ${response.status} ${await response.text()}`);
192
133
  }
193
- return headers;
194
- }
195
-
196
- function authHeaders(token?: string): Record<string, string> {
197
- return token ? { Authorization: `Bearer ${token}` } : {};
134
+ return (await response.json()) as AppleRelayResponse;
198
135
  }
199
136
 
200
- function jsonContentHeaders(): Record<string, string> {
201
- return {
202
- Accept: 'application/json, text/javascript, */*; q=0.01',
203
- 'Content-Type': 'application/json',
204
- 'X-Requested-With': 'XMLHttpRequest',
205
- };
137
+ export async function fetchAppleAccountSession(
138
+ limbuildApiUrl: string,
139
+ appleSessionId: string,
140
+ token?: string,
141
+ ) {
142
+ const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/finalize', token), {
143
+ method: 'POST',
144
+ headers: jsonHeaders(token),
145
+ body: JSON.stringify({ appleSessionId }),
146
+ });
147
+ if (!response.ok) {
148
+ throw new Error(`Apple session finalization failed: HTTP ${response.status} ${await response.text()}`);
149
+ }
150
+ return (await response.json()) as AppleRelayResponse;
206
151
  }
207
152
 
208
- async function responseToAppleRelayResponse<T>(response: Response): Promise<AppleRelayResponse<T>> {
209
- const rawBody = await response.text();
210
- let body: T | undefined;
211
- try {
212
- body = rawBody ? (JSON.parse(rawBody) as T) : undefined;
213
- } catch {
214
- body = undefined;
153
+ export async function proxyProvisioningRequest<T = unknown>(
154
+ limbuildApiUrl: string,
155
+ appleSessionId: string,
156
+ request: AppleProvisioningRequest,
157
+ token?: string,
158
+ ) {
159
+ const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/provisioning', token), {
160
+ method: 'POST',
161
+ headers: jsonHeaders(token),
162
+ body: JSON.stringify({ appleSessionId, ...request }),
163
+ });
164
+ if (!response.ok) {
165
+ throw new Error(`Apple provisioning proxy failed: HTTP ${response.status} ${await response.text()}`);
215
166
  }
216
- return {
217
- status: response.status,
218
- statusText: `${response.status} ${response.statusText}`.trim(),
219
- headers: Object.fromEntries(response.headers.entries()),
220
- body,
221
- rawBody,
222
- bodyBase64: bytesToBase64(new TextEncoder().encode(rawBody)),
223
- };
167
+ return normalizeAppleProxyResponse<T>((await response.json()) as AppleRelayResponse<T>);
224
168
  }
225
169
 
226
- async function fetchAppleHashcash(limbuildApiUrl: string, appleSessionId: string, token?: string) {
227
- const config = await relayAppleRequest<{ authServiceKey?: string }>(
228
- limbuildApiUrl,
229
- appleSessionId,
230
- {
231
- method: 'GET',
232
- url: 'https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com',
233
- headers: { Accept: 'application/json' },
234
- },
235
- token,
236
- );
237
- const widgetKey = config.body?.authServiceKey;
238
- const response = await relayAppleRequest(
239
- limbuildApiUrl,
240
- appleSessionId,
241
- {
242
- method: 'GET',
243
- url: `https://idmsa.apple.com/appleauth/auth/signin${widgetKey ? `?widgetKey=${encodeURIComponent(widgetKey)}` : ''}`,
244
- headers: { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' },
245
- },
246
- token,
247
- );
248
- const bits = response.headers?.['x-apple-hc-bits'];
249
- const challenge = response.headers?.['x-apple-hc-challenge'];
250
- if (!bits || !challenge) {
251
- return undefined;
170
+ async function postAppleProxy<T>(
171
+ limbuildApiUrl: string,
172
+ path: string,
173
+ appleSessionId: string,
174
+ payload: unknown,
175
+ token?: string,
176
+ ) {
177
+ const response = await fetch(limbuildURL(limbuildApiUrl, path, token), {
178
+ method: 'POST',
179
+ headers: jsonHeaders(token),
180
+ body: JSON.stringify({ appleSessionId, payload }),
181
+ });
182
+ if (!response.ok) {
183
+ throw new Error(`Apple proxy ${path} failed: HTTP ${response.status} ${await response.text()}`);
252
184
  }
253
- return makeAppleHashcash(parseInt(bits, 10), challenge);
185
+ return normalizeAppleProxyResponse<T>((await response.json()) as AppleRelayResponse<T>);
254
186
  }
255
187
 
256
- async function makeAppleHashcash(bits: number, challenge: string) {
257
- if (!Number.isFinite(bits) || bits <= 0) {
258
- return undefined;
188
+ function normalizeAppleProxyResponse<T>(response: AppleRelayResponse<T>) {
189
+ if (response.body !== undefined || !response.rawBody) {
190
+ return response;
259
191
  }
260
- const date = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14);
261
- for (let counter = 0; ; counter += 1) {
262
- const value = `1:${bits}:${date}:${challenge}::${counter}`;
263
- const digest = new Uint8Array(await crypto.subtle.digest('SHA-1', new TextEncoder().encode(value)));
264
- if (hasLeadingZeroBits(digest, bits)) {
265
- return value;
266
- }
192
+ try {
193
+ return {
194
+ ...response,
195
+ body: JSON.parse(response.rawBody) as T,
196
+ };
197
+ } catch {
198
+ return response;
267
199
  }
268
200
  }
269
201
 
270
- function hasLeadingZeroBits(bytes: Uint8Array, bits: number) {
271
- for (const byte of bytes) {
272
- if (bits <= 0) return true;
273
- if (bits >= 8) {
274
- if (byte !== 0) return false;
275
- bits -= 8;
276
- continue;
277
- }
278
- return byte >> (8 - bits) === 0;
202
+ function limbuildURL(limbuildApiUrl: string, path: string, token?: string) {
203
+ const base = limbuildApiUrl.replace(/\/$/, '');
204
+ const suffix = path.startsWith('/') ? path : `/${path}`;
205
+ const url = new URL(`${base}${suffix}`);
206
+ if (token) {
207
+ url.searchParams.set('token', token);
279
208
  }
280
- return bits <= 0;
209
+ return url;
281
210
  }
282
211
 
283
- function formEncode(payload: unknown) {
284
- const params = new URLSearchParams();
285
- if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
286
- return params;
287
- }
288
- for (const [key, value] of Object.entries(payload)) {
289
- if (value === undefined || value === null) continue;
290
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
291
- params.set(key, String(value));
292
- } else {
293
- params.set(key, JSON.stringify(value));
294
- }
295
- }
296
- return params;
212
+ function jsonHeaders(token?: string): Record<string, string> {
213
+ return {
214
+ 'Content-Type': 'application/json',
215
+ ...authHeaders(token),
216
+ };
297
217
  }
298
218
 
299
- function bytesToBase64(bytes: Uint8Array) {
300
- let binary = '';
301
- for (const byte of bytes) {
302
- binary += String.fromCharCode(byte);
303
- }
304
- return btoa(binary);
219
+ function authHeaders(token?: string): Record<string, string> {
220
+ return token ? { Authorization: `Bearer ${token}` } : {};
305
221
  }
@@ -73,6 +73,18 @@ export async function getLatestSigningAssets() {
73
73
  )[0];
74
74
  }
75
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
+
76
88
  export async function putSigningAssets(input: PutSigningAssetsInput) {
77
89
  const normalizedBundleID = normalizeBundleID(input.bundleID);
78
90
  if (!normalizedBundleID) {
@@ -118,7 +130,20 @@ export function profileMatchesBundleID(profile: ProvisioningProfileInfo, bundleI
118
130
  }
119
131
 
120
132
  export async function parseProvisioningProfile(file: File) {
121
- const text = new TextDecoder('latin1').decode(await file.arrayBuffer());
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);
122
147
  const start = text.indexOf('<?xml');
123
148
  const end = text.indexOf('</plist>');
124
149
  if (start < 0 || end < start) {
@@ -1,10 +1,10 @@
1
1
  export type DeviceInstallLog = (message: string, detail?: string) => void;
2
2
 
3
- export type DeviceInstallStep = 'build' | 'usb' | 'pair' | 'install';
3
+ export type DeviceInstallStep = 'signing' | 'connect' | 'build' | 'install';
4
4
 
5
5
  export type DeviceInstallStepStatus = 'idle' | 'active' | 'complete' | 'error';
6
6
 
7
- export type DeviceInstallBusyAction = 'build' | 'usb' | 'pair' | 'install';
7
+ export type DeviceInstallBusyAction = 'signing' | 'usb' | 'pair' | 'build' | 'install';
8
8
 
9
9
  export type DeviceInstallBuildStatus =
10
10
  | 'idle'