@chidchanun/bcp 0.1.6 → 0.1.7
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 +31 -1
- package/README.md +77 -1
- package/docs/server-request-apis.md +404 -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 +23 -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/standalone-production-runtime-v2.ts +44 -13
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AsyncLocalStorage,
|
|
3
|
+
} from "node:async_hooks";
|
|
4
|
+
import {
|
|
5
|
+
randomUUID,
|
|
6
|
+
} from "node:crypto";
|
|
7
|
+
import {
|
|
8
|
+
isIP,
|
|
9
|
+
} from "node:net";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* =====================================
|
|
13
|
+
* Public Types
|
|
14
|
+
* =====================================
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface RequestCookie {
|
|
18
|
+
name: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type CookieSameSite =
|
|
23
|
+
| "lax"
|
|
24
|
+
| "strict"
|
|
25
|
+
| "none";
|
|
26
|
+
|
|
27
|
+
export interface ResponseCookieOptions {
|
|
28
|
+
domain?: string;
|
|
29
|
+
|
|
30
|
+
path?: string;
|
|
31
|
+
|
|
32
|
+
expires?:
|
|
33
|
+
Date;
|
|
34
|
+
|
|
35
|
+
maxAge?:
|
|
36
|
+
number;
|
|
37
|
+
|
|
38
|
+
httpOnly?:
|
|
39
|
+
boolean;
|
|
40
|
+
|
|
41
|
+
secure?:
|
|
42
|
+
boolean;
|
|
43
|
+
|
|
44
|
+
sameSite?:
|
|
45
|
+
CookieSameSite;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ResponseCookie
|
|
49
|
+
extends
|
|
50
|
+
RequestCookie,
|
|
51
|
+
ResponseCookieOptions {}
|
|
52
|
+
|
|
53
|
+
export interface RequestCookieStore {
|
|
54
|
+
get(
|
|
55
|
+
name: string
|
|
56
|
+
): RequestCookie | undefined;
|
|
57
|
+
|
|
58
|
+
getAll(
|
|
59
|
+
name?: string
|
|
60
|
+
): RequestCookie[];
|
|
61
|
+
|
|
62
|
+
has(
|
|
63
|
+
name: string
|
|
64
|
+
): boolean;
|
|
65
|
+
|
|
66
|
+
set(
|
|
67
|
+
name: string,
|
|
68
|
+
value: string,
|
|
69
|
+
options?:
|
|
70
|
+
ResponseCookieOptions
|
|
71
|
+
): void;
|
|
72
|
+
|
|
73
|
+
set(
|
|
74
|
+
cookie:
|
|
75
|
+
ResponseCookie
|
|
76
|
+
): void;
|
|
77
|
+
|
|
78
|
+
delete(
|
|
79
|
+
name: string,
|
|
80
|
+
options?: {
|
|
81
|
+
domain?: string;
|
|
82
|
+
path?: string;
|
|
83
|
+
}
|
|
84
|
+
): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ClientIpOptions {
|
|
88
|
+
trustProxy?:
|
|
89
|
+
boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface BcpRequestContext {
|
|
93
|
+
url:
|
|
94
|
+
URL;
|
|
95
|
+
|
|
96
|
+
method:
|
|
97
|
+
string;
|
|
98
|
+
|
|
99
|
+
requestId:
|
|
100
|
+
string;
|
|
101
|
+
|
|
102
|
+
remoteAddress:
|
|
103
|
+
string | null;
|
|
104
|
+
|
|
105
|
+
headers:
|
|
106
|
+
Headers;
|
|
107
|
+
|
|
108
|
+
cookies:
|
|
109
|
+
BcpRequestCookieStore;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface RequestContextOptions {
|
|
113
|
+
remoteAddress?:
|
|
114
|
+
string | null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const requestStorage =
|
|
118
|
+
new AsyncLocalStorage<
|
|
119
|
+
BcpRequestContext
|
|
120
|
+
>();
|
|
121
|
+
|
|
122
|
+
export async function requestUrl():
|
|
123
|
+
Promise<URL> {
|
|
124
|
+
const context =
|
|
125
|
+
getRequestContext();
|
|
126
|
+
|
|
127
|
+
return new URL(
|
|
128
|
+
context.url.href
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function requestMethod():
|
|
133
|
+
Promise<string> {
|
|
134
|
+
return getRequestContext()
|
|
135
|
+
.method;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function requestId():
|
|
139
|
+
Promise<string> {
|
|
140
|
+
return getRequestContext()
|
|
141
|
+
.requestId;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function bearerToken():
|
|
145
|
+
Promise<string | null> {
|
|
146
|
+
const authorization =
|
|
147
|
+
getRequestContext()
|
|
148
|
+
.headers.get(
|
|
149
|
+
"authorization"
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!authorization) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const match =
|
|
157
|
+
/^Bearer\s+(.+)$/i.exec(
|
|
158
|
+
authorization.trim()
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (!match) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const token =
|
|
166
|
+
match[1].trim();
|
|
167
|
+
|
|
168
|
+
return token.length > 0
|
|
169
|
+
? token
|
|
170
|
+
: null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function clientIp(
|
|
174
|
+
options:
|
|
175
|
+
ClientIpOptions = {}
|
|
176
|
+
): Promise<string | null> {
|
|
177
|
+
const context =
|
|
178
|
+
getRequestContext();
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
options.trustProxy ===
|
|
182
|
+
true
|
|
183
|
+
) {
|
|
184
|
+
const forwarded =
|
|
185
|
+
parseForwardedFor(
|
|
186
|
+
context.headers
|
|
187
|
+
.get(
|
|
188
|
+
"forwarded"
|
|
189
|
+
)
|
|
190
|
+
) ??
|
|
191
|
+
parseXForwardedFor(
|
|
192
|
+
context.headers
|
|
193
|
+
.get(
|
|
194
|
+
"x-forwarded-for"
|
|
195
|
+
)
|
|
196
|
+
) ??
|
|
197
|
+
normalizeIpAddress(
|
|
198
|
+
context.headers
|
|
199
|
+
.get(
|
|
200
|
+
"x-real-ip"
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (forwarded) {
|
|
205
|
+
return forwarded;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return normalizeIpAddress(
|
|
210
|
+
context.remoteAddress
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function headers():
|
|
215
|
+
Promise<Headers> {
|
|
216
|
+
const context =
|
|
217
|
+
getRequestContext();
|
|
218
|
+
|
|
219
|
+
return new Headers(
|
|
220
|
+
context.headers
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function cookies():
|
|
225
|
+
Promise<RequestCookieStore> {
|
|
226
|
+
const context =
|
|
227
|
+
getRequestContext();
|
|
228
|
+
|
|
229
|
+
return context.cookies;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function runWithRequestContext<
|
|
233
|
+
T
|
|
234
|
+
>(
|
|
235
|
+
request: Request,
|
|
236
|
+
callback: () => T,
|
|
237
|
+
options:
|
|
238
|
+
RequestContextOptions = {}
|
|
239
|
+
): T {
|
|
240
|
+
const requestHeaders =
|
|
241
|
+
new Headers(
|
|
242
|
+
request.headers
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const cookieStore =
|
|
246
|
+
new BcpRequestCookieStore(
|
|
247
|
+
requestHeaders.get(
|
|
248
|
+
"cookie"
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const incomingRequestId =
|
|
253
|
+
normalizeRequestId(
|
|
254
|
+
requestHeaders.get(
|
|
255
|
+
"x-request-id"
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
return requestStorage.run(
|
|
260
|
+
{
|
|
261
|
+
url:
|
|
262
|
+
new URL(
|
|
263
|
+
request.url
|
|
264
|
+
),
|
|
265
|
+
|
|
266
|
+
method:
|
|
267
|
+
request.method
|
|
268
|
+
.toUpperCase(),
|
|
269
|
+
|
|
270
|
+
requestId:
|
|
271
|
+
incomingRequestId ??
|
|
272
|
+
randomUUID(),
|
|
273
|
+
|
|
274
|
+
remoteAddress:
|
|
275
|
+
options.remoteAddress ??
|
|
276
|
+
null,
|
|
277
|
+
|
|
278
|
+
headers:
|
|
279
|
+
requestHeaders,
|
|
280
|
+
|
|
281
|
+
cookies:
|
|
282
|
+
cookieStore,
|
|
283
|
+
},
|
|
284
|
+
callback
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function applyResponseCookies(
|
|
289
|
+
response: Response
|
|
290
|
+
): Response {
|
|
291
|
+
const context =
|
|
292
|
+
getRequestContext();
|
|
293
|
+
|
|
294
|
+
const setCookies =
|
|
295
|
+
context.cookies
|
|
296
|
+
.getPendingSetCookies();
|
|
297
|
+
|
|
298
|
+
if (
|
|
299
|
+
setCookies.length ===
|
|
300
|
+
0
|
|
301
|
+
) {
|
|
302
|
+
return response;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const responseHeaders =
|
|
306
|
+
new Headers(
|
|
307
|
+
response.headers
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
for (
|
|
311
|
+
const cookie
|
|
312
|
+
of setCookies
|
|
313
|
+
) {
|
|
314
|
+
responseHeaders.append(
|
|
315
|
+
"set-cookie",
|
|
316
|
+
cookie
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return new Response(
|
|
321
|
+
response.body,
|
|
322
|
+
{
|
|
323
|
+
status:
|
|
324
|
+
response.status,
|
|
325
|
+
|
|
326
|
+
statusText:
|
|
327
|
+
response.statusText,
|
|
328
|
+
|
|
329
|
+
headers:
|
|
330
|
+
responseHeaders,
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getRequestContext():
|
|
336
|
+
BcpRequestContext {
|
|
337
|
+
const context =
|
|
338
|
+
requestStorage.getStore();
|
|
339
|
+
|
|
340
|
+
if (!context) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
"BCP Framework: server request APIs can only be used while handling a request."
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return context;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function normalizeRequestId(
|
|
350
|
+
value:
|
|
351
|
+
string | null
|
|
352
|
+
): string | null {
|
|
353
|
+
if (!value) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const normalized =
|
|
358
|
+
value.trim();
|
|
359
|
+
|
|
360
|
+
if (
|
|
361
|
+
normalized.length ===
|
|
362
|
+
0 ||
|
|
363
|
+
normalized.length >
|
|
364
|
+
256 ||
|
|
365
|
+
/[\u0000-\u001F\u007F]/.test(
|
|
366
|
+
normalized
|
|
367
|
+
)
|
|
368
|
+
) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return normalized;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function parseXForwardedFor(
|
|
376
|
+
value:
|
|
377
|
+
string | null
|
|
378
|
+
): string | null {
|
|
379
|
+
if (!value) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (
|
|
384
|
+
const part
|
|
385
|
+
of value.split(",")
|
|
386
|
+
) {
|
|
387
|
+
const candidate =
|
|
388
|
+
normalizeIpAddress(
|
|
389
|
+
part
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (candidate) {
|
|
393
|
+
return candidate;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function parseForwardedFor(
|
|
401
|
+
value:
|
|
402
|
+
string | null
|
|
403
|
+
): string | null {
|
|
404
|
+
if (!value) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (
|
|
409
|
+
const forwardedElement
|
|
410
|
+
of value.split(",")
|
|
411
|
+
) {
|
|
412
|
+
for (
|
|
413
|
+
const parameter
|
|
414
|
+
of forwardedElement
|
|
415
|
+
.split(";")
|
|
416
|
+
) {
|
|
417
|
+
const separator =
|
|
418
|
+
parameter.indexOf("=");
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
separator <= 0 ||
|
|
422
|
+
parameter
|
|
423
|
+
.slice(
|
|
424
|
+
0,
|
|
425
|
+
separator
|
|
426
|
+
)
|
|
427
|
+
.trim()
|
|
428
|
+
.toLowerCase() !==
|
|
429
|
+
"for"
|
|
430
|
+
) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let candidate =
|
|
435
|
+
parameter
|
|
436
|
+
.slice(
|
|
437
|
+
separator + 1
|
|
438
|
+
)
|
|
439
|
+
.trim();
|
|
440
|
+
|
|
441
|
+
if (
|
|
442
|
+
candidate.startsWith(
|
|
443
|
+
"\""
|
|
444
|
+
) &&
|
|
445
|
+
candidate.endsWith(
|
|
446
|
+
"\""
|
|
447
|
+
)
|
|
448
|
+
) {
|
|
449
|
+
candidate =
|
|
450
|
+
candidate.slice(
|
|
451
|
+
1,
|
|
452
|
+
-1
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const normalized =
|
|
457
|
+
normalizeIpAddress(
|
|
458
|
+
candidate
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
if (normalized) {
|
|
462
|
+
return normalized;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function normalizeIpAddress(
|
|
471
|
+
value:
|
|
472
|
+
string | null
|
|
473
|
+
): string | null {
|
|
474
|
+
if (!value) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let normalized =
|
|
479
|
+
value.trim();
|
|
480
|
+
|
|
481
|
+
if (
|
|
482
|
+
normalized.length ===
|
|
483
|
+
0 ||
|
|
484
|
+
normalized.toLowerCase() ===
|
|
485
|
+
"unknown" ||
|
|
486
|
+
normalized.startsWith(
|
|
487
|
+
"_"
|
|
488
|
+
)
|
|
489
|
+
) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (
|
|
494
|
+
normalized.startsWith(
|
|
495
|
+
"["
|
|
496
|
+
)
|
|
497
|
+
) {
|
|
498
|
+
const closingBracket =
|
|
499
|
+
normalized.indexOf(
|
|
500
|
+
"]"
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (
|
|
504
|
+
closingBracket > 0
|
|
505
|
+
) {
|
|
506
|
+
normalized =
|
|
507
|
+
normalized.slice(
|
|
508
|
+
1,
|
|
509
|
+
closingBracket
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
const ipv4WithPort =
|
|
514
|
+
/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/.exec(
|
|
515
|
+
normalized
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
if (ipv4WithPort) {
|
|
519
|
+
normalized =
|
|
520
|
+
ipv4WithPort[1];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (
|
|
525
|
+
normalized.startsWith(
|
|
526
|
+
"::ffff:"
|
|
527
|
+
)
|
|
528
|
+
) {
|
|
529
|
+
normalized =
|
|
530
|
+
normalized.slice(
|
|
531
|
+
"::ffff:".length
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return isIP(
|
|
536
|
+
normalized
|
|
537
|
+
) > 0
|
|
538
|
+
? normalized
|
|
539
|
+
: null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
class BcpRequestCookieStore
|
|
543
|
+
implements RequestCookieStore {
|
|
544
|
+
private readonly items:
|
|
545
|
+
RequestCookie[];
|
|
546
|
+
|
|
547
|
+
private readonly pendingSetCookies:
|
|
548
|
+
string[] = [];
|
|
549
|
+
|
|
550
|
+
constructor(
|
|
551
|
+
cookieHeader:
|
|
552
|
+
string | null
|
|
553
|
+
) {
|
|
554
|
+
this.items =
|
|
555
|
+
parseCookieHeader(
|
|
556
|
+
cookieHeader
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
get(
|
|
561
|
+
name: string
|
|
562
|
+
):
|
|
563
|
+
RequestCookie |
|
|
564
|
+
undefined {
|
|
565
|
+
const cookie =
|
|
566
|
+
this.items.find(
|
|
567
|
+
(item) =>
|
|
568
|
+
item.name ===
|
|
569
|
+
name
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
if (!cookie) {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
...cookie,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
getAll(
|
|
582
|
+
name?: string
|
|
583
|
+
): RequestCookie[] {
|
|
584
|
+
const result =
|
|
585
|
+
name === undefined
|
|
586
|
+
? this.items
|
|
587
|
+
: this.items.filter(
|
|
588
|
+
(item) =>
|
|
589
|
+
item.name ===
|
|
590
|
+
name
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
return result.map(
|
|
594
|
+
(cookie) => ({
|
|
595
|
+
...cookie,
|
|
596
|
+
})
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
has(
|
|
601
|
+
name: string
|
|
602
|
+
): boolean {
|
|
603
|
+
return this.items.some(
|
|
604
|
+
(cookie) =>
|
|
605
|
+
cookie.name ===
|
|
606
|
+
name
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
set(
|
|
611
|
+
nameOrCookie:
|
|
612
|
+
string |
|
|
613
|
+
ResponseCookie,
|
|
614
|
+
|
|
615
|
+
value?: string,
|
|
616
|
+
|
|
617
|
+
options:
|
|
618
|
+
ResponseCookieOptions =
|
|
619
|
+
{}
|
|
620
|
+
): void {
|
|
621
|
+
let cookie:
|
|
622
|
+
ResponseCookie;
|
|
623
|
+
|
|
624
|
+
if (
|
|
625
|
+
typeof nameOrCookie ===
|
|
626
|
+
"string"
|
|
627
|
+
) {
|
|
628
|
+
if (
|
|
629
|
+
value ===
|
|
630
|
+
undefined
|
|
631
|
+
) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
"BCP Framework: cookie value is required."
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
cookie = {
|
|
638
|
+
name:
|
|
639
|
+
nameOrCookie,
|
|
640
|
+
|
|
641
|
+
value,
|
|
642
|
+
|
|
643
|
+
...options,
|
|
644
|
+
};
|
|
645
|
+
} else {
|
|
646
|
+
cookie = {
|
|
647
|
+
...nameOrCookie,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
validateCookieName(
|
|
652
|
+
cookie.name
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
validateCookieValue(
|
|
656
|
+
cookie.value
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
this.upsertLocalCookie(
|
|
660
|
+
cookie.name,
|
|
661
|
+
cookie.value
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
this.pendingSetCookies.push(
|
|
665
|
+
serializeCookie(
|
|
666
|
+
cookie
|
|
667
|
+
)
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
delete(
|
|
672
|
+
name: string,
|
|
673
|
+
options: {
|
|
674
|
+
domain?: string;
|
|
675
|
+
path?: string;
|
|
676
|
+
} = {}
|
|
677
|
+
): void {
|
|
678
|
+
validateCookieName(
|
|
679
|
+
name
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
for (
|
|
683
|
+
let index =
|
|
684
|
+
this.items.length - 1;
|
|
685
|
+
|
|
686
|
+
index >= 0;
|
|
687
|
+
|
|
688
|
+
index--
|
|
689
|
+
) {
|
|
690
|
+
if (
|
|
691
|
+
this.items[index]
|
|
692
|
+
.name ===
|
|
693
|
+
name
|
|
694
|
+
) {
|
|
695
|
+
this.items.splice(
|
|
696
|
+
index,
|
|
697
|
+
1
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
this.pendingSetCookies.push(
|
|
703
|
+
serializeCookie({
|
|
704
|
+
name,
|
|
705
|
+
|
|
706
|
+
value:
|
|
707
|
+
"",
|
|
708
|
+
|
|
709
|
+
path:
|
|
710
|
+
options.path ??
|
|
711
|
+
"/",
|
|
712
|
+
|
|
713
|
+
domain:
|
|
714
|
+
options.domain,
|
|
715
|
+
|
|
716
|
+
expires:
|
|
717
|
+
new Date(0),
|
|
718
|
+
|
|
719
|
+
maxAge:
|
|
720
|
+
0,
|
|
721
|
+
})
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
getPendingSetCookies():
|
|
726
|
+
string[] {
|
|
727
|
+
return [
|
|
728
|
+
...this.pendingSetCookies,
|
|
729
|
+
];
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private upsertLocalCookie(
|
|
733
|
+
name: string,
|
|
734
|
+
value: string
|
|
735
|
+
): void {
|
|
736
|
+
const existing =
|
|
737
|
+
this.items.find(
|
|
738
|
+
(cookie) =>
|
|
739
|
+
cookie.name ===
|
|
740
|
+
name
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
if (existing) {
|
|
744
|
+
existing.value =
|
|
745
|
+
value;
|
|
746
|
+
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
this.items.push({
|
|
751
|
+
name,
|
|
752
|
+
value,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function parseCookieHeader(
|
|
758
|
+
header:
|
|
759
|
+
string | null
|
|
760
|
+
): RequestCookie[] {
|
|
761
|
+
if (!header) {
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const result:
|
|
766
|
+
RequestCookie[] = [];
|
|
767
|
+
|
|
768
|
+
for (
|
|
769
|
+
const segment
|
|
770
|
+
of header.split(";")
|
|
771
|
+
) {
|
|
772
|
+
const separator =
|
|
773
|
+
segment.indexOf("=");
|
|
774
|
+
|
|
775
|
+
if (
|
|
776
|
+
separator <= 0
|
|
777
|
+
) {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const name =
|
|
782
|
+
segment
|
|
783
|
+
.slice(
|
|
784
|
+
0,
|
|
785
|
+
separator
|
|
786
|
+
)
|
|
787
|
+
.trim();
|
|
788
|
+
|
|
789
|
+
if (!name) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const rawValue =
|
|
794
|
+
segment
|
|
795
|
+
.slice(
|
|
796
|
+
separator + 1
|
|
797
|
+
)
|
|
798
|
+
.trim();
|
|
799
|
+
|
|
800
|
+
result.push({
|
|
801
|
+
name,
|
|
802
|
+
|
|
803
|
+
value:
|
|
804
|
+
decodeCookieValue(
|
|
805
|
+
rawValue
|
|
806
|
+
),
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return result;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function serializeCookie(
|
|
814
|
+
cookie:
|
|
815
|
+
ResponseCookie
|
|
816
|
+
): string {
|
|
817
|
+
validateCookieName(
|
|
818
|
+
cookie.name
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
validateCookieValue(
|
|
822
|
+
cookie.value
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
const parts:
|
|
826
|
+
string[] = [
|
|
827
|
+
`${cookie.name}=${encodeURIComponent(
|
|
828
|
+
cookie.value
|
|
829
|
+
)}`,
|
|
830
|
+
];
|
|
831
|
+
|
|
832
|
+
if (
|
|
833
|
+
cookie.domain !==
|
|
834
|
+
undefined
|
|
835
|
+
) {
|
|
836
|
+
validateCookieAttribute(
|
|
837
|
+
cookie.domain,
|
|
838
|
+
"domain"
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
parts.push(
|
|
842
|
+
`Domain=${cookie.domain}`
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (
|
|
847
|
+
cookie.path !==
|
|
848
|
+
undefined
|
|
849
|
+
) {
|
|
850
|
+
validateCookieAttribute(
|
|
851
|
+
cookie.path,
|
|
852
|
+
"path"
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
parts.push(
|
|
856
|
+
`Path=${cookie.path}`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (
|
|
861
|
+
cookie.expires !==
|
|
862
|
+
undefined
|
|
863
|
+
) {
|
|
864
|
+
if (
|
|
865
|
+
!(
|
|
866
|
+
cookie.expires
|
|
867
|
+
instanceof Date
|
|
868
|
+
) ||
|
|
869
|
+
Number.isNaN(
|
|
870
|
+
cookie.expires.getTime()
|
|
871
|
+
)
|
|
872
|
+
) {
|
|
873
|
+
throw new Error(
|
|
874
|
+
"BCP Framework: cookie expires must be a valid Date."
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
parts.push(
|
|
879
|
+
`Expires=${cookie.expires.toUTCString()}`
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (
|
|
884
|
+
cookie.maxAge !==
|
|
885
|
+
undefined
|
|
886
|
+
) {
|
|
887
|
+
if (
|
|
888
|
+
!Number.isFinite(
|
|
889
|
+
cookie.maxAge
|
|
890
|
+
)
|
|
891
|
+
) {
|
|
892
|
+
throw new Error(
|
|
893
|
+
"BCP Framework: cookie maxAge must be a finite number."
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
parts.push(
|
|
898
|
+
`Max-Age=${Math.trunc(
|
|
899
|
+
cookie.maxAge
|
|
900
|
+
)}`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (
|
|
905
|
+
cookie.httpOnly
|
|
906
|
+
) {
|
|
907
|
+
parts.push(
|
|
908
|
+
"HttpOnly"
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (
|
|
913
|
+
cookie.secure
|
|
914
|
+
) {
|
|
915
|
+
parts.push(
|
|
916
|
+
"Secure"
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (
|
|
921
|
+
cookie.sameSite !==
|
|
922
|
+
undefined
|
|
923
|
+
) {
|
|
924
|
+
parts.push(
|
|
925
|
+
`SameSite=${formatSameSite(
|
|
926
|
+
cookie.sameSite
|
|
927
|
+
)}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return parts.join(
|
|
932
|
+
"; "
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function validateCookieName(
|
|
937
|
+
name: string
|
|
938
|
+
): void {
|
|
939
|
+
if (
|
|
940
|
+
typeof name !==
|
|
941
|
+
"string" ||
|
|
942
|
+
name.length ===
|
|
943
|
+
0
|
|
944
|
+
) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
"BCP Framework: cookie name must be a non-empty string."
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (
|
|
951
|
+
!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(
|
|
952
|
+
name
|
|
953
|
+
)
|
|
954
|
+
) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`BCP Framework: invalid cookie name "${name}".`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function validateCookieValue(
|
|
962
|
+
value: string
|
|
963
|
+
): void {
|
|
964
|
+
if (
|
|
965
|
+
typeof value !==
|
|
966
|
+
"string"
|
|
967
|
+
) {
|
|
968
|
+
throw new Error(
|
|
969
|
+
"BCP Framework: cookie value must be a string."
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (
|
|
974
|
+
/[\u0000-\u001F\u007F]/.test(
|
|
975
|
+
value
|
|
976
|
+
)
|
|
977
|
+
) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
"BCP Framework: cookie value contains invalid control characters."
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function validateCookieAttribute(
|
|
985
|
+
value: string,
|
|
986
|
+
label: string
|
|
987
|
+
): void {
|
|
988
|
+
if (
|
|
989
|
+
typeof value !==
|
|
990
|
+
"string" ||
|
|
991
|
+
value.length ===
|
|
992
|
+
0
|
|
993
|
+
) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
`BCP Framework: cookie ${label} must be a non-empty string.`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (
|
|
1000
|
+
/[\r\n;]/.test(
|
|
1001
|
+
value
|
|
1002
|
+
)
|
|
1003
|
+
) {
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
`BCP Framework: invalid cookie ${label}.`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function formatSameSite(
|
|
1011
|
+
value:
|
|
1012
|
+
CookieSameSite
|
|
1013
|
+
): string {
|
|
1014
|
+
switch (value) {
|
|
1015
|
+
case "strict":
|
|
1016
|
+
return "Strict";
|
|
1017
|
+
|
|
1018
|
+
case "none":
|
|
1019
|
+
return "None";
|
|
1020
|
+
|
|
1021
|
+
case "lax":
|
|
1022
|
+
return "Lax";
|
|
1023
|
+
|
|
1024
|
+
default:
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`BCP Framework: invalid SameSite value "${String(
|
|
1027
|
+
value
|
|
1028
|
+
)}".`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function decodeCookieValue(
|
|
1034
|
+
value: string
|
|
1035
|
+
): string {
|
|
1036
|
+
try {
|
|
1037
|
+
return decodeURIComponent(
|
|
1038
|
+
value
|
|
1039
|
+
);
|
|
1040
|
+
} catch {
|
|
1041
|
+
return value;
|
|
1042
|
+
}
|
|
1043
|
+
}
|