@hraness/suite-accounts 0.9.20
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.
- package/LICENSE +21 -0
- package/README.md +496 -0
- package/compiler-foundation.css +1 -0
- package/dist/auth-client.js +16 -0
- package/dist/auth-proxy.js +757 -0
- package/dist/bearer-verifier.js +1119 -0
- package/dist/browser-session.js +208 -0
- package/dist/client-configuration.js +470 -0
- package/dist/convex-browser-auth-browser.js +923 -0
- package/dist/convex-browser-auth-server.js +846 -0
- package/dist/convex-browser-auth.js +597 -0
- package/dist/entitlements.js +502 -0
- package/dist/identity/functions.js +44 -0
- package/dist/identity/index.js +1716 -0
- package/dist/identity/return-targets.js +33 -0
- package/dist/index.js +1133 -0
- package/dist/oidc-device-code.js +253 -0
- package/dist/oidc-rp.js +2259 -0
- package/dist/oidc-session-policy.js +5 -0
- package/dist/oidc-surface-server.js +2332 -0
- package/dist/profile-form.js +1934 -0
- package/dist/profile.js +17 -0
- package/dist/public-config.js +462 -0
- package/dist/react.js +137 -0
- package/dist/receipt-verifier.js +619 -0
- package/dist/registry.js +327 -0
- package/dist/stylex-manifest.json +1 -0
- package/dist/stylex.css +435 -0
- package/dist/urls.js +391 -0
- package/package.json +263 -0
- package/portfolio-inventory.json +37 -0
- package/src/auth-client.ts +37 -0
- package/src/auth-proxy.ts +473 -0
- package/src/bearer-verifier.ts +478 -0
- package/src/browser-session.ts +260 -0
- package/src/client-configuration.ts +172 -0
- package/src/convex-browser-auth-browser.ts +238 -0
- package/src/convex-browser-auth-server.ts +492 -0
- package/src/convex-browser-auth.ts +208 -0
- package/src/convex-url.ts +74 -0
- package/src/entitlements.ts +317 -0
- package/src/identity/catalog.ts +227 -0
- package/src/identity/consumers.ts +53 -0
- package/src/identity/functions.ts +70 -0
- package/src/identity/identifiers.ts +82 -0
- package/src/identity/index.ts +10 -0
- package/src/identity/links.ts +280 -0
- package/src/identity/membership.ts +156 -0
- package/src/identity/principals.ts +301 -0
- package/src/identity/profiles-v2.ts +330 -0
- package/src/identity/profiles.ts +524 -0
- package/src/identity/return-targets.ts +20 -0
- package/src/identity/usernames.ts +101 -0
- package/src/identity/views.ts +289 -0
- package/src/immutable.ts +25 -0
- package/src/index.ts +7 -0
- package/src/oidc-continuation.ts +59 -0
- package/src/oidc-device-code.ts +401 -0
- package/src/oidc-rp.ts +2217 -0
- package/src/oidc-session-policy.ts +6 -0
- package/src/oidc-surface-server.ts +186 -0
- package/src/profile-form.css +1 -0
- package/src/profile-form.stylex.ts +226 -0
- package/src/profile-form.tsx +314 -0
- package/src/profile.ts +33 -0
- package/src/public-config.ts +294 -0
- package/src/public-profile-form-result.ts +82 -0
- package/src/public-profile-form.tsx +319 -0
- package/src/public-profile-links.ts +100 -0
- package/src/react.tsx +196 -0
- package/src/receipt-verifier.ts +273 -0
- package/src/registry.ts +600 -0
- package/src/urls.ts +234 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { deepFreeze } from "./immutable.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OAuth 2.0 Device Authorization Grant (RFC 8628) client-side protocol support.
|
|
5
|
+
*
|
|
6
|
+
* This module owns only the request/response shapes, parsing, and polling
|
|
7
|
+
* semantics used by suite CLI clients. The issuer-side device_authorization
|
|
8
|
+
* endpoint and device_code grant handler live in the Accounts authority.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const SUITE_OIDC_DEVICE_CODE_GRANT_TYPE =
|
|
12
|
+
"urn:ietf:params:oauth:grant-type:device_code" as const;
|
|
13
|
+
|
|
14
|
+
const MIN_DEVICE_CODE_INTERVAL_MS = 1_000;
|
|
15
|
+
const MAX_DEVICE_CODE_INTERVAL_MS = 600_000;
|
|
16
|
+
const MAX_USER_CODE_LENGTH = 256;
|
|
17
|
+
const MAX_DEVICE_CODE_LENGTH = 2_048;
|
|
18
|
+
const MAX_VERIFICATION_URI_LENGTH = 4_096;
|
|
19
|
+
|
|
20
|
+
export type SuiteOidcDeviceAuthorizationRequest = Readonly<{
|
|
21
|
+
/** Registered OIDC client id, e.g. `hraness:ghostget:production:v1`. */
|
|
22
|
+
clientId: string;
|
|
23
|
+
/** Optional space-delimited scope list; suite resource is implied by the client. */
|
|
24
|
+
scopes?: readonly string[];
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
export type SuiteOidcDeviceAuthorizationResponse = Readonly<{
|
|
28
|
+
/** Opaque device-bound code used by the CLI when polling the token endpoint. */
|
|
29
|
+
deviceCode: string;
|
|
30
|
+
/** Short human-readable code the user types or confirms in the browser. */
|
|
31
|
+
userCode: string;
|
|
32
|
+
/** Absolute browser URL for approving the device request. */
|
|
33
|
+
verificationUri: string;
|
|
34
|
+
/** Optional pre-formatted URI containing the user code. */
|
|
35
|
+
verificationUriComplete: string | null;
|
|
36
|
+
/** Absolute monotonic deadline after which the device code is expired. */
|
|
37
|
+
expiresAtMs: number;
|
|
38
|
+
/** Minimum polling interval in milliseconds. */
|
|
39
|
+
intervalMs: number;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
export type SuiteOidcDeviceTokenRequest = Readonly<{
|
|
43
|
+
clientId: string;
|
|
44
|
+
deviceCode: string;
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
export type SuiteOidcDeviceTokenSuccess = Readonly<{
|
|
48
|
+
kind: "token";
|
|
49
|
+
accessToken: string;
|
|
50
|
+
tokenType: string;
|
|
51
|
+
expiresAtMs: number;
|
|
52
|
+
refreshToken: string | null;
|
|
53
|
+
idToken: string | null;
|
|
54
|
+
scope: string | null;
|
|
55
|
+
}>;
|
|
56
|
+
|
|
57
|
+
export type SuiteOidcDevicePollOutcome =
|
|
58
|
+
| SuiteOidcDeviceTokenSuccess
|
|
59
|
+
| Readonly<{ kind: "authorization_pending"; intervalMs: number }>
|
|
60
|
+
| Readonly<{ kind: "slow_down"; intervalMs: number }>
|
|
61
|
+
| Readonly<{ kind: "access_denied" }>
|
|
62
|
+
| Readonly<{ kind: "expired_token" }>
|
|
63
|
+
| Readonly<{ kind: "error"; error: string; errorDescription: string | null }>;
|
|
64
|
+
|
|
65
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
66
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function boundedString(
|
|
70
|
+
value: unknown,
|
|
71
|
+
minimum: number,
|
|
72
|
+
maximum: number,
|
|
73
|
+
): value is string {
|
|
74
|
+
return typeof value === "string"
|
|
75
|
+
&& value.length >= minimum
|
|
76
|
+
&& value.length <= maximum
|
|
77
|
+
&& !value.includes("\u0000");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function safeInteger(value: unknown): value is number {
|
|
81
|
+
return typeof value === "number"
|
|
82
|
+
&& Number.isSafeInteger(value)
|
|
83
|
+
&& value >= 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function containsControlCharacter(value: string): boolean {
|
|
87
|
+
for (const character of value) {
|
|
88
|
+
const codePoint = character.codePointAt(0) ?? -1;
|
|
89
|
+
if (codePoint <= 0x1F || codePoint === 0x7F) return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeScope(value: unknown): string | null {
|
|
95
|
+
if (value === undefined || value === null) return null;
|
|
96
|
+
if (typeof value !== "string") return null;
|
|
97
|
+
const trimmed = value.trim();
|
|
98
|
+
if (trimmed.length === 0) return null;
|
|
99
|
+
if (trimmed.length > 4_096 || containsControlCharacter(trimmed)) return null;
|
|
100
|
+
return trimmed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function absoluteHttpsUri(value: unknown): string | null {
|
|
104
|
+
if (typeof value !== "string" || value.length === 0
|
|
105
|
+
|| value.length > MAX_VERIFICATION_URI_LENGTH) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const url = new URL(value);
|
|
110
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
111
|
+
if (url.username !== "" || url.password !== "") return null;
|
|
112
|
+
if (url.hash !== "") return null;
|
|
113
|
+
return url.href;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parse a device authorization endpoint response. Fails closed on any
|
|
121
|
+
* unsupported or malformed field.
|
|
122
|
+
*/
|
|
123
|
+
export function parseDeviceAuthorizationResponse(
|
|
124
|
+
value: unknown,
|
|
125
|
+
nowMs: number,
|
|
126
|
+
): SuiteOidcDeviceAuthorizationResponse | null {
|
|
127
|
+
if (!isRecord(value)) return null;
|
|
128
|
+
const deviceCode = value["device_code"];
|
|
129
|
+
const userCode = value["user_code"];
|
|
130
|
+
const verificationUri = value["verification_uri"];
|
|
131
|
+
const verificationUriComplete = value["verification_uri_complete"];
|
|
132
|
+
const expiresIn = value["expires_in"];
|
|
133
|
+
const interval = value["interval"];
|
|
134
|
+
if (
|
|
135
|
+
!boundedString(deviceCode, 1, MAX_DEVICE_CODE_LENGTH)
|
|
136
|
+
|| !boundedString(userCode, 1, MAX_USER_CODE_LENGTH)
|
|
137
|
+
|| !boundedString(verificationUri, 1, MAX_VERIFICATION_URI_LENGTH)
|
|
138
|
+
|| !safeInteger(expiresIn)
|
|
139
|
+
|| expiresIn <= 0
|
|
140
|
+
|| expiresIn > 86_400
|
|
141
|
+
|| !safeInteger(interval)
|
|
142
|
+
|| interval < 1
|
|
143
|
+
|| interval > MAX_DEVICE_CODE_INTERVAL_MS / 1_000
|
|
144
|
+
) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const verificationUriCompleteValue =
|
|
148
|
+
verificationUriComplete === undefined || verificationUriComplete === null
|
|
149
|
+
? null
|
|
150
|
+
: absoluteHttpsUri(verificationUriComplete);
|
|
151
|
+
if (
|
|
152
|
+
verificationUriComplete !== undefined
|
|
153
|
+
&& verificationUriComplete !== null
|
|
154
|
+
&& verificationUriCompleteValue === null
|
|
155
|
+
) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const intervalMs = Math.max(
|
|
159
|
+
MIN_DEVICE_CODE_INTERVAL_MS,
|
|
160
|
+
interval * 1_000,
|
|
161
|
+
);
|
|
162
|
+
return deepFreeze({
|
|
163
|
+
deviceCode,
|
|
164
|
+
userCode,
|
|
165
|
+
verificationUri,
|
|
166
|
+
verificationUriComplete: verificationUriCompleteValue,
|
|
167
|
+
expiresAtMs: nowMs + expiresIn * 1_000,
|
|
168
|
+
intervalMs,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Parse a token endpoint response for the device_code grant. Returns a stable
|
|
174
|
+
* outcome for both success and RFC 8628 / OAuth 2.0 error responses.
|
|
175
|
+
*/
|
|
176
|
+
export function parseDeviceTokenResponse(
|
|
177
|
+
value: unknown,
|
|
178
|
+
nowMs: number,
|
|
179
|
+
): SuiteOidcDevicePollOutcome | null {
|
|
180
|
+
if (!isRecord(value)) return null;
|
|
181
|
+
const error = value["error"];
|
|
182
|
+
const errorDescription = value["error_description"];
|
|
183
|
+
if (error !== undefined) {
|
|
184
|
+
if (typeof error !== "string" || error.length === 0 || error.length > 256) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const normalizedError = normalizeScope(error);
|
|
188
|
+
if (normalizedError === null || normalizedError !== error) return null;
|
|
189
|
+
const normalizedDescription =
|
|
190
|
+
errorDescription === undefined || errorDescription === null
|
|
191
|
+
? null
|
|
192
|
+
: normalizeScope(errorDescription);
|
|
193
|
+
if (
|
|
194
|
+
errorDescription !== undefined
|
|
195
|
+
&& errorDescription !== null
|
|
196
|
+
&& normalizedDescription === null
|
|
197
|
+
) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
switch (normalizedError) {
|
|
201
|
+
case "authorization_pending":
|
|
202
|
+
return deepFreeze({ kind: "authorization_pending", intervalMs: 5_000 });
|
|
203
|
+
case "slow_down":
|
|
204
|
+
return deepFreeze({ kind: "slow_down", intervalMs: 10_000 });
|
|
205
|
+
case "access_denied":
|
|
206
|
+
return deepFreeze({ kind: "access_denied" });
|
|
207
|
+
case "expired_token":
|
|
208
|
+
return deepFreeze({ kind: "expired_token" });
|
|
209
|
+
default:
|
|
210
|
+
return deepFreeze({
|
|
211
|
+
kind: "error",
|
|
212
|
+
error: normalizedError,
|
|
213
|
+
errorDescription: normalizedDescription,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const accessToken = value["access_token"];
|
|
218
|
+
const tokenType = value["token_type"];
|
|
219
|
+
const expiresIn = value["expires_in"];
|
|
220
|
+
const refreshToken = value["refresh_token"];
|
|
221
|
+
const idToken = value["id_token"];
|
|
222
|
+
const scope = value["scope"];
|
|
223
|
+
if (
|
|
224
|
+
!boundedString(accessToken, 1, 16_384)
|
|
225
|
+
|| !boundedString(tokenType, 1, 64)
|
|
226
|
+
|| !safeInteger(expiresIn)
|
|
227
|
+
|| expiresIn <= 0
|
|
228
|
+
|| expiresIn > 86_400
|
|
229
|
+
) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const normalizedRefreshToken =
|
|
233
|
+
refreshToken === undefined || refreshToken === null
|
|
234
|
+
? null
|
|
235
|
+
: boundedString(refreshToken, 1, 16_384)
|
|
236
|
+
? refreshToken
|
|
237
|
+
: null;
|
|
238
|
+
if (
|
|
239
|
+
refreshToken !== undefined
|
|
240
|
+
&& refreshToken !== null
|
|
241
|
+
&& normalizedRefreshToken === null
|
|
242
|
+
) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const normalizedIdToken =
|
|
246
|
+
idToken === undefined || idToken === null
|
|
247
|
+
? null
|
|
248
|
+
: boundedString(idToken, 1, 16_384) ? idToken : null;
|
|
249
|
+
if (
|
|
250
|
+
idToken !== undefined
|
|
251
|
+
&& idToken !== null
|
|
252
|
+
&& normalizedIdToken === null
|
|
253
|
+
) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const normalizedScope = normalizeScope(scope);
|
|
257
|
+
if (scope !== undefined && scope !== null && normalizedScope === null) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return deepFreeze({
|
|
261
|
+
kind: "token",
|
|
262
|
+
accessToken,
|
|
263
|
+
tokenType: tokenType.toLowerCase(),
|
|
264
|
+
expiresAtMs: nowMs + expiresIn * 1_000,
|
|
265
|
+
refreshToken: normalizedRefreshToken,
|
|
266
|
+
idToken: normalizedIdToken,
|
|
267
|
+
scope: normalizedScope,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type SuiteOidcDeviceFetch = (
|
|
272
|
+
input: string | URL | Request,
|
|
273
|
+
init?: RequestInit,
|
|
274
|
+
) => Promise<Response>;
|
|
275
|
+
|
|
276
|
+
export type SuiteOidcDeviceDependencies = Readonly<{
|
|
277
|
+
fetch?: SuiteOidcDeviceFetch;
|
|
278
|
+
now?: () => Date;
|
|
279
|
+
}>;
|
|
280
|
+
|
|
281
|
+
function deviceAuthorizationBody(
|
|
282
|
+
request: SuiteOidcDeviceAuthorizationRequest,
|
|
283
|
+
): string {
|
|
284
|
+
const params = new URLSearchParams();
|
|
285
|
+
params.set("client_id", request.clientId);
|
|
286
|
+
if (request.scopes !== undefined && request.scopes.length > 0) {
|
|
287
|
+
for (const scope of request.scopes) {
|
|
288
|
+
if (typeof scope !== "string" || scope.length === 0 || scope.length > 256
|
|
289
|
+
|| /\s/u.test(scope) || containsControlCharacter(scope)) {
|
|
290
|
+
throw new Error("Invalid device authorization scope.");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
params.set("scope", request.scopes.join(" "));
|
|
294
|
+
}
|
|
295
|
+
return params.toString();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function readBoundedJson(response: Response): Promise<unknown> {
|
|
299
|
+
const contentType = response.headers
|
|
300
|
+
.get("content-type")
|
|
301
|
+
?.split(";", 1)[0]
|
|
302
|
+
?.trim()
|
|
303
|
+
.toLowerCase();
|
|
304
|
+
if (contentType !== "application/json") {
|
|
305
|
+
throw new Error("The device-code response was not application/json.");
|
|
306
|
+
}
|
|
307
|
+
const text = await response.text();
|
|
308
|
+
if (text.length > 64 * 1_024) {
|
|
309
|
+
throw new Error("The device-code response body was too large.");
|
|
310
|
+
}
|
|
311
|
+
return JSON.parse(text) as unknown;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Initiate a device authorization request against the configured issuer
|
|
316
|
+
* endpoint. Returns the parsed response and a bound polling function.
|
|
317
|
+
*/
|
|
318
|
+
export async function initiateSuiteOidcDeviceAuthorization(
|
|
319
|
+
configuration: Readonly<{
|
|
320
|
+
deviceAuthorizationEndpoint: string;
|
|
321
|
+
deviceTokenEndpoint: string;
|
|
322
|
+
}>,
|
|
323
|
+
request: SuiteOidcDeviceAuthorizationRequest,
|
|
324
|
+
dependencies: SuiteOidcDeviceDependencies = {},
|
|
325
|
+
): Promise<
|
|
326
|
+
Readonly<{
|
|
327
|
+
poll: () => Promise<SuiteOidcDevicePollOutcome>;
|
|
328
|
+
response: SuiteOidcDeviceAuthorizationResponse;
|
|
329
|
+
}>
|
|
330
|
+
> {
|
|
331
|
+
const fetcher = dependencies.fetch ?? globalThis.fetch;
|
|
332
|
+
const now = dependencies.now?.().getTime() ?? Date.now();
|
|
333
|
+
const response = await fetcher(configuration.deviceAuthorizationEndpoint, {
|
|
334
|
+
body: deviceAuthorizationBody(request),
|
|
335
|
+
headers: {
|
|
336
|
+
"accept": "application/json",
|
|
337
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
338
|
+
},
|
|
339
|
+
method: "POST",
|
|
340
|
+
redirect: "error",
|
|
341
|
+
});
|
|
342
|
+
const body = await readBoundedJson(response);
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
throw new Error("The device authorization request failed.");
|
|
345
|
+
}
|
|
346
|
+
const parsed = parseDeviceAuthorizationResponse(body, now);
|
|
347
|
+
if (parsed === null) {
|
|
348
|
+
throw new Error("The device authorization response was invalid.");
|
|
349
|
+
}
|
|
350
|
+
const tokenRequest: SuiteOidcDeviceTokenRequest = {
|
|
351
|
+
clientId: request.clientId,
|
|
352
|
+
deviceCode: parsed.deviceCode,
|
|
353
|
+
};
|
|
354
|
+
return {
|
|
355
|
+
poll: () =>
|
|
356
|
+
pollSuiteOidcDeviceToken(configuration.deviceTokenEndpoint, tokenRequest, {
|
|
357
|
+
...(dependencies.now === undefined
|
|
358
|
+
? {}
|
|
359
|
+
: { now: dependencies.now }),
|
|
360
|
+
fetch: fetcher,
|
|
361
|
+
}),
|
|
362
|
+
response: parsed,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function tokenRequestBody(request: SuiteOidcDeviceTokenRequest): string {
|
|
367
|
+
const params = new URLSearchParams();
|
|
368
|
+
params.set("grant_type", SUITE_OIDC_DEVICE_CODE_GRANT_TYPE);
|
|
369
|
+
params.set("client_id", request.clientId);
|
|
370
|
+
params.set("device_code", request.deviceCode);
|
|
371
|
+
return params.toString();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Poll the token endpoint for a device_code grant outcome. Callers are expected
|
|
376
|
+
* to honor `intervalMs` from the previous `authorization_pending` or
|
|
377
|
+
* `slow_down` outcome, or from the initial device authorization response.
|
|
378
|
+
*/
|
|
379
|
+
export async function pollSuiteOidcDeviceToken(
|
|
380
|
+
tokenEndpoint: string,
|
|
381
|
+
request: SuiteOidcDeviceTokenRequest,
|
|
382
|
+
dependencies: SuiteOidcDeviceDependencies = {},
|
|
383
|
+
): Promise<SuiteOidcDevicePollOutcome> {
|
|
384
|
+
const fetcher = dependencies.fetch ?? globalThis.fetch;
|
|
385
|
+
const now = dependencies.now?.().getTime() ?? Date.now();
|
|
386
|
+
const response = await fetcher(tokenEndpoint, {
|
|
387
|
+
body: tokenRequestBody(request),
|
|
388
|
+
headers: {
|
|
389
|
+
"accept": "application/json",
|
|
390
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
391
|
+
},
|
|
392
|
+
method: "POST",
|
|
393
|
+
redirect: "error",
|
|
394
|
+
});
|
|
395
|
+
const body = await readBoundedJson(response);
|
|
396
|
+
const parsed = parseDeviceTokenResponse(body, now);
|
|
397
|
+
if (parsed === null) {
|
|
398
|
+
throw new Error("The device token response was invalid.");
|
|
399
|
+
}
|
|
400
|
+
return parsed;
|
|
401
|
+
}
|