@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,158 +0,0 @@
1
- // @vitest-environment node
2
-
3
- import forge from 'node-forge';
4
- import { describe, expect, test } from 'vitest';
5
- import type { AppleRelayWebSocketClient } from '../core/device-install/apple';
6
- import {
7
- APPLE_CERTIFICATE_SECRET_TYPE,
8
- appleCertificateSecretName,
9
- ensureAppleCertificateSecret,
10
- type SigningSecret,
11
- type SigningSecretMetadata,
12
- type SigningSecretStore,
13
- type SigningSecretType,
14
- } from './index';
15
-
16
- /**
17
- * A self-signed certificate is enough for exportAppleCertificateP12: the
18
- * p12 bundling never verifies that the key matches the certificate.
19
- */
20
- function selfSignedCertificateBase64() {
21
- const keys = forge.pki.rsa.generateKeyPair(1024);
22
- const cert = forge.pki.createCertificate();
23
- cert.publicKey = keys.publicKey;
24
- cert.serialNumber = '01';
25
- cert.validity.notBefore = new Date();
26
- cert.validity.notAfter = new Date(Date.now() + 24 * 60 * 60 * 1000);
27
- const attrs = [{ name: 'commonName', value: 'Apple Development Test' }];
28
- cert.setSubject(attrs);
29
- cert.setIssuer(attrs);
30
- cert.sign(keys.privateKey);
31
- return forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes());
32
- }
33
-
34
- function memorySecretStore(options: { failPuts?: () => boolean } = {}) {
35
- const entries = new Map<string, SigningSecret>();
36
- const store: SigningSecretStore = {
37
- async put(type: SigningSecretType, name: string, data: Record<string, string>) {
38
- if (options.failPuts?.()) {
39
- throw new Error('secret store unavailable');
40
- }
41
- const secret: SigningSecret = { type, name, data, createdAt: new Date().toISOString() };
42
- entries.set(`${type}:${name}`, secret);
43
- return secret;
44
- },
45
- async get(type: SigningSecretType, name: string) {
46
- return entries.get(`${type}:${name}`);
47
- },
48
- async list() {
49
- return [...entries.values()].map(({ data: _data, ...metadata }): SigningSecretMetadata => metadata);
50
- },
51
- async delete(type: SigningSecretType, name: string) {
52
- entries.delete(`${type}:${name}`);
53
- },
54
- };
55
- return { store, entries };
56
- }
57
-
58
- type PortalState = { mintedCertificateIds: string[]; certificateBase64: string };
59
-
60
- /**
61
- * Fakes the relay's provisioning proxy: minting appends to
62
- * mintedCertificateIds, listing reflects what has been minted so far.
63
- */
64
- function fakePortalRelay(state: PortalState): AppleRelayWebSocketClient {
65
- return {
66
- async request(_type: string, payload: unknown) {
67
- const request = payload as { path: string };
68
- if (request.path.includes('listCertRequests')) {
69
- return portalOK({
70
- certRequests: state.mintedCertificateIds.map((certificateId) => ({ certificateId })),
71
- });
72
- }
73
- if (request.path.includes('submitCertificateRequest')) {
74
- const certificateId = `CERT${state.mintedCertificateIds.length + 1}`;
75
- state.mintedCertificateIds.push(certificateId);
76
- return portalOK({
77
- certRequest: {
78
- certificateId,
79
- serialNum: '01',
80
- expirationDateString: '2027-01-01',
81
- },
82
- });
83
- }
84
- if (request.path.includes('downloadCertificateContent')) {
85
- return { status: 200, statusText: 'OK', rawBodyBase64: state.certificateBase64 };
86
- }
87
- throw new Error(`unexpected portal path: ${request.path}`);
88
- },
89
- } as unknown as AppleRelayWebSocketClient;
90
- }
91
-
92
- function portalOK(body: Record<string, unknown>) {
93
- return { status: 200, statusText: 'OK', body: { resultCode: 0, ...body } };
94
- }
95
-
96
- describe('ensureAppleCertificateSecret', () => {
97
- const teamId = 'TEAM1';
98
- const secretName = appleCertificateSecretName(teamId, 'DEVELOPMENT');
99
-
100
- test('surfaces an actionable error when the store write fails after minting', async () => {
101
- const state: PortalState = {
102
- mintedCertificateIds: [],
103
- certificateBase64: selfSignedCertificateBase64(),
104
- };
105
- const broken = memorySecretStore({ failPuts: () => true });
106
-
107
- await expect(
108
- ensureAppleCertificateSecret({
109
- relay: fakePortalRelay(state),
110
- teamId,
111
- secretStore: broken.store,
112
- }),
113
- ).rejects.toThrow(/revoke certificate CERT1/);
114
- expect(state.mintedCertificateIds).toEqual(['CERT1']);
115
- });
116
-
117
- test('mints and stores the certificate with its type in the secret name', async () => {
118
- const state: PortalState = {
119
- mintedCertificateIds: [],
120
- certificateBase64: selfSignedCertificateBase64(),
121
- };
122
- const org = memorySecretStore();
123
-
124
- const result = await ensureAppleCertificateSecret({
125
- relay: fakePortalRelay(state),
126
- teamId,
127
- secretStore: org.store,
128
- });
129
- expect(result.created).toBe(true);
130
- expect(result.certificateId).toBe('CERT1');
131
- const stored = await org.store.get(APPLE_CERTIFICATE_SECRET_TYPE, secretName);
132
- expect(stored?.data.certificateID).toBe('CERT1');
133
- expect(stored?.data.certificateType).toBe('DEVELOPMENT');
134
- expect(stored?.data.certificateP12Base64).toBeTruthy();
135
- });
136
-
137
- test('reuses the stored certificate when it is still on the team', async () => {
138
- const state: PortalState = {
139
- mintedCertificateIds: ['CERT1'],
140
- certificateBase64: selfSignedCertificateBase64(),
141
- };
142
- const org = memorySecretStore();
143
- await org.store.put(APPLE_CERTIFICATE_SECRET_TYPE, secretName, {
144
- certificateP12Base64: 'cDEy',
145
- certificateID: 'CERT1',
146
- teamID: teamId,
147
- });
148
-
149
- const result = await ensureAppleCertificateSecret({
150
- relay: fakePortalRelay(state),
151
- teamId,
152
- secretStore: org.store,
153
- });
154
- expect(result.created).toBe(false);
155
- expect(result.certificateId).toBe('CERT1');
156
- expect(state.mintedCertificateIds).toEqual(['CERT1']);
157
- });
158
- });
@@ -1,241 +0,0 @@
1
- /**
2
- * Headless helpers that turn an authenticated Apple relay session into
3
- * signing credentials persisted in a SigningSecretStore. The Apple relay
4
- * only proxies: certificate and profile bytes always land in the browser
5
- * first, and the browser writes them into the configured store (Limrun's
6
- * org secret store by default, or a customer-provided one).
7
- */
8
- import {
9
- createAppleCertificate,
10
- downloadAppleCertificate,
11
- downloadAppleProfile,
12
- exportAppleCertificateP12,
13
- generateAppleSigningKeyAndCSR,
14
- listAppleCertificates,
15
- listAppleProfiles,
16
- } from '../app-store-relay';
17
- import type { AppleRelayWebSocketClient } from '../core/device-install/apple';
18
- import {
19
- normalizeCertificateSerial,
20
- parseProvisioningProfileBase64,
21
- } from '../core/device-install/storage/browser-storage';
22
- import {
23
- APPLE_CERTIFICATE_SECRET_TYPE,
24
- putAppleCertificateSecret,
25
- putAppleProvisioningProfileSecret,
26
- type AppleCertificateSecretData,
27
- type AppleCertificateType,
28
- type SigningSecret,
29
- type SigningSecretStore,
30
- } from '../core/device-install/storage/secret-store';
31
-
32
- export {
33
- APPLE_CERTIFICATE_SECRET_TYPE,
34
- APPLE_PROVISIONING_PROFILE_SECRET_TYPE,
35
- createBrowserSecretStore,
36
- createLimrunSecretStore,
37
- putAppleCertificateSecret,
38
- putAppleProvisioningProfileSecret,
39
- type AppleCertificateSecretData,
40
- type AppleCertificateType,
41
- type AppleProvisioningProfileSecretData,
42
- type LimrunSecretStoreOptions,
43
- type SigningSecret,
44
- type SigningSecretData,
45
- type SigningSecretMetadata,
46
- type SigningSecretStore,
47
- type SigningSecretType,
48
- } from '../core/device-install/storage/secret-store';
49
- export { normalizeCertificateSerial } from '../core/device-install/storage/browser-storage';
50
-
51
- /**
52
- * Conventional secret name of a team's certificate bundle. One bundle per
53
- * team and Apple certificate type; the type is in the name so development
54
- * and distribution material never collide.
55
- */
56
- export function appleCertificateSecretName(teamId: string, certificateType: AppleCertificateType) {
57
- return `${teamId}/${certificateType}`;
58
- }
59
-
60
- export type EnsureAppleCertificateInput = {
61
- relay: AppleRelayWebSocketClient;
62
- teamId: string;
63
- secretStore: SigningSecretStore;
64
- /** Common name used when minting a new certificate. */
65
- commonName?: string;
66
- log?: (message: string, detail?: string) => void;
67
- };
68
-
69
- export type EnsureAppleCertificateResult = {
70
- secret: SigningSecret;
71
- certificateId: string;
72
- /** True when a new certificate was minted instead of reusing the stored one. */
73
- created: boolean;
74
- };
75
-
76
- /**
77
- * Returns a usable development certificate secret for the team, reusing the
78
- * stored one when its certificate is still on the team and minting a new
79
- * one otherwise. Apple caps development certificates at 2 and never returns
80
- * private keys, so reuse of the stored p12 is strongly preferred.
81
- *
82
- * Durability of the stored material is the secret store's concern:
83
- * implementors who want retries or fallbacks build them into their
84
- * SigningSecretStore.
85
- */
86
- export async function ensureAppleCertificateSecret({
87
- relay,
88
- teamId,
89
- secretStore,
90
- commonName,
91
- log = () => {},
92
- }: EnsureAppleCertificateInput): Promise<EnsureAppleCertificateResult> {
93
- // The browser flow mints via the portal's development kind, which is
94
- // the DEVELOPMENT certificate type in Apple's App Store Connect enum.
95
- const certificateType: AppleCertificateType = 'DEVELOPMENT';
96
- const secretName = appleCertificateSecretName(teamId, certificateType);
97
- const current = await listAppleCertificates({ relay, teamId, certificateKind: 'development' });
98
- const findOnTeam = (certificateId: string | undefined) =>
99
- certificateId === undefined ? undefined : (
100
- current.find(
101
- (item) =>
102
- stringField(item, 'certificateId') === certificateId ||
103
- stringField(item, 'certRequestId') === certificateId,
104
- )
105
- );
106
-
107
- const stored = await secretStore.get(APPLE_CERTIFICATE_SECRET_TYPE, secretName);
108
- const storedCertificateId = stringField(stored?.data, 'certificateID');
109
- if (stored && storedCertificateId && stored.data.certificateP12Base64) {
110
- // The stored value may be a certRequestId; resolve the canonical
111
- // certificateId the profile API expects. If the cert is no longer on
112
- // the team (revoked), fall through and mint a new one.
113
- const matched = findOnTeam(storedCertificateId);
114
- if (matched) {
115
- const certificateId = stringField(matched, 'certificateId') ?? storedCertificateId;
116
- log('Reusing stored Apple certificate', certificateId);
117
- return { secret: stored, certificateId, created: false };
118
- }
119
- log('Stored certificate is no longer on the team', 'Minting a new one.');
120
- }
121
-
122
- // The private key is generated and kept in this browser; Apple only ever
123
- // sees the CSR. Without this key the downloaded cert cannot sign anything.
124
- const key = await generateAppleSigningKeyAndCSR({ commonName: commonName ?? `Limrun ${teamId}` });
125
- const certificate = await createAppleCertificate({
126
- relay,
127
- teamId,
128
- certificateKind: 'development',
129
- csrPEM: key.csrPEM,
130
- });
131
- const certificateId =
132
- stringField(certificate, 'certificateId') ?? stringField(certificate, 'certRequestId');
133
- if (!certificateId) {
134
- throw new Error('Apple certificate creation did not return a certificate ID.');
135
- }
136
- const downloaded = await downloadAppleCertificate({
137
- relay,
138
- teamId,
139
- certificateKind: 'development',
140
- certificateId,
141
- });
142
- if (!downloaded.rawBodyBase64) {
143
- throw new Error('Apple certificate download returned no bytes.');
144
- }
145
- const data: AppleCertificateSecretData = {
146
- certificateP12Base64: exportAppleCertificateP12({
147
- privateKeyPKCS8Base64: key.privateKeyPKCS8Base64,
148
- certificateBase64: downloaded.rawBodyBase64,
149
- password: '',
150
- friendlyName: `Apple Development ${teamId}`,
151
- }),
152
- certificateType,
153
- teamID: teamId,
154
- certificateID: certificateId,
155
- // Normalized so it equals the serials profiles reference.
156
- serialNumber: normalizeCertificateSerial(stringField(certificate, 'serialNum')),
157
- expirationDate: stringField(certificate, 'expirationDateString'),
158
- };
159
-
160
- let secret: SigningSecret;
161
- try {
162
- secret = await putAppleCertificateSecret(secretStore, secretName, data);
163
- } catch (error) {
164
- // The key existed only in this browser's memory; once this throw
165
- // unwinds it is gone, and the portal certificate is unusable.
166
- throw new Error(
167
- `Certificate ${certificateId} was created on the Apple Developer portal but saving its private ` +
168
- `key to the secret store failed: ${errorText(error)}. The key cannot be recovered; revoke ` +
169
- `certificate ${certificateId} at developer.apple.com before retrying, or the retry will mint ` +
170
- `another certificate against Apple's limit.`,
171
- );
172
- }
173
- log('Apple certificate stored', certificateId);
174
- return { secret, certificateId, created: true };
175
- }
176
-
177
- function errorText(error: unknown) {
178
- return error instanceof Error ? error.message : String(error);
179
- }
180
-
181
- export type SaveAppleProfileInput = {
182
- relay: AppleRelayWebSocketClient;
183
- teamId: string;
184
- profileId: string;
185
- secretStore: SigningSecretStore;
186
- log?: (message: string, detail?: string) => void;
187
- };
188
-
189
- /**
190
- * Downloads the provisioning profile and stores it as an
191
- * appleProvisioningProfile secret named `${teamId}/${uuid}`. The UUID is
192
- * unique per profile, so a team can hold many profiles for the same
193
- * bundle ID and certificate set; the reference fields (certificate
194
- * serials, bundle IDs, device IDs parsed out of the profile) are what
195
- * consumers filter on.
196
- */
197
- export async function saveAppleProfileSecret({
198
- relay,
199
- teamId,
200
- profileId,
201
- secretStore,
202
- log = () => {},
203
- }: SaveAppleProfileInput): Promise<SigningSecret> {
204
- const downloaded = await downloadAppleProfile({ relay, teamId, profileId });
205
- const profileBase64 = downloaded.rawBodyBase64;
206
- if (!profileBase64) {
207
- throw new Error('Apple provisioning profile download returned no bytes.');
208
- }
209
- const info = parseProvisioningProfileBase64(profileBase64);
210
- const name = `${teamId}/${info.uuid ?? profileId}`;
211
- const secret = await putAppleProvisioningProfileSecret(secretStore, name, {
212
- provisioningProfileBase64: profileBase64,
213
- certificateSerialNumbers: info.certificateSerialNumbers.join(','),
214
- bundleIDs: info.bundleID,
215
- deviceIDs: info.provisionedDevices.join(','),
216
- teamID: info.teamID ?? teamId,
217
- profileName: info.name,
218
- uuid: info.uuid,
219
- expirationDate: info.expirationDate,
220
- });
221
- log('Apple provisioning profile stored', name);
222
- return secret;
223
- }
224
-
225
- export type ListTeamProfilesInput = {
226
- relay: AppleRelayWebSocketClient;
227
- teamId: string;
228
- };
229
-
230
- /** Lists the team's development provisioning profiles from the portal. */
231
- export async function listTeamAppleProfiles({ relay, teamId }: ListTeamProfilesInput) {
232
- return listAppleProfiles({ relay, teamId, profileKind: 'development' });
233
- }
234
-
235
- /** Read a string-ish value from a loosely typed portal record. */
236
- export function stringField(record: Record<string, unknown> | undefined, key: string) {
237
- const value = record?.[key];
238
- if (typeof value === 'string') return value;
239
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
240
- return undefined;
241
- }
@@ -1,143 +0,0 @@
1
- import { AppleGsaSrpClient } from './gsa-srp';
2
- import {
3
- openAppleRelayWebSocket,
4
- fetchAppleAccountSession,
5
- proxyPhoneTwoFactorCode,
6
- proxySrpComplete,
7
- proxySrpInit,
8
- proxyTwoFactorCode,
9
- triggerPhoneTwoFactor,
10
- triggerTrustedDeviceTwoFactor,
11
- type AppleRelayWebSocketClient,
12
- type AppleRelayResponse,
13
- } from './relay';
14
-
15
- export type AppleIDLoginInput = {
16
- registryApiUrl: string;
17
- accountName: string;
18
- password: string;
19
- token?: string;
20
- organizationId?: string;
21
- };
22
-
23
- export type AppleIDLoginResult = {
24
- relay: AppleRelayWebSocketClient;
25
- completeResponse: AppleRelayResponse;
26
- twoFactorChallengeResponse?: AppleRelayResponse;
27
- requiresTwoFactor: boolean;
28
- finishTwoFactor: (code: string) => Promise<AppleRelayResponse>;
29
- finalize: () => Promise<AppleRelayResponse>;
30
- close: () => Promise<void>;
31
- };
32
-
33
- type TwoFactorMethod = { type: 'trustedDevice' } | { type: 'phone'; phoneNumberId: number; mode: string };
34
-
35
- export async function startBrowserOwnedAppleIDLogin({
36
- registryApiUrl,
37
- accountName,
38
- password,
39
- token,
40
- organizationId,
41
- }: AppleIDLoginInput): Promise<AppleIDLoginResult> {
42
- const relay = await openAppleRelayWebSocket(registryApiUrl, token, organizationId);
43
- try {
44
- const srp = new AppleGsaSrpClient(accountName);
45
- const initResponse = await proxySrpInit(relay, await srp.init());
46
- if (initResponse.status < 200 || initResponse.status >= 300) {
47
- throw new Error(
48
- `Apple SRP init failed: HTTP ${initResponse.status} ${initResponse.rawBody ?? ''}`.trim(),
49
- );
50
- }
51
- if (!initResponse.body) {
52
- throw new Error('Apple SRP init response did not include a body.');
53
- }
54
- const proof = await srp.complete(password, initResponse.body);
55
- const completeResponse = await proxySrpComplete(relay, {
56
- ...proof,
57
- rememberMe: false,
58
- trustTokens: [],
59
- });
60
- const requiresTwoFactor = completeResponse.status === 409;
61
- let twoFactorChallengeResponse: AppleRelayResponse | undefined;
62
- let twoFactorMethod: TwoFactorMethod = { type: 'trustedDevice' };
63
- if (requiresTwoFactor) {
64
- twoFactorChallengeResponse = await triggerTrustedDeviceTwoFactor(relay);
65
- const phone = trustedPhoneNumberFromChallenge(twoFactorChallengeResponse.body);
66
- // Only route codes through the phone/SMS endpoint when Apple actually
67
- // falls back to phone verification (HTTP 412). The trusted-device
68
- // challenge body frequently includes trusted-phone metadata even for a
69
- // normal device push; routing those codes to the phone endpoint breaks
70
- // trusted-device login.
71
- if (twoFactorChallengeResponse.status === 412) {
72
- if (!phone) {
73
- throw new Error('Apple requested phone verification but did not include a trusted phone number.');
74
- }
75
- twoFactorMethod = {
76
- type: 'phone',
77
- phoneNumberId: phone.id,
78
- mode: phone.pushMode ?? 'sms',
79
- };
80
- twoFactorChallengeResponse = await triggerPhoneTwoFactor(relay, phone.id, phone.pushMode ?? 'sms');
81
- }
82
- if (twoFactorChallengeResponse.status < 200 || twoFactorChallengeResponse.status >= 300) {
83
- throw new Error(
84
- `Apple two-factor challenge failed: HTTP ${twoFactorChallengeResponse.status} ${
85
- twoFactorChallengeResponse.rawBody ?? ''
86
- }`.trim(),
87
- );
88
- }
89
- } else if (completeResponse.status < 200 || completeResponse.status >= 300) {
90
- throw new Error(
91
- `Apple SRP complete failed: HTTP ${completeResponse.status} ${completeResponse.rawBody ?? ''}`.trim(),
92
- );
93
- }
94
- return {
95
- relay,
96
- completeResponse,
97
- twoFactorChallengeResponse,
98
- requiresTwoFactor,
99
- finishTwoFactor: async (code) => {
100
- const response =
101
- twoFactorMethod.type === 'phone' ?
102
- await proxyPhoneTwoFactorCode(relay, twoFactorMethod.phoneNumberId, code, twoFactorMethod.mode)
103
- : await proxyTwoFactorCode(relay, code);
104
- if (response.status < 200 || response.status >= 300) {
105
- throw new Error(
106
- `Apple two-factor code failed: HTTP ${response.status} ${response.rawBody ?? ''}`.trim(),
107
- );
108
- }
109
- return response;
110
- },
111
- finalize: async () => fetchAppleAccountSession(relay),
112
- close: async () => relay.close(),
113
- };
114
- } catch (error) {
115
- relay.close();
116
- throw error;
117
- }
118
- }
119
-
120
- function trustedPhoneNumberFromChallenge(body: unknown) {
121
- if (!isRecord(body)) return undefined;
122
- const verification = isRecord(body.phoneNumberVerification) ? body.phoneNumberVerification : undefined;
123
- const trustedPhoneNumber =
124
- recordValue(verification?.trustedPhoneNumber) ??
125
- recordValue(body.trustedPhoneNumber) ??
126
- recordValue(body.phoneNumber);
127
- if (!trustedPhoneNumber) return undefined;
128
- const id = trustedPhoneNumber.id;
129
- if (typeof id !== 'number') return undefined;
130
- const pushMode =
131
- typeof trustedPhoneNumber.pushMode === 'string' ? trustedPhoneNumber.pushMode
132
- : typeof body.mode === 'string' ? body.mode
133
- : undefined;
134
- return { id, pushMode };
135
- }
136
-
137
- function recordValue(value: unknown) {
138
- return isRecord(value) ? value : undefined;
139
- }
140
-
141
- function isRecord(value: unknown): value is Record<string, unknown> {
142
- return typeof value === 'object' && value !== null && !Array.isArray(value);
143
- }