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();
}
|