1import { ExpoUpdatesManifest } from '@expo/config'; 2import { Updates } from '@expo/config-plugins'; 3import assert from 'assert'; 4import { v4 as uuidv4 } from 'uuid'; 5 6import { getProjectAsync } from '../../../api/getProject'; 7import { APISettings } from '../../../api/settings'; 8import { signExpoGoManifestAsync } from '../../../api/signManifest'; 9import UserSettings from '../../../api/user/UserSettings'; 10import { ANONYMOUS_USERNAME, getUserAsync } from '../../../api/user/user'; 11import { logEvent } from '../../../utils/analytics/rudderstackClient'; 12import { CommandError } from '../../../utils/errors'; 13import { memoize } from '../../../utils/fn'; 14import { stripPort } from '../../../utils/url'; 15import { ManifestMiddleware, ParsedHeaders } from './ManifestMiddleware'; 16import { 17 assertMissingRuntimePlatform, 18 assertRuntimePlatform, 19 parsePlatformHeader, 20} from './resolvePlatform'; 21import { ServerHeaders, ServerRequest } from './server.types'; 22 23export class ExpoGoManifestHandlerMiddleware extends ManifestMiddleware { 24 public getParsedHeaders(req: ServerRequest): ParsedHeaders { 25 const platform = parsePlatformHeader(req); 26 assertMissingRuntimePlatform(platform); 27 assertRuntimePlatform(platform); 28 29 return { 30 platform, 31 acceptSignature: !!req.headers['expo-accept-signature'], 32 hostname: stripPort(req.headers['host']), 33 }; 34 } 35 36 protected getDefaultResponseHeaders(): Map<string, any> { 37 const headers = new Map<string, any>(); 38 // set required headers for Expo Updates manifest specification 39 headers.set('expo-protocol-version', 0); 40 headers.set('expo-sfv-version', 0); 41 headers.set('cache-control', 'private, max-age=0'); 42 headers.set('content-type', 'application/json'); 43 return headers; 44 } 45 46 public async _getManifestResponseAsync(requestOptions: ParsedHeaders): Promise<{ 47 body: string; 48 version: string; 49 headers: ServerHeaders; 50 }> { 51 const { exp, hostUri, expoGoConfig, bundleUrl } = await this._resolveProjectSettingsAsync( 52 requestOptions 53 ); 54 55 const runtimeVersion = Updates.getRuntimeVersion( 56 { ...exp, runtimeVersion: exp.runtimeVersion ?? { policy: 'sdkVersion' } }, 57 requestOptions.platform 58 ); 59 if (!runtimeVersion) { 60 throw new CommandError( 61 'MANIFEST_MIDDLEWARE', 62 `Unable to determine runtime version for platform '${requestOptions.platform}'` 63 ); 64 } 65 66 const easProjectId = exp.extra?.eas?.projectId; 67 const shouldUseAnonymousManifest = await shouldUseAnonymousManifestAsync(easProjectId); 68 const userAnonymousIdentifier = await UserSettings.getAnonymousIdentifierAsync(); 69 if (!shouldUseAnonymousManifest) { 70 assert(easProjectId); 71 } 72 const scopeKey = shouldUseAnonymousManifest 73 ? `@${ANONYMOUS_USERNAME}/${exp.slug}-${userAnonymousIdentifier}` 74 : await this.getScopeKeyForProjectIdAsync(easProjectId); 75 76 const expoUpdatesManifest: ExpoUpdatesManifest = { 77 id: uuidv4(), 78 createdAt: new Date().toISOString(), 79 runtimeVersion, 80 launchAsset: { 81 key: 'bundle', 82 contentType: 'application/javascript', 83 url: bundleUrl, 84 }, 85 assets: [], // assets are not used in development 86 metadata: {}, // required for the client to detect that this is an expo-updates manifest 87 extra: { 88 eas: { 89 projectId: easProjectId ?? undefined, 90 }, 91 expoClient: { 92 ...exp, 93 hostUri, 94 }, 95 expoGo: expoGoConfig, 96 scopeKey, 97 }, 98 }; 99 100 const headers = this.getDefaultResponseHeaders(); 101 if (requestOptions.acceptSignature && !shouldUseAnonymousManifest) { 102 const manifestSignature = await this.getSignedManifestStringAsync(expoUpdatesManifest); 103 headers.set('expo-manifest-signature', manifestSignature); 104 } 105 106 return { 107 body: JSON.stringify(expoUpdatesManifest), 108 version: runtimeVersion, 109 headers, 110 }; 111 } 112 113 protected trackManifest(version?: string) { 114 logEvent('Serve Expo Updates Manifest', { 115 runtimeVersion: version, 116 }); 117 } 118 119 private getSignedManifestStringAsync = memoize(signExpoGoManifestAsync); 120 121 private getScopeKeyForProjectIdAsync = memoize(getScopeKeyForProjectIdAsync); 122} 123 124/** 125 * Whether an anonymous scope key should be used. It should be used when: 126 * 1. Offline 127 * 2. Not logged-in 128 * 3. No EAS project ID in config 129 */ 130async function shouldUseAnonymousManifestAsync( 131 easProjectId: string | undefined | null 132): Promise<boolean> { 133 if (!easProjectId || APISettings.isOffline) { 134 return true; 135 } 136 137 return !(await getUserAsync()); 138} 139 140async function getScopeKeyForProjectIdAsync(projectId: string): Promise<string> { 141 const project = await getProjectAsync(projectId); 142 return project.scopeKey; 143} 144