@chidchanun/bcp 0.1.6 → 0.1.8
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/CHANGELOG.md +61 -1
- package/README.md +185 -1
- package/docs/server-request-apis.md +404 -0
- package/docs/session-auth.md +186 -0
- package/package.json +6 -1
- package/packages/bundler/src/client-boundary.ts +8 -6
- package/packages/bundler/src/server-production.ts +17 -0
- package/packages/client/src/server.ts +36 -0
- package/packages/server/src/index.ts +32 -13
- package/packages/server/src/request-context.ts +1043 -0
- package/packages/server/src/server-response.ts +82 -0
- package/packages/server/src/session.ts +694 -0
- package/packages/server/src/standalone-production-runtime-v2.ts +44 -13
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHmac,
|
|
3
|
+
timingSafeEqual,
|
|
4
|
+
} from "node:crypto";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
cookies,
|
|
8
|
+
type CookieSameSite,
|
|
9
|
+
} from "./request-context.js";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_COOKIE_NAME =
|
|
12
|
+
"bcp_session";
|
|
13
|
+
const DEFAULT_EXPIRES_IN =
|
|
14
|
+
60 * 60 * 12;
|
|
15
|
+
const MAX_TOKEN_LENGTH =
|
|
16
|
+
16 * 1024;
|
|
17
|
+
const MINIMUM_SECRET_BYTES =
|
|
18
|
+
32;
|
|
19
|
+
|
|
20
|
+
export interface SessionPayload {
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SessionClaims
|
|
25
|
+
extends SessionPayload {
|
|
26
|
+
iat: number;
|
|
27
|
+
exp: number;
|
|
28
|
+
iss?: string;
|
|
29
|
+
aud?:
|
|
30
|
+
string |
|
|
31
|
+
string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SessionTokenOptions {
|
|
35
|
+
secret?: string;
|
|
36
|
+
expiresIn?: number;
|
|
37
|
+
issuer?: string;
|
|
38
|
+
audience?:
|
|
39
|
+
string |
|
|
40
|
+
string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SessionCookieOptions
|
|
44
|
+
extends SessionTokenOptions {
|
|
45
|
+
cookieName?: string;
|
|
46
|
+
path?: string;
|
|
47
|
+
domain?: string;
|
|
48
|
+
httpOnly?: boolean;
|
|
49
|
+
secure?: boolean;
|
|
50
|
+
sameSite?:
|
|
51
|
+
CookieSameSite;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function createSessionToken<
|
|
55
|
+
T extends object
|
|
56
|
+
>(
|
|
57
|
+
payload: T,
|
|
58
|
+
options:
|
|
59
|
+
SessionTokenOptions = {}
|
|
60
|
+
): Promise<string> {
|
|
61
|
+
assertPayload(
|
|
62
|
+
payload
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const secret =
|
|
66
|
+
resolveSessionSecret(
|
|
67
|
+
options.secret
|
|
68
|
+
);
|
|
69
|
+
const expiresIn =
|
|
70
|
+
resolveExpiresIn(
|
|
71
|
+
options.expiresIn
|
|
72
|
+
);
|
|
73
|
+
const issuer =
|
|
74
|
+
resolveOptionalIssuer(
|
|
75
|
+
options.issuer
|
|
76
|
+
);
|
|
77
|
+
const audience =
|
|
78
|
+
options.audience ===
|
|
79
|
+
undefined
|
|
80
|
+
? undefined
|
|
81
|
+
: normalizeAudience(
|
|
82
|
+
options.audience
|
|
83
|
+
);
|
|
84
|
+
const now =
|
|
85
|
+
Math.floor(
|
|
86
|
+
Date.now() /
|
|
87
|
+
1000
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const header = {
|
|
91
|
+
alg:
|
|
92
|
+
"HS256",
|
|
93
|
+
typ:
|
|
94
|
+
"JWT",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const claims:
|
|
98
|
+
SessionClaims = {
|
|
99
|
+
...payload,
|
|
100
|
+
iat:
|
|
101
|
+
now,
|
|
102
|
+
exp:
|
|
103
|
+
now +
|
|
104
|
+
expiresIn,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
issuer !==
|
|
109
|
+
undefined
|
|
110
|
+
) {
|
|
111
|
+
claims.iss =
|
|
112
|
+
issuer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
audience !==
|
|
117
|
+
undefined
|
|
118
|
+
) {
|
|
119
|
+
claims.aud =
|
|
120
|
+
audience;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const encodedHeader =
|
|
124
|
+
encodeJson(
|
|
125
|
+
header
|
|
126
|
+
);
|
|
127
|
+
const encodedPayload =
|
|
128
|
+
encodeJson(
|
|
129
|
+
claims
|
|
130
|
+
);
|
|
131
|
+
const signingInput =
|
|
132
|
+
`${encodedHeader}.${encodedPayload}`;
|
|
133
|
+
const signature =
|
|
134
|
+
signHs256(
|
|
135
|
+
signingInput,
|
|
136
|
+
secret
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return `${signingInput}.${signature}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function verifySessionToken<
|
|
143
|
+
T extends object =
|
|
144
|
+
SessionPayload
|
|
145
|
+
>(
|
|
146
|
+
token: string,
|
|
147
|
+
options:
|
|
148
|
+
SessionTokenOptions = {}
|
|
149
|
+
): Promise<(
|
|
150
|
+
T & SessionClaims
|
|
151
|
+
) | null> {
|
|
152
|
+
const secret =
|
|
153
|
+
resolveSessionSecret(
|
|
154
|
+
options.secret
|
|
155
|
+
);
|
|
156
|
+
const issuer =
|
|
157
|
+
resolveOptionalIssuer(
|
|
158
|
+
options.issuer
|
|
159
|
+
);
|
|
160
|
+
const audience =
|
|
161
|
+
options.audience ===
|
|
162
|
+
undefined
|
|
163
|
+
? undefined
|
|
164
|
+
: normalizeAudience(
|
|
165
|
+
options.audience
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
if (
|
|
170
|
+
typeof token !==
|
|
171
|
+
"string" ||
|
|
172
|
+
token.length === 0 ||
|
|
173
|
+
token.length >
|
|
174
|
+
MAX_TOKEN_LENGTH
|
|
175
|
+
) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const parts =
|
|
180
|
+
token.split(".");
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
parts.length !==
|
|
184
|
+
3
|
|
185
|
+
) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const [
|
|
190
|
+
encodedHeader,
|
|
191
|
+
encodedPayload,
|
|
192
|
+
signature,
|
|
193
|
+
] = parts;
|
|
194
|
+
|
|
195
|
+
const header =
|
|
196
|
+
decodeJson(
|
|
197
|
+
encodedHeader
|
|
198
|
+
) as Record<
|
|
199
|
+
string,
|
|
200
|
+
unknown
|
|
201
|
+
>;
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
header.alg !==
|
|
205
|
+
"HS256" ||
|
|
206
|
+
(
|
|
207
|
+
header.typ !==
|
|
208
|
+
undefined &&
|
|
209
|
+
header.typ !==
|
|
210
|
+
"JWT"
|
|
211
|
+
)
|
|
212
|
+
) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const signingInput =
|
|
217
|
+
`${encodedHeader}.${encodedPayload}`;
|
|
218
|
+
const expectedSignature =
|
|
219
|
+
signHs256(
|
|
220
|
+
signingInput,
|
|
221
|
+
secret
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (
|
|
225
|
+
!safeEqual(
|
|
226
|
+
signature,
|
|
227
|
+
expectedSignature
|
|
228
|
+
)
|
|
229
|
+
) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const claims =
|
|
234
|
+
decodeJson(
|
|
235
|
+
encodedPayload
|
|
236
|
+
) as Record<
|
|
237
|
+
string,
|
|
238
|
+
unknown
|
|
239
|
+
>;
|
|
240
|
+
const now =
|
|
241
|
+
Math.floor(
|
|
242
|
+
Date.now() /
|
|
243
|
+
1000
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
typeof claims.iat !==
|
|
248
|
+
"number" ||
|
|
249
|
+
!Number.isFinite(
|
|
250
|
+
claims.iat
|
|
251
|
+
) ||
|
|
252
|
+
typeof claims.exp !==
|
|
253
|
+
"number" ||
|
|
254
|
+
!Number.isFinite(
|
|
255
|
+
claims.exp
|
|
256
|
+
) ||
|
|
257
|
+
claims.exp <=
|
|
258
|
+
now
|
|
259
|
+
) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (
|
|
264
|
+
claims.nbf !==
|
|
265
|
+
undefined
|
|
266
|
+
) {
|
|
267
|
+
if (
|
|
268
|
+
typeof claims.nbf !==
|
|
269
|
+
"number" ||
|
|
270
|
+
!Number.isFinite(
|
|
271
|
+
claims.nbf
|
|
272
|
+
) ||
|
|
273
|
+
claims.nbf >
|
|
274
|
+
now
|
|
275
|
+
) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (
|
|
281
|
+
issuer !==
|
|
282
|
+
undefined &&
|
|
283
|
+
claims.iss !==
|
|
284
|
+
issuer
|
|
285
|
+
) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (
|
|
290
|
+
audience !==
|
|
291
|
+
undefined &&
|
|
292
|
+
!audienceMatches(
|
|
293
|
+
claims.aud,
|
|
294
|
+
audience
|
|
295
|
+
)
|
|
296
|
+
) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return claims as
|
|
301
|
+
T & SessionClaims;
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function createSession<
|
|
308
|
+
T extends object
|
|
309
|
+
>(
|
|
310
|
+
payload: T,
|
|
311
|
+
options:
|
|
312
|
+
SessionCookieOptions = {}
|
|
313
|
+
): Promise<string> {
|
|
314
|
+
const expiresIn =
|
|
315
|
+
resolveExpiresIn(
|
|
316
|
+
options.expiresIn
|
|
317
|
+
);
|
|
318
|
+
const token =
|
|
319
|
+
await createSessionToken(
|
|
320
|
+
payload,
|
|
321
|
+
{
|
|
322
|
+
secret:
|
|
323
|
+
options.secret,
|
|
324
|
+
expiresIn,
|
|
325
|
+
issuer:
|
|
326
|
+
options.issuer,
|
|
327
|
+
audience:
|
|
328
|
+
options.audience,
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
const cookieStore =
|
|
332
|
+
await cookies();
|
|
333
|
+
|
|
334
|
+
cookieStore.set(
|
|
335
|
+
resolveCookieName(
|
|
336
|
+
options.cookieName
|
|
337
|
+
),
|
|
338
|
+
token,
|
|
339
|
+
{
|
|
340
|
+
httpOnly:
|
|
341
|
+
options.httpOnly ??
|
|
342
|
+
true,
|
|
343
|
+
secure:
|
|
344
|
+
options.secure ??
|
|
345
|
+
process.env.NODE_ENV ===
|
|
346
|
+
"production",
|
|
347
|
+
sameSite:
|
|
348
|
+
options.sameSite ??
|
|
349
|
+
"lax",
|
|
350
|
+
path:
|
|
351
|
+
options.path ??
|
|
352
|
+
"/",
|
|
353
|
+
domain:
|
|
354
|
+
options.domain,
|
|
355
|
+
maxAge:
|
|
356
|
+
expiresIn,
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
return token;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export async function getSession<
|
|
364
|
+
T extends object =
|
|
365
|
+
SessionPayload
|
|
366
|
+
>(
|
|
367
|
+
options:
|
|
368
|
+
SessionCookieOptions = {}
|
|
369
|
+
): Promise<(
|
|
370
|
+
T & SessionClaims
|
|
371
|
+
) | null> {
|
|
372
|
+
const cookieStore =
|
|
373
|
+
await cookies();
|
|
374
|
+
const token =
|
|
375
|
+
cookieStore.get(
|
|
376
|
+
resolveCookieName(
|
|
377
|
+
options.cookieName
|
|
378
|
+
)
|
|
379
|
+
)?.value;
|
|
380
|
+
|
|
381
|
+
if (!token) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return verifySessionToken<
|
|
386
|
+
T
|
|
387
|
+
>(
|
|
388
|
+
token,
|
|
389
|
+
{
|
|
390
|
+
secret:
|
|
391
|
+
options.secret,
|
|
392
|
+
issuer:
|
|
393
|
+
options.issuer,
|
|
394
|
+
audience:
|
|
395
|
+
options.audience,
|
|
396
|
+
}
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function destroySession(
|
|
401
|
+
options:
|
|
402
|
+
SessionCookieOptions = {}
|
|
403
|
+
): Promise<void> {
|
|
404
|
+
const cookieStore =
|
|
405
|
+
await cookies();
|
|
406
|
+
|
|
407
|
+
cookieStore.delete(
|
|
408
|
+
resolveCookieName(
|
|
409
|
+
options.cookieName
|
|
410
|
+
),
|
|
411
|
+
{
|
|
412
|
+
path:
|
|
413
|
+
options.path ??
|
|
414
|
+
"/",
|
|
415
|
+
domain:
|
|
416
|
+
options.domain,
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function resolveSessionSecret(
|
|
422
|
+
explicitSecret:
|
|
423
|
+
string | undefined
|
|
424
|
+
): string {
|
|
425
|
+
const secret =
|
|
426
|
+
explicitSecret ??
|
|
427
|
+
process.env.BCP_SESSION_SECRET;
|
|
428
|
+
|
|
429
|
+
if (!secret) {
|
|
430
|
+
throw new Error(
|
|
431
|
+
"BCP Framework: BCP_SESSION_SECRET is required for session tokens."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (
|
|
436
|
+
Buffer.byteLength(
|
|
437
|
+
secret,
|
|
438
|
+
"utf8"
|
|
439
|
+
) <
|
|
440
|
+
MINIMUM_SECRET_BYTES
|
|
441
|
+
) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`BCP Framework: session secret must be at least ${MINIMUM_SECRET_BYTES} bytes.`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return secret;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function resolveExpiresIn(
|
|
451
|
+
value:
|
|
452
|
+
number | undefined
|
|
453
|
+
): number {
|
|
454
|
+
const expiresIn =
|
|
455
|
+
value ??
|
|
456
|
+
DEFAULT_EXPIRES_IN;
|
|
457
|
+
|
|
458
|
+
if (
|
|
459
|
+
!Number.isFinite(
|
|
460
|
+
expiresIn
|
|
461
|
+
) ||
|
|
462
|
+
expiresIn <= 0
|
|
463
|
+
) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
"BCP Framework: session expiresIn must be a positive finite number of seconds."
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return Math.floor(
|
|
470
|
+
expiresIn
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function resolveCookieName(
|
|
475
|
+
value:
|
|
476
|
+
string | undefined
|
|
477
|
+
): string {
|
|
478
|
+
const cookieName =
|
|
479
|
+
value ??
|
|
480
|
+
DEFAULT_COOKIE_NAME;
|
|
481
|
+
|
|
482
|
+
assertNonEmptyString(
|
|
483
|
+
cookieName,
|
|
484
|
+
"cookieName"
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
return cookieName;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function resolveOptionalIssuer(
|
|
491
|
+
value:
|
|
492
|
+
string | undefined
|
|
493
|
+
): string | undefined {
|
|
494
|
+
if (
|
|
495
|
+
value ===
|
|
496
|
+
undefined
|
|
497
|
+
) {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
assertNonEmptyString(
|
|
502
|
+
value,
|
|
503
|
+
"issuer"
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function assertPayload(
|
|
510
|
+
payload:
|
|
511
|
+
object
|
|
512
|
+
): void {
|
|
513
|
+
if (
|
|
514
|
+
payload === null ||
|
|
515
|
+
typeof payload !==
|
|
516
|
+
"object" ||
|
|
517
|
+
Array.isArray(
|
|
518
|
+
payload
|
|
519
|
+
)
|
|
520
|
+
) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
"BCP Framework: session payload must be a plain object."
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function assertNonEmptyString(
|
|
528
|
+
value: string,
|
|
529
|
+
label: string
|
|
530
|
+
): void {
|
|
531
|
+
if (
|
|
532
|
+
typeof value !==
|
|
533
|
+
"string" ||
|
|
534
|
+
value.trim().length ===
|
|
535
|
+
0
|
|
536
|
+
) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`BCP Framework: session ${label} must be a non-empty string.`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function normalizeAudience(
|
|
544
|
+
value:
|
|
545
|
+
string |
|
|
546
|
+
string[]
|
|
547
|
+
): string | string[] {
|
|
548
|
+
if (
|
|
549
|
+
typeof value ===
|
|
550
|
+
"string"
|
|
551
|
+
) {
|
|
552
|
+
assertNonEmptyString(
|
|
553
|
+
value,
|
|
554
|
+
"audience"
|
|
555
|
+
);
|
|
556
|
+
return value;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (
|
|
560
|
+
!Array.isArray(
|
|
561
|
+
value
|
|
562
|
+
) ||
|
|
563
|
+
value.length ===
|
|
564
|
+
0
|
|
565
|
+
) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
"BCP Framework: session audience must contain at least one value."
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
for (
|
|
572
|
+
const audience
|
|
573
|
+
of value
|
|
574
|
+
) {
|
|
575
|
+
assertNonEmptyString(
|
|
576
|
+
audience,
|
|
577
|
+
"audience"
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return [
|
|
582
|
+
...value,
|
|
583
|
+
];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function audienceMatches(
|
|
587
|
+
claim: unknown,
|
|
588
|
+
expected:
|
|
589
|
+
string |
|
|
590
|
+
string[]
|
|
591
|
+
): boolean {
|
|
592
|
+
const expectedValues =
|
|
593
|
+
typeof expected ===
|
|
594
|
+
"string"
|
|
595
|
+
? [
|
|
596
|
+
expected,
|
|
597
|
+
]
|
|
598
|
+
: expected;
|
|
599
|
+
const claimValues =
|
|
600
|
+
typeof claim ===
|
|
601
|
+
"string"
|
|
602
|
+
? [
|
|
603
|
+
claim,
|
|
604
|
+
]
|
|
605
|
+
: Array.isArray(
|
|
606
|
+
claim
|
|
607
|
+
)
|
|
608
|
+
? claim.filter(
|
|
609
|
+
(
|
|
610
|
+
value
|
|
611
|
+
): value is string =>
|
|
612
|
+
typeof value ===
|
|
613
|
+
"string"
|
|
614
|
+
)
|
|
615
|
+
: [];
|
|
616
|
+
|
|
617
|
+
return expectedValues.some(
|
|
618
|
+
(value) =>
|
|
619
|
+
claimValues.includes(
|
|
620
|
+
value
|
|
621
|
+
)
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function encodeJson(
|
|
626
|
+
value: unknown
|
|
627
|
+
): string {
|
|
628
|
+
return Buffer.from(
|
|
629
|
+
JSON.stringify(
|
|
630
|
+
value
|
|
631
|
+
),
|
|
632
|
+
"utf8"
|
|
633
|
+
).toString(
|
|
634
|
+
"base64url"
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function decodeJson(
|
|
639
|
+
value: string
|
|
640
|
+
): unknown {
|
|
641
|
+
const decoded =
|
|
642
|
+
Buffer.from(
|
|
643
|
+
value,
|
|
644
|
+
"base64url"
|
|
645
|
+
).toString(
|
|
646
|
+
"utf8"
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
return JSON.parse(
|
|
650
|
+
decoded
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function signHs256(
|
|
655
|
+
signingInput: string,
|
|
656
|
+
secret: string
|
|
657
|
+
): string {
|
|
658
|
+
return createHmac(
|
|
659
|
+
"sha256",
|
|
660
|
+
secret
|
|
661
|
+
)
|
|
662
|
+
.update(
|
|
663
|
+
signingInput,
|
|
664
|
+
"utf8"
|
|
665
|
+
)
|
|
666
|
+
.digest(
|
|
667
|
+
"base64url"
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function safeEqual(
|
|
672
|
+
actual: string,
|
|
673
|
+
expected: string
|
|
674
|
+
): boolean {
|
|
675
|
+
const actualBuffer =
|
|
676
|
+
Buffer.from(
|
|
677
|
+
actual,
|
|
678
|
+
"base64url"
|
|
679
|
+
);
|
|
680
|
+
const expectedBuffer =
|
|
681
|
+
Buffer.from(
|
|
682
|
+
expected,
|
|
683
|
+
"base64url"
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
return (
|
|
687
|
+
actualBuffer.length ===
|
|
688
|
+
expectedBuffer.length &&
|
|
689
|
+
timingSafeEqual(
|
|
690
|
+
actualBuffer,
|
|
691
|
+
expectedBuffer
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
}
|