@limrun/ui 0.14.0-rc.5 → 0.14.1

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 +1351 -1196
  6. package/package.json +10 -51
  7. package/src/components/remote-control.tsx +414 -2
  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 -204
  12. package/dist/app-store-relay/index.js +0 -677
  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 -110
  18. package/dist/apple-credentials/index.js +0 -291
  19. package/dist/browser-storage-BnO-Ma-e.mjs +0 -219
  20. package/dist/browser-storage-DLxIqZk5.js +0 -1
  21. package/dist/client-C9z_sgOf.js +0 -1
  22. package/dist/client-CFDB4mX5.mjs +0 -191
  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 -237
  28. package/dist/core/device-install/apple/relay.d.ts +0 -53
  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 -124
  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 -594
  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-CDPvxd1h.mjs +0 -270
  73. package/dist/provisioning-CbC4h--7.js +0 -1
  74. package/src/app-store-relay/index.test.ts +0 -60
  75. package/src/app-store-relay/index.ts +0 -846
  76. package/src/app-store-relay/react.ts +0 -131
  77. package/src/apple-credentials/index.test.ts +0 -180
  78. package/src/apple-credentials/index.ts +0 -388
  79. package/src/core/device-install/apple/client.ts +0 -150
  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 -114
  84. package/src/core/device-install/apple/provisioning.ts +0 -419
  85. package/src/core/device-install/apple/relay.ts +0 -202
  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 -239
  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 -332
  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,325 +0,0 @@
