summaryrefslogtreecommitdiff
path: root/src/jet/dependencies/net.ts
blob: dd7fdb997ca4647b6498a4568d9ea00efceb2e4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import type { Network, FetchRequest, FetchResponse } from '@jet/environment';
import { fromEntries } from '@amp/web-apps-utils';

import {
    shouldUseSearchJWT,
    makeSearchJWTAuthorizationHeader,
} from '~/config/media-api';

const CORRELATION_KEY_HEADER = 'x-apple-jingle-correlation-key';

type FetchFunction = typeof window.fetch;

// TODO: these URLs are also referenced in `bag` definition; we should have a single
// source-of-truth for these domains
const MEDIA_API_ORIGINS = [
    'https://amp-api.apps.apple.com',
    'https://amp-api-edge.apps.apple.com',
    'https://amp-api-search-edge.apps.apple.com',
];

export interface FeaturesCallbacks {
    getITFEValues(): string | undefined;
}

export class Net implements Network {
    private readonly underlyingFetch: FetchFunction;
    private readonly getITFEValues: () => string | undefined = () => undefined;

    constructor(
        underlyingFetch: FetchFunction,
        featuresCallbacks?: FeaturesCallbacks,
    ) {
        this.underlyingFetch = underlyingFetch;
        this.getITFEValues =
            featuresCallbacks?.getITFEValues ?? this.getITFEValues;
    }

    async fetch(request: FetchRequest): Promise<FetchResponse> {
        const requestStartTime = getTimestampMs();
        const requestURL = new URL(request.url);

        request.headers = request.headers ?? {};

        if (MEDIA_API_ORIGINS.includes(requestURL.origin)) {
            // Need to fake this for the server due to Kong origin checks.
            // Has no effect clientside.
            request.headers['origin'] = 'https://apps.apple.com';

            const itfe = this.getITFEValues?.();

            if (itfe) {
                // Add ITFE value as query string when set
                requestURL.searchParams.set('itfe', itfe);
            }
        }

        // The App Store Client will have already injected the JWT from the
        // `media-token-service` ObjectGraph dependency into the headers. However,
        // some endpoints need a different JWT. Here we determine if that's the
        // case and override the existing JWT if necessary.
        if (shouldUseSearchJWT(requestURL)) {
            request.headers = {
                ...request.headers,
                ...makeSearchJWTAuthorizationHeader(),
            };
        }

        // TODO: rdar://78158575: timeout
        const response = await this.underlyingFetch(requestURL.toString(), {
            ...request,
            cache: request.cache ?? undefined,
            credentials: 'include',
            headers: request.headers ?? undefined,
            method: request.method ?? undefined,
        });

        const responseStartTime = getTimestampMs();

        const { ok, redirected, status, statusText, url } = response;

        const headers = fromEntries(response.headers);
        const body = await response.text();

        const responseEndTime = getTimestampMs();

        return {
            ok,
            headers,
            redirected,
            status,
            statusText,
            url,
            body,
            // TODO: rdar://78158575: redirect: 'manual' to get all metrics?
            metrics: [
                {
                    clientCorrelationKey: response.headers.get(
                        CORRELATION_KEY_HEADER,
                    ),
                    pageURL: response.url,
                    requestStartTime,
                    responseStartTime,
                    responseEndTime,
                    // TODO: rdar://78158575: responseWasCached?
                    // TODO: rdar://78158575: parseStartTime/parseEndTime
                },
            ],
        };
    }
}

/**
 * Returns the current UTC timestamp in milliseconds.
 */
function getTimestampMs(): number {
    return Date.now();
}