1
- import forge from 'node-forge';
2
- import type {
3
- DeviceInstallSigningMode,
4
- ProvisioningProfileInfo,
5
- PutSigningAssetsInput,
6
- StoredPairRecord,
7
- StoredSigningAssets,
8
- } from '../types';
9
- import type { PairRecordPayload } from '../types';
10
-
11
- const PAIRING_DB_NAME = 'limbuild-device-pairing';
12
- const PAIRING_DB_VERSION = 1;
13
- const PAIRING_STORE_NAME = 'pairRecords';
14
- const SIGNING_DB_NAME = 'limbuild-device-signing';
15
- const SIGNING_DB_VERSION = 1;
16
- const SIGNING_STORE_NAME = 'signingAssets';
17
-
18
- export function normalizeUDID(udid?: string) {
19
- return (udid ?? '').replace(/-/g, '').replace(/[^a-fA-F0-9]/g, '');
20
- }
21
-
22
- export function normalizeBundleID(bundleID?: string) {
23
- return (bundleID ?? '').trim();
24
- }
25
-
26
- export async function getPairRecord(udid?: string) {
27
- const normalized = normalizeUDID(udid);
28
- if (!normalized) return undefined;
29
- const db = await openDB(PAIRING_DB_NAME, PAIRING_DB_VERSION, PAIRING_STORE_NAME, 'udid');
30
- return requestToPromise<StoredPairRecord | undefined>(
31
- db.transaction(PAIRING_STORE_NAME, 'readonly').objectStore(PAIRING_STORE_NAME).get(normalized),
32
- );
33
- }
34
-
35
- export async function putPairRecord(record: PairRecordPayload, metadata: { productName?: string } = {}) {
36
- const normalized = normalizeUDID(record.udid);
37
- if (!normalized) throw new Error('Cannot store pair record without a UDID.');
38
- const stored: StoredPairRecord = {
39
- ...record,
40
- udid: normalized,
41
- productName: metadata.productName,
42
- updatedAt: new Date().toISOString(),
43
- };
44
- const db = await openDB(PAIRING_DB_NAME, PAIRING_DB_VERSION, PAIRING_STORE_NAME, 'udid');
45
- await requestToPromise(
46
- db.transaction(PAIRING_STORE_NAME, 'readwrite').objectStore(PAIRING_STORE_NAME).put(stored),
47
- );
48
- return stored;
49
- }
50
-
51
- export async function getSigningAssets({
52
- deviceUDID,
53
- bundleID,
54
- signingMode = 'development',
55
- }: {
56
- deviceUDID?: string;
57
- bundleID?: string;
58
- signingMode?: DeviceInstallSigningMode;
59
- }) {
60
- const normalizedBundleID = normalizeBundleID(bundleID);
61
- if (!normalizedBundleID) return undefined;
62
- const normalizedUDID = normalizeUDID(deviceUDID);
63
- const bundleScoped = await getSigningAssetsByID(signingAssetID('bundle', normalizedBundleID, signingMode));
64
- if (bundleScoped) return bundleScoped;
65
- if (normalizedUDID) {
66
- const exact = await getSigningAssetsByID(signingAssetID(normalizedUDID, normalizedBundleID, signingMode));
67
- if (exact) return exact;
68
- }
69
- const candidates = await findSigningAssetsForBundle(normalizedBundleID, signingMode);
70
- return candidates[0];
71
- }
72
-
73
- export async function getLatestSigningAssets(signingMode: DeviceInstallSigningMode = 'development') {
74
- const all = await getAllSigningAssets();
75
- return all
76
- .filter((asset) => (asset.signingMode ?? 'development') === signingMode)
77
- .sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime())[0];
78
- }
79
-
80
- export async function getLatestSigningAssetsWithCertificate(
81
- teamID?: string,
82
- signingMode: DeviceInstallSigningMode = 'development',
83
- ) {
84
- const all = await getAllSigningAssets();
85
- return all
86
- .filter((asset) => {
87
- if (!asset.certificateID || !asset.certificateP12Base64) {
88
- return false;
89
- }
90
- if ((asset.signingMode ?? 'development') !== signingMode) {
91
- return false;
92
- }
93
- return !teamID || !asset.teamID || asset.teamID === teamID;
94
- })
95
- .sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime())[0];
96
- }
97
-
98
- export async function putSigningAssets(input: PutSigningAssetsInput) {
99
- const normalizedBundleID = normalizeBundleID(input.bundleID);
100
- if (!normalizedBundleID) {
101
- throw new Error('Cannot store signing assets without a bundle ID.');
102
- }
103
- const normalizedUDID = normalizeUDID(input.deviceUDID);
104
- const signingMode = input.signingMode ?? 'development';
105
- const id = signingAssetID('bundle', normalizedBundleID, signingMode);
106
- const stored: StoredSigningAssets = {
107
- ...input,
108
- id,
109
- deviceUDID: normalizedUDID || undefined,
110
- signingMode,
111
- bundleID: normalizedBundleID,
112
- updatedAt: new Date().toISOString(),
113
- };
114
- const db = await openDB(SIGNING_DB_NAME, SIGNING_DB_VERSION, SIGNING_STORE_NAME, 'id');
115
- await requestToPromise(
116
- db.transaction(SIGNING_STORE_NAME, 'readwrite').objectStore(SIGNING_STORE_NAME).put(stored),
117
- );
118
- return stored;
119
- }
120
-
121
- export async function findSigningAssetsForBundle(
122
- bundleID?: string,
123
- signingMode: DeviceInstallSigningMode = 'development',
124
- ) {
125
- const normalized = normalizeBundleID(bundleID);
126
- if (!normalized) return [];
127
- const all = await getAllSigningAssets();
128
- return all.filter(
129
- (asset) => asset.bundleID === normalized && (asset.signingMode ?? 'development') === signingMode,
130
- );
131
- }
132
-
133
- export function profileContainsDevice(profile: ProvisioningProfileInfo, deviceUDID?: string) {
134
- const normalized = normalizeUDID(deviceUDID);
135
- return !!normalized && profile.provisionedDevices.some((device) => normalizeUDID(device) === normalized);
136
- }
137
-
138
- export function profileMatchesBundleID(profile: ProvisioningProfileInfo, bundleID?: string) {
139
- const expected = normalizeBundleID(bundleID);
140
- const profileBundleID = normalizeBundleID(profile.bundleID);
141
- if (!expected || !profileBundleID) return false;
142
- if (profileBundleID === expected) return true;
143
- if (profileBundleID === '*') return true;
144
- if (!profileBundleID.endsWith('.*')) return false;
145
- const prefix = profileBundleID.slice(0, -1);
146
- return expected.startsWith(prefix);
147
- }
148
-
149
- export async function parseProvisioningProfile(file: File) {
150
- return parseProvisioningProfileBytes(new Uint8Array(await file.arrayBuffer()));
151
- }
152
-
153
- export function parseProvisioningProfileBase64(base64: string) {
154
- const binary = atob(base64);
155
- const bytes = new Uint8Array(binary.length);
156
- for (let index = 0; index < binary.length; index += 1) {
157
- bytes[index] = binary.charCodeAt(index);
158
- }
159
- return parseProvisioningProfileBytes(bytes);
160
- }
161
-
162
- export function parseProvisioningProfileBytes(bytes: Uint8Array) {
163
- const text = new TextDecoder('latin1').decode(bytes);
164
- const start = text.indexOf('<?xml');
165
- const end = text.indexOf('</plist>');
166
- if (start < 0 || end < start) {
167
- throw new Error('Provisioning profile plist not found.');
168
- }
169
- const xml = text.slice(start, end + '</plist>'.length);
170
- // Defensively reject entity declarations before parsing: they are the vector
171
- // for XML entity-expansion (billion-laughs) attacks and never appear in a
172
- // real provisioning profile. The standard Apple `<!DOCTYPE plist PUBLIC ...>`
173
- // (an external DTD reference with no internal entity subset) is benign —
174
- // browsers' DOMParser does not fetch it — so it is intentionally allowed.
175
- if (/<!ENTITY/i.test(xml)) {
176
- throw new Error('Provisioning profile plist contains a disallowed ENTITY declaration.');
177
- }
178
- const doc = new DOMParser().parseFromString(xml, 'application/xml');
179
- if (doc.querySelector('parsererror')) {
180
- throw new Error('Provisioning profile plist could not be parsed.');
181
- }
182
- const dict = doc.querySelector('plist > dict');
183
- if (!dict) {
184
- throw new Error('Provisioning profile plist dictionary not found.');
185
- }
186
- const value = readPlistValue(dict);
187
- if (!isRecord(value)) {
188
- throw new Error('Provisioning profile plist has an unexpected shape.');
189
- }
190
- const entitlements = isRecord(value.Entitlements) ? value.Entitlements : {};
191
- const applicationIdentifier = stringValue(entitlements['application-identifier']);
192
- const bundleID = bundleIDFromApplicationIdentifier(applicationIdentifier);
193
- return {
194
- name: stringValue(value.Name),
195
- uuid: stringValue(value.UUID),
196
- teamID:
197
- stringValue(entitlements['com.apple.developer.team-identifier']) ??
198
- stringArrayValue(value.TeamIdentifier)[0],
199
- applicationIdentifier,
200
- bundleID,
201
- provisionedDevices: stringArrayValue(value.ProvisionedDevices),
202
- certificateSerialNumbers: certificateSerialNumbers(stringArrayValue(value.DeveloperCertificates)),
203
- getTaskAllow: booleanValue(entitlements['get-task-allow']),
204
- expirationDate: stringValue(value.ExpirationDate),
205
- } satisfies ProvisioningProfileInfo;
206
- }
207
-
208
- /**
209
- * Normalizes a certificate serial number for comparison: uppercase hex
210
- * without leading zeros, the format Apple's portal reports in serialNum.
211
- */
212
- export function normalizeCertificateSerial(serial?: string) {
213
- const normalized = (serial ?? '').replace(/^0+/, '').toUpperCase();
214
- return normalized || undefined;
215
- }
216
-
217
- /**
218
- * Reads the serial numbers of the DER certificates embedded in a profile's
219
- * DeveloperCertificates array. Unparseable entries are skipped: a profile
220
- * with an exotic certificate should degrade to weaker filtering, not fail
221
- * the whole parse.
222
- */
223
- function certificateSerialNumbers(developerCertificatesBase64: string[]) {
224
- const serials: string[] = [];
225
- for (const entry of developerCertificatesBase64) {
226
- try {
227
- // Plist <data> payloads wrap base64 across lines; atob rejects whitespace.
228
- const der = atob(entry.replace(/\s+/g, ''));
229
- const certificate = forge.pki.certificateFromAsn1(forge.asn1.fromDer(forge.util.createBuffer(der)));
230
- const serial = normalizeCertificateSerial(certificate.serialNumber);
231
- if (serial && !serials.includes(serial)) serials.push(serial);
232
- } catch {
233
- // Skip certificates forge cannot parse.
234
- }
235
- }
236
- return serials;
237
- }
238
-
239
- async function getSigningAssetsByID(id?: string) {
240
- if (!id) return undefined;
241
- const db = await openDB(SIGNING_DB_NAME, SIGNING_DB_VERSION, SIGNING_STORE_NAME, 'id');
242
- return requestToPromise<StoredSigningAssets | undefined>(
243
- db.transaction(SIGNING_STORE_NAME, 'readonly').objectStore(SIGNING_STORE_NAME).get(id),
244
- );
245
- }
246
-
247
- async function getAllSigningAssets() {
248
- const db = await openDB(SIGNING_DB_NAME, SIGNING_DB_VERSION, SIGNING_STORE_NAME, 'id');
249
- return requestToPromise<StoredSigningAssets[]>(
250
- db.transaction(SIGNING_STORE_NAME, 'readonly').objectStore(SIGNING_STORE_NAME).getAll(),
251
- );
252
- }
253
-
254
- function signingAssetID(deviceUDID: string, bundleID: string, signingMode: DeviceInstallSigningMode) {
255
- return `${signingMode}:${deviceUDID}:${bundleID}`;
256
- }
257
-
258
- function bundleIDFromApplicationIdentifier(applicationIdentifier?: string) {
259
- if (!applicationIdentifier) return undefined;
260
- const dot = applicationIdentifier.indexOf('.');
261
- return dot >= 0 ? applicationIdentifier.slice(dot + 1) : undefined;
262
- }
263
-
264
- function readPlistValue(element: Element): unknown {
265
- switch (element.tagName) {
266
- case 'dict':
267
- return readPlistDict(element);
268
- case 'array':
269
- return Array.from(element.children).map(readPlistValue);
270
- case 'string':
271
- case 'date':
272
- return element.textContent ?? '';
273
- default:
274
- return element.textContent ?? '';
275
- }
276
- }
277
-
278
- function readPlistDict(dict: Element) {
279
- const result: Record<string, unknown> = {};
280
- const children = Array.from(dict.children);
281
- for (let index = 0; index < children.length; index += 2) {
282
- const key = children[index];
283
- const value = children[index + 1];
284
- if (!key || key.tagName !== 'key' || !value) continue;
285
- result[key.textContent ?? ''] = readPlistValue(value);
286
- }
287
- return result;
288
- }
289
-
290
- function stringValue(value: unknown) {
291
- return typeof value === 'string' && value ? value : undefined;
292
- }
293
-
294
- function stringArrayValue(value: unknown) {
295
- return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
296
- }
297
-
298
- function booleanValue(value: unknown) {
299
- return typeof value === 'boolean' ? value : undefined;
300
- }
301
-
302
- function isRecord(value: unknown): value is Record<string, unknown> {
303
- return typeof value === 'object' && value !== null && !Array.isArray(value);
304
- }
305
-
306
- function openDB(dbName: string, dbVersion: number, storeName: string, keyPath: string) {
307
- return new Promise<IDBDatabase>((resolve, reject) => {
308
- const request = indexedDB.open(dbName, dbVersion);
309
- request.onupgradeneeded = () => {
310
- const db = request.result;
311
- if (!db.objectStoreNames.contains(storeName)) {
312
- db.createObjectStore(storeName, { keyPath });
313
- }
314
- };
315
- request.onsuccess = () => resolve(request.result);
316
- request.onerror = () => reject(request.error ?? new Error('Open IndexedDB failed'));
317
- });
318
- }
319
-
320
- function requestToPromise<T = unknown>(request: IDBRequest<T>) {
321
- return new Promise<T>((resolve, reject) => {
322
- request.onsuccess = () => resolve(request.result);
323
- request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed'));
324
- });
325
- }
@@ -1,2 +0,0 @@
1
- export * from './browser-storage';
2
- export * from './secret-store';
@@ -1,332 +0,0 @@
1
- /**
2
- * A pluggable store for Apple signing secrets.
3
- *
4
- * Credential material always lands in the browser first (the Apple relay
5
- * only proxies); the browser then writes it into a store implementing this
6
- * interface. Limrun's org secret store is the default implementation, an
7
- * IndexedDB-backed one keeps everything local, and customers can bring
8
- * their own store by implementing the same interface.
9
- */
10
-
11
- export const APPLE_CERTIFICATE_SECRET_TYPE = 'appleCertificate';
12
- export const APPLE_PROVISIONING_PROFILE_SECRET_TYPE = 'appleProvisioningProfile';
13
- export const APP_STORE_CONNECT_API_KEY_SECRET_TYPE = 'appStoreConnectApiKey';
14
-
15
- export type SigningSecretType =
16
- | typeof APPLE_CERTIFICATE_SECRET_TYPE
17
- | typeof APPLE_PROVISIONING_PROFILE_SECRET_TYPE
18
- | typeof APP_STORE_CONNECT_API_KEY_SECRET_TYPE;
19
-
20
- export type SigningSecretMetadata = {
21
- type: string;
22
- name: string;
23
- createdAt?: string;
24
- };
25
-
26
- /** Type-specific flat key-value payload of a signing secret. */
27
- export type SigningSecretData = Record<string, string>;
28
-
29
- export type SigningSecret = SigningSecretMetadata & {
30
- data: SigningSecretData;
31
- };
32
-
33
- export interface SigningSecretStore {
34
- /**
35
- * Stores a secret. When a secret with the same type and name already
36
- * exists its data is overwritten. Returns the stored secret; callers
37
- * should use the returned data.
38
- */
39
- put(type: SigningSecretType, name: string, data: SigningSecretData): Promise<SigningSecret>;
40
- /** Returns the secret including its data, or undefined when absent. */
41
- get(type: SigningSecretType, name: string): Promise<SigningSecret | undefined>;
42
- /** Lists metadata of all stored signing secrets, never their data. */
43
- list(): Promise<SigningSecretMetadata[]>;
44
- /** Deletes a secret; resolves even when the secret does not exist. */
45
- delete(type: SigningSecretType, name: string): Promise<void>;
46
- }
47
-
48
- /**
49
- * Apple's certificate types as defined by the App Store Connect API
50
- * (https://developer.apple.com/documentation/appstoreconnectapi/certificatetype).
51
- */
52
- export type AppleCertificateType =
53
- | 'DEVELOPMENT'
54
- | 'DISTRIBUTION'
55
- | 'IOS_DEVELOPMENT'
56
- | 'IOS_DISTRIBUTION'
57
- | 'MAC_APP_DEVELOPMENT'
58
- | 'MAC_APP_DISTRIBUTION'
59
- | 'MAC_INSTALLER_DISTRIBUTION'
60
- | 'DEVELOPER_ID_APPLICATION'
61
- | 'DEVELOPER_ID_INSTALLER'
62
- | 'DEVELOPER_ID_KEXT'
63
- | 'PASS_TYPE_ID'
64
- | 'PASS_TYPE_ID_WITH_NFC';
65
-
66
- /** Data payload of an appleCertificate secret. */
67
- export type AppleCertificateSecretData = {
68
- certificateP12Base64: string;
69
- /** Which of Apple's certificate types the p12 holds. */
70
- certificateType: AppleCertificateType;
71
- certificatePassword?: string;
72
- teamID?: string;
73
- /** Apple's portal certificate id, never a Limrun DB id. */
74
- certificateID?: string;
75
- /** The certificate's serial number: uppercase hex, no leading zeros. */
76
- serialNumber?: string;
77
- expirationDate?: string;
78
- };
79
-
80
- /**
81
- * Data payload of an appleProvisioningProfile secret. The
82
- * certificateSerialNumbers, bundleIDs and deviceIDs fields duplicate what
83
- * the signed profile binds (as comma-separated lists) so profiles can be
84
- * filtered by certificate, bundle ID or device without parsing every
85
- * entry. Certificates are referenced by serial number, the identifier
86
- * Apple embeds in the profile itself.
87
- */
88
- export type AppleProvisioningProfileSecretData = {
89
- provisioningProfileBase64: string;
90
- certificateSerialNumbers?: string;
91
- bundleIDs?: string;
92
- deviceIDs?: string;
93
- teamID?: string;
94
- profileName?: string;
95
- uuid?: string;
96
- expirationDate?: string;
97
- };
98
-
99
- /**
100
- * Data payload of an appStoreConnectApiKey secret: the private half of an
101
- * App Store Connect API key plus the identifiers needed to sign JWTs with
102
- * it. Apple serves the private key exactly once, so the stored copy is the
103
- * only one.
104
- */
105
- export type AppStoreConnectApiKeySecretData = {
106
- /** Base64 of the .p8 private key PEM. */
107
- privateKeyP8Base64: string;
108
- /** Apple's key ID, e.g. 2X9R4HXF34. */
109
- keyId: string;
110
- /** Issuer ID for team keys. Absent for individual keys. */
111
- issuerId?: string;
112
- nickname?: string;
113
- teamID?: string;
114
- expirationDate?: string;
115
- };
116
-
117
- function compactData(data: Record<string, string | undefined>): SigningSecretData {
118
- const result: SigningSecretData = {};
119
- for (const [key, value] of Object.entries(data)) {
120
- if (value) result[key] = value;
121
- }
122
- return result;
123
- }
124
-
125
- /** Stores an Apple certificate bundle, conventionally named `${teamID}/${certificateType}`. */
126
- export async function putAppleCertificateSecret(
127
- store: SigningSecretStore,
128
- name: string,
129
- data: AppleCertificateSecretData,
130
- ) {
131
- return store.put(APPLE_CERTIFICATE_SECRET_TYPE, name, compactData(data));
132
- }
133
-
134
- /**
135
- * Stores a provisioning profile, conventionally named `${teamID}/${uuid}`
136
- * so a team can hold many profiles for the same bundle ID and certificate
137
- * set; consumers select by the reference fields, not by name.
138
- */
139
- export async function putAppleProvisioningProfileSecret(
140
- store: SigningSecretStore,
141
- name: string,
142
- data: AppleProvisioningProfileSecretData,
143
- ) {
144
- return store.put(APPLE_PROVISIONING_PROFILE_SECRET_TYPE, name, compactData(data));
145
- }
146
-
147
- /**
148
- * Stores an App Store Connect API key, conventionally named
149
- * `${teamID}/APP_STORE_CONNECT_API_KEY`: one shared key per team.
150
- */
151
- export async function putAppStoreConnectApiKeySecret(
152
- store: SigningSecretStore,
153
- name: string,
154
- data: AppStoreConnectApiKeySecretData,
155
- ) {
156
- return store.put(APP_STORE_CONNECT_API_KEY_SECRET_TYPE, name, compactData(data));
157
- }
158
-
159
- export type LimrunSecretStoreOptions = {
160
- /** Base URL of the Limrun backend API, e.g. https://api.limrun.com */
161
- apiUrl: string;
162
- /** Bearer token: a user or organization token. */
163
- token: string;
164
- /** Organization TID owning the secrets. */
165
- organizationId: string;
166
- /** Custom fetch, mainly for tests. */
167
- fetch?: typeof fetch;
168
- };
169
-
170
- type BackendSecretResult = {
171
- id: string;
172
- type: string;
173
- name: string;
174
- organizationId: string;
175
- data: SigningSecretData;
176
- createdAt: string;
177
- };
178
-
179
- /**
180
- * A SigningSecretStore backed by Limrun's organization secret store
181
- * (`/v1/organizations/{org}/secrets`). Secrets are stored server-side and
182
- * shared across the organization.
183
- */
184
- export function createLimrunSecretStore(options: LimrunSecretStoreOptions): SigningSecretStore {
185
- const doFetch = options.fetch ?? fetch.bind(globalThis);
186
- const base = options.apiUrl.replace(/\/+$/, '');
187
- const secretUrl = (type: string, name: string) =>
188
- `${base}/v1/organizations/${encodeURIComponent(options.organizationId)}/secrets/${encodeURIComponent(
189
- type,
190
- )}/${encodeURIComponent(name)}`;
191
- const headers = {
192
- Authorization: `Bearer ${options.token}`,
193
- 'Content-Type': 'application/json',
194
- };
195
-
196
- async function fail(response: Response, action: string): Promise<never> {
197
- let message = `${response.status}`;
198
- try {
199
- const body = (await response.json()) as { message?: string };
200
- if (body.message) message = body.message;
201
- } catch {
202
- // Non-JSON error body; the status code is the best we have.
203
- }
204
- throw new Error(`Failed to ${action} secret: ${message}`);
205
- }
206
-
207
- return {
208
- async put(type, name, data) {
209
- const response = await doFetch(secretUrl(type, name), {
210
- method: 'PUT',
211
- headers,
212
- body: JSON.stringify({ data, replace: true }),
213
- });
214
- if (!response.ok) await fail(response, 'store');
215
- const result = (await response.json()) as BackendSecretResult;
216
- return { type: result.type, name: result.name, createdAt: result.createdAt, data: result.data };
217
- },
218
- async get(type, name) {
219
- const response = await doFetch(secretUrl(type, name), { method: 'GET', headers });
220
- if (response.status === 404) return undefined;
221
- if (!response.ok) await fail(response, 'fetch');
222
- const result = (await response.json()) as BackendSecretResult;
223
- return { type: result.type, name: result.name, createdAt: result.createdAt, data: result.data };
224
- },
225
- async list() {
226
- const response = await doFetch(
227
- `${base}/v1/organizations/${encodeURIComponent(options.organizationId)}/secrets`,
228
- { method: 'GET', headers },
229
- );
230
- if (!response.ok) await fail(response, 'list');
231
- const result = (await response.json()) as Omit<BackendSecretResult, 'data'>[];
232
- return result
233
- .filter(
234
- (s) =>
235
- s.type === APPLE_CERTIFICATE_SECRET_TYPE ||
236
- s.type === APPLE_PROVISIONING_PROFILE_SECRET_TYPE ||
237
- s.type === APP_STORE_CONNECT_API_KEY_SECRET_TYPE,
238
- )
239
- .map((s) => ({ type: s.type, name: s.name, createdAt: s.createdAt }));
240
- },
241
- async delete(type, name) {
242
- const response = await doFetch(secretUrl(type, name), { method: 'DELETE', headers });
243
- if (response.status === 404) return;
244
- if (!response.ok) await fail(response, 'delete');
245
- },
246
- };
247
- }
248
-
249
- const SECRETS_DB_NAME = 'limrun-signing-secrets';
250
- const SECRETS_DB_VERSION = 1;
251
- const SECRETS_STORE_NAME = 'secrets';
252
-
253
- type BrowserStoredSecret = {
254
- id: string;
255
- type: string;
256
- name: string;
257
- data: SigningSecretData;
258
- createdAt: string;
259
- };
260
-
261
- /**
262
- * A SigningSecretStore keeping everything in the browser's IndexedDB.
263
- * Nothing leaves the machine; secrets are per-browser-profile.
264
- */
265
- export function createBrowserSecretStore(): SigningSecretStore {
266
- const idOf = (type: string, name: string) => `${type}:${name}`;
267
-
268
- function open() {
269
- return new Promise<IDBDatabase>((resolve, reject) => {
270
- const request = indexedDB.open(SECRETS_DB_NAME, SECRETS_DB_VERSION);
271
- request.onupgradeneeded = () => {
272
- const db = request.result;
273
- if (!db.objectStoreNames.contains(SECRETS_STORE_NAME)) {
274
- db.createObjectStore(SECRETS_STORE_NAME, { keyPath: 'id' });
275
- }
276
- };
277
- request.onsuccess = () => resolve(request.result);
278
- request.onerror = () => reject(request.error ?? new Error('Open IndexedDB failed'));
279
- });
280
- }
281
-
282
- function toPromise<T = unknown>(request: IDBRequest<T>) {
283
- return new Promise<T>((resolve, reject) => {
284
- request.onsuccess = () => resolve(request.result);
285
- request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed'));
286
- });
287
- }
288
-
289
- return {
290
- async put(type, name, data) {
291
- const db = await open();
292
- const existing = await toPromise<BrowserStoredSecret | undefined>(
293
- db.transaction(SECRETS_STORE_NAME, 'readonly').objectStore(SECRETS_STORE_NAME).get(idOf(type, name)),
294
- );
295
- const stored: BrowserStoredSecret = {
296
- id: idOf(type, name),
297
- type,
298
- name,
299
- data,
300
- createdAt: existing?.createdAt ?? new Date().toISOString(),
301
- };
302
- await toPromise(
303
- db.transaction(SECRETS_STORE_NAME, 'readwrite').objectStore(SECRETS_STORE_NAME).put(stored),
304
- );
305
- return { type, name, createdAt: stored.createdAt, data };
306
- },
307
- async get(type, name) {
308
- const db = await open();
309
- const stored = await toPromise<BrowserStoredSecret | undefined>(
310
- db.transaction(SECRETS_STORE_NAME, 'readonly').objectStore(SECRETS_STORE_NAME).get(idOf(type, name)),
311
- );
312
- if (!stored) return undefined;
313
- return { type: stored.type, name: stored.name, createdAt: stored.createdAt, data: stored.data };
314
- },
315
- async list() {
316
- const db = await open();
317
- const all = await toPromise<BrowserStoredSecret[]>(
318
- db.transaction(SECRETS_STORE_NAME, 'readonly').objectStore(SECRETS_STORE_NAME).getAll(),
319
- );
320
- return all.map((s) => ({ type: s.type, name: s.name, createdAt: s.createdAt }));
321
- },
322
- async delete(type, name) {
323
- const db = await open();
324
- await toPromise(
325
- db
326
- .transaction(SECRETS_STORE_NAME, 'readwrite')
327
- .objectStore(SECRETS_STORE_NAME)
328
- .delete(idOf(type, name)),
329
- );
330
- },
331
- };
332
- }