@effect/platform 0.48.15 → 0.48.17
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/Http/Cookies/package.json +6 -0
- package/README.md +235 -46
- package/dist/cjs/Http/Client.js +6 -1
- package/dist/cjs/Http/Client.js.map +1 -1
- package/dist/cjs/Http/ClientRequest.js.map +1 -1
- package/dist/cjs/Http/ClientResponse.js.map +1 -1
- package/dist/cjs/Http/Cookies.js +566 -0
- package/dist/cjs/Http/Cookies.js.map +1 -0
- package/dist/cjs/Http/Headers.js +2 -2
- package/dist/cjs/Http/Headers.js.map +1 -1
- package/dist/cjs/Http/IncomingMessage.js +12 -16
- package/dist/cjs/Http/IncomingMessage.js.map +1 -1
- package/dist/cjs/Http/Multipart.js.map +1 -1
- package/dist/cjs/Http/Router.js +11 -1
- package/dist/cjs/Http/Router.js.map +1 -1
- package/dist/cjs/Http/ServerRequest.js +6 -1
- package/dist/cjs/Http/ServerRequest.js.map +1 -1
- package/dist/cjs/Http/ServerResponse.js +36 -1
- package/dist/cjs/Http/ServerResponse.js.map +1 -1
- package/dist/cjs/Http/UrlParams.js +2 -2
- package/dist/cjs/Http/UrlParams.js.map +1 -1
- package/dist/cjs/HttpClient.js +3 -1
- package/dist/cjs/HttpClient.js.map +1 -1
- package/dist/cjs/HttpServer.js +3 -1
- package/dist/cjs/HttpServer.js.map +1 -1
- package/dist/cjs/internal/http/body.js +2 -2
- package/dist/cjs/internal/http/body.js.map +1 -1
- package/dist/cjs/internal/http/client.js +9 -3
- package/dist/cjs/internal/http/client.js.map +1 -1
- package/dist/cjs/internal/http/clientRequest.js +2 -2
- package/dist/cjs/internal/http/clientRequest.js.map +1 -1
- package/dist/cjs/internal/http/clientResponse.js +16 -8
- package/dist/cjs/internal/http/clientResponse.js.map +1 -1
- package/dist/cjs/internal/http/multipart.js +4 -4
- package/dist/cjs/internal/http/multipart.js.map +1 -1
- package/dist/cjs/internal/http/router.js +34 -7
- package/dist/cjs/internal/http/router.js.map +1 -1
- package/dist/cjs/internal/http/serverRequest.js +30 -15
- package/dist/cjs/internal/http/serverRequest.js.map +1 -1
- package/dist/cjs/internal/http/serverResponse.js +48 -23
- package/dist/cjs/internal/http/serverResponse.js.map +1 -1
- package/dist/dts/Error.d.ts.map +1 -1
- package/dist/dts/Http/App.d.ts +1 -1
- package/dist/dts/Http/App.d.ts.map +1 -1
- package/dist/dts/Http/Client.d.ts +13 -2
- package/dist/dts/Http/Client.d.ts.map +1 -1
- package/dist/dts/Http/ClientRequest.d.ts +2 -1
- package/dist/dts/Http/ClientRequest.d.ts.map +1 -1
- package/dist/dts/Http/ClientResponse.d.ts +7 -4
- package/dist/dts/Http/ClientResponse.d.ts.map +1 -1
- package/dist/dts/Http/Cookies.d.ts +253 -0
- package/dist/dts/Http/Cookies.d.ts.map +1 -0
- package/dist/dts/Http/Headers.d.ts +2 -2
- package/dist/dts/Http/Headers.d.ts.map +1 -1
- package/dist/dts/Http/IncomingMessage.d.ts +7 -6
- package/dist/dts/Http/IncomingMessage.d.ts.map +1 -1
- package/dist/dts/Http/Multipart.d.ts +3 -2
- package/dist/dts/Http/Multipart.d.ts.map +1 -1
- package/dist/dts/Http/Router.d.ts +29 -3
- package/dist/dts/Http/Router.d.ts.map +1 -1
- package/dist/dts/Http/ServerRequest.d.ts +14 -6
- package/dist/dts/Http/ServerRequest.d.ts.map +1 -1
- package/dist/dts/Http/ServerResponse.d.ts +89 -13
- package/dist/dts/Http/ServerResponse.d.ts.map +1 -1
- package/dist/dts/Http/UrlParams.d.ts +3 -2
- package/dist/dts/Http/UrlParams.d.ts.map +1 -1
- package/dist/dts/HttpClient.d.ts +8 -0
- package/dist/dts/HttpClient.d.ts.map +1 -1
- package/dist/dts/HttpServer.d.ts +8 -0
- package/dist/dts/HttpServer.d.ts.map +1 -1
- package/dist/dts/Socket.d.ts +3 -3
- package/dist/dts/Socket.d.ts.map +1 -1
- package/dist/dts/internal/http/router.d.ts.map +1 -1
- package/dist/esm/Http/Client.js +5 -0
- package/dist/esm/Http/Client.js.map +1 -1
- package/dist/esm/Http/ClientRequest.js.map +1 -1
- package/dist/esm/Http/ClientResponse.js.map +1 -1
- package/dist/esm/Http/Cookies.js +518 -0
- package/dist/esm/Http/Cookies.js.map +1 -0
- package/dist/esm/Http/Headers.js +2 -2
- package/dist/esm/Http/Headers.js.map +1 -1
- package/dist/esm/Http/IncomingMessage.js +12 -15
- package/dist/esm/Http/IncomingMessage.js.map +1 -1
- package/dist/esm/Http/Multipart.js.map +1 -1
- package/dist/esm/Http/Router.js +10 -0
- package/dist/esm/Http/Router.js.map +1 -1
- package/dist/esm/Http/ServerRequest.js +5 -0
- package/dist/esm/Http/ServerRequest.js.map +1 -1
- package/dist/esm/Http/ServerResponse.js +35 -0
- package/dist/esm/Http/ServerResponse.js.map +1 -1
- package/dist/esm/Http/UrlParams.js +2 -2
- package/dist/esm/Http/UrlParams.js.map +1 -1
- package/dist/esm/HttpClient.js +8 -0
- package/dist/esm/HttpClient.js.map +1 -1
- package/dist/esm/HttpServer.js +8 -0
- package/dist/esm/HttpServer.js.map +1 -1
- package/dist/esm/internal/http/body.js +2 -2
- package/dist/esm/internal/http/body.js.map +1 -1
- package/dist/esm/internal/http/client.js +8 -2
- package/dist/esm/internal/http/client.js.map +1 -1
- package/dist/esm/internal/http/clientRequest.js +2 -2
- package/dist/esm/internal/http/clientRequest.js.map +1 -1
- package/dist/esm/internal/http/clientResponse.js +16 -8
- package/dist/esm/internal/http/clientResponse.js.map +1 -1
- package/dist/esm/internal/http/multipart.js +4 -4
- package/dist/esm/internal/http/multipart.js.map +1 -1
- package/dist/esm/internal/http/router.js +31 -6
- package/dist/esm/internal/http/router.js.map +1 -1
- package/dist/esm/internal/http/serverRequest.js +28 -14
- package/dist/esm/internal/http/serverRequest.js.map +1 -1
- package/dist/esm/internal/http/serverResponse.js +47 -22
- package/dist/esm/internal/http/serverResponse.js.map +1 -1
- package/package.json +12 -4
- package/src/Http/Client.ts +16 -2
- package/src/Http/ClientRequest.ts +3 -1
- package/src/Http/ClientResponse.ts +18 -7
- package/src/Http/Cookies.ts +718 -0
- package/src/Http/Headers.ts +16 -8
- package/src/Http/IncomingMessage.ts +17 -12
- package/src/Http/Multipart.ts +5 -2
- package/src/Http/Router.ts +50 -3
- package/src/Http/ServerRequest.ts +25 -7
- package/src/Http/ServerResponse.ts +145 -13
- package/src/Http/UrlParams.ts +3 -2
- package/src/HttpClient.ts +8 -0
- package/src/HttpServer.ts +8 -0
- package/src/internal/http/body.ts +3 -2
- package/src/internal/http/client.ts +45 -5
- package/src/internal/http/clientRequest.ts +3 -2
- package/src/internal/http/clientResponse.ts +18 -8
- package/src/internal/http/multipart.ts +7 -4
- package/src/internal/http/router.ts +80 -6
- package/src/internal/http/serverRequest.ts +41 -15
- package/src/internal/http/serverResponse.ts +190 -18
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 1.0.0
|
|
3
|
+
*/
|
|
4
|
+
import * as Duration from "effect/Duration"
|
|
5
|
+
import * as Either from "effect/Either"
|
|
6
|
+
import { dual } from "effect/Function"
|
|
7
|
+
import * as Inspectable from "effect/Inspectable"
|
|
8
|
+
import * as Option from "effect/Option"
|
|
9
|
+
import { type Pipeable, pipeArguments } from "effect/Pipeable"
|
|
10
|
+
import * as Predicate from "effect/Predicate"
|
|
11
|
+
import * as ReadonlyRecord from "effect/ReadonlyRecord"
|
|
12
|
+
import type * as Types from "effect/Types"
|
|
13
|
+
import { TypeIdError } from "../Error.js"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @since 1.0.0
|
|
17
|
+
* @category type ids
|
|
18
|
+
*/
|
|
19
|
+
export const TypeId = Symbol.for("@effect/platform/Http/Cookies")
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @since 1.0.0
|
|
23
|
+
* @category type ids
|
|
24
|
+
*/
|
|
25
|
+
export type TypeId = typeof TypeId
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @since 1.0.0
|
|
29
|
+
* @category refinements
|
|
30
|
+
*/
|
|
31
|
+
export const isCookies = (u: unknown): u is Cookies => Predicate.hasProperty(u, TypeId)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @since 1.0.0
|
|
35
|
+
* @category models
|
|
36
|
+
*/
|
|
37
|
+
export interface Cookies extends Pipeable, Inspectable.Inspectable {
|
|
38
|
+
readonly [TypeId]: TypeId
|
|
39
|
+
readonly cookies: ReadonlyRecord.ReadonlyRecord<string, Cookie>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @since 1.0.0
|
|
44
|
+
* @category type ids
|
|
45
|
+
*/
|
|
46
|
+
export const CookieTypeId = Symbol.for("@effect/platform/Http/Cookies/Cookie")
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @since 1.0.0
|
|
50
|
+
* @category type ids
|
|
51
|
+
*/
|
|
52
|
+
export type CookieTypeId = typeof CookieTypeId
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @since 1.0.0
|
|
56
|
+
* @category cookie
|
|
57
|
+
*/
|
|
58
|
+
export interface Cookie extends Inspectable.Inspectable {
|
|
59
|
+
readonly [CookieTypeId]: CookieTypeId
|
|
60
|
+
readonly name: string
|
|
61
|
+
readonly value: string
|
|
62
|
+
readonly valueEncoded: string
|
|
63
|
+
readonly options?: {
|
|
64
|
+
readonly domain?: string | undefined
|
|
65
|
+
readonly expires?: Date | undefined
|
|
66
|
+
readonly maxAge?: Duration.DurationInput | undefined
|
|
67
|
+
readonly path?: string | undefined
|
|
68
|
+
readonly priority?: "low" | "medium" | "high" | undefined
|
|
69
|
+
readonly httpOnly?: boolean | undefined
|
|
70
|
+
readonly secure?: boolean | undefined
|
|
71
|
+
readonly partitioned?: boolean | undefined
|
|
72
|
+
readonly sameSite?: "lax" | "strict" | "none" | undefined
|
|
73
|
+
} | undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @since 1.0.0
|
|
78
|
+
* @category type ids
|
|
79
|
+
*/
|
|
80
|
+
export const ErrorTypeId = Symbol.for("@effect/platform/Http/Cookies/CookieError")
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @since 1.0.0
|
|
84
|
+
* @category type ids
|
|
85
|
+
*/
|
|
86
|
+
export type ErrorTypeId = typeof ErrorTypeId
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @since 1.0.0
|
|
90
|
+
* @category errors
|
|
91
|
+
*/
|
|
92
|
+
export class CookiesError extends TypeIdError(ErrorTypeId, "CookieError")<{
|
|
93
|
+
readonly reason: "InvalidName" | "InvalidValue" | "InvalidDomain" | "InvalidPath" | "InfinityMaxAge"
|
|
94
|
+
}> {
|
|
95
|
+
get message() {
|
|
96
|
+
return this.reason
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const Proto: Omit<Cookies, "cookies"> = {
|
|
101
|
+
[TypeId]: TypeId,
|
|
102
|
+
...Inspectable.BaseProto,
|
|
103
|
+
toJSON(this: Cookies) {
|
|
104
|
+
return {
|
|
105
|
+
_id: "@effect/platform/Http/Cookies",
|
|
106
|
+
cookies: ReadonlyRecord.map(this.cookies, (cookie) => cookie.toJSON())
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
pipe() {
|
|
110
|
+
return pipeArguments(this, arguments)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create a Cookies object from an Iterable
|
|
116
|
+
*
|
|
117
|
+
* @since 1.0.0
|
|
118
|
+
* @category constructors
|
|
119
|
+
*/
|
|
120
|
+
export const fromReadonlyRecord = (cookies: ReadonlyRecord.ReadonlyRecord<string, Cookie>): Cookies => {
|
|
121
|
+
const self = Object.create(Proto)
|
|
122
|
+
self.cookies = cookies
|
|
123
|
+
return self
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a Cookies object from an Iterable
|
|
128
|
+
*
|
|
129
|
+
* @since 1.0.0
|
|
130
|
+
* @category constructors
|
|
131
|
+
*/
|
|
132
|
+
export const fromIterable = (cookies: Iterable<Cookie>): Cookies => {
|
|
133
|
+
const record: Record<string, Cookie> = {}
|
|
134
|
+
for (const cookie of cookies) {
|
|
135
|
+
record[cookie.name] = cookie
|
|
136
|
+
}
|
|
137
|
+
return fromReadonlyRecord(record)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create a Cookies object from a set of Set-Cookie headers
|
|
142
|
+
*
|
|
143
|
+
* @since 1.0.0
|
|
144
|
+
* @category constructors
|
|
145
|
+
*/
|
|
146
|
+
export const fromSetCookie = (headers: Iterable<string> | string): Cookies => {
|
|
147
|
+
const arrayHeaders = typeof headers === "string" ? [headers] : headers
|
|
148
|
+
const cookies: Array<Cookie> = []
|
|
149
|
+
for (const header of arrayHeaders) {
|
|
150
|
+
const cookie = parseSetCookie(header.trim())
|
|
151
|
+
if (Option.isSome(cookie)) {
|
|
152
|
+
cookies.push(cookie.value)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return fromIterable(cookies)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseSetCookie(header: string): Option.Option<Cookie> {
|
|
160
|
+
const parts = header.split(";").map((_) => _.trim()).filter((_) => _ !== "")
|
|
161
|
+
if (parts.length === 0) {
|
|
162
|
+
return Option.none()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const firstEqual = parts[0].indexOf("=")
|
|
166
|
+
if (firstEqual === -1) {
|
|
167
|
+
return Option.none()
|
|
168
|
+
}
|
|
169
|
+
const name = parts[0].slice(0, firstEqual)
|
|
170
|
+
if (!fieldContentRegExp.test(name)) {
|
|
171
|
+
return Option.none()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const valueEncoded = parts[0].slice(firstEqual + 1)
|
|
175
|
+
const value = tryDecodeURIComponent(valueEncoded)
|
|
176
|
+
|
|
177
|
+
if (parts.length === 1) {
|
|
178
|
+
return Option.some(Object.assign(Object.create(CookieProto), {
|
|
179
|
+
name,
|
|
180
|
+
value,
|
|
181
|
+
valueEncoded
|
|
182
|
+
}))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const options: Types.Mutable<Cookie["options"]> = {}
|
|
186
|
+
|
|
187
|
+
for (let i = 1; i < parts.length; i++) {
|
|
188
|
+
const part = parts[i]
|
|
189
|
+
const equalIndex = part.indexOf("=")
|
|
190
|
+
const key = equalIndex === -1 ? part : part.slice(0, equalIndex).trim()
|
|
191
|
+
const value = equalIndex === -1 ? undefined : part.slice(equalIndex + 1).trim()
|
|
192
|
+
|
|
193
|
+
switch (key.toLowerCase()) {
|
|
194
|
+
case "domain": {
|
|
195
|
+
if (value === undefined) {
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
const domain = value.trim().replace(/^\./, "")
|
|
199
|
+
if (domain) {
|
|
200
|
+
options.domain = domain
|
|
201
|
+
}
|
|
202
|
+
break
|
|
203
|
+
}
|
|
204
|
+
case "expires": {
|
|
205
|
+
if (value === undefined) {
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
const date = new Date(value)
|
|
209
|
+
if (!isNaN(date.getTime())) {
|
|
210
|
+
options.expires = date
|
|
211
|
+
}
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
case "max-age": {
|
|
215
|
+
if (value === undefined) {
|
|
216
|
+
break
|
|
217
|
+
}
|
|
218
|
+
const maxAge = parseInt(value, 10)
|
|
219
|
+
if (!isNaN(maxAge)) {
|
|
220
|
+
options.maxAge = Duration.seconds(maxAge)
|
|
221
|
+
}
|
|
222
|
+
break
|
|
223
|
+
}
|
|
224
|
+
case "path": {
|
|
225
|
+
if (value === undefined) {
|
|
226
|
+
break
|
|
227
|
+
}
|
|
228
|
+
if (value[0] === "/") {
|
|
229
|
+
options.path = value
|
|
230
|
+
}
|
|
231
|
+
break
|
|
232
|
+
}
|
|
233
|
+
case "priority": {
|
|
234
|
+
if (value === undefined) {
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
switch (value.toLowerCase()) {
|
|
238
|
+
case "low":
|
|
239
|
+
options.priority = "low"
|
|
240
|
+
break
|
|
241
|
+
case "medium":
|
|
242
|
+
options.priority = "medium"
|
|
243
|
+
break
|
|
244
|
+
case "high":
|
|
245
|
+
options.priority = "high"
|
|
246
|
+
break
|
|
247
|
+
}
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
case "httponly": {
|
|
251
|
+
options.httpOnly = true
|
|
252
|
+
break
|
|
253
|
+
}
|
|
254
|
+
case "secure": {
|
|
255
|
+
options.secure = true
|
|
256
|
+
break
|
|
257
|
+
}
|
|
258
|
+
case "partitioned": {
|
|
259
|
+
options.partitioned = true
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
case "samesite": {
|
|
263
|
+
if (value === undefined) {
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
switch (value.toLowerCase()) {
|
|
267
|
+
case "lax":
|
|
268
|
+
options.sameSite = "lax"
|
|
269
|
+
break
|
|
270
|
+
case "strict":
|
|
271
|
+
options.sameSite = "strict"
|
|
272
|
+
break
|
|
273
|
+
case "none":
|
|
274
|
+
options.sameSite = "none"
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
break
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return Option.some(Object.assign(Object.create(CookieProto), {
|
|
283
|
+
name,
|
|
284
|
+
value,
|
|
285
|
+
valueEncoded,
|
|
286
|
+
options: Object.keys(options).length > 0 ? options : undefined
|
|
287
|
+
}))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* An empty Cookies object
|
|
292
|
+
*
|
|
293
|
+
* @since 1.0.0
|
|
294
|
+
* @category constructors
|
|
295
|
+
*/
|
|
296
|
+
export const empty: Cookies = fromIterable([])
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @since 1.0.0
|
|
300
|
+
* @category refinements
|
|
301
|
+
*/
|
|
302
|
+
export const isEmpty = (self: Cookies): boolean => ReadonlyRecord.isEmptyRecord(self.cookies)
|
|
303
|
+
|
|
304
|
+
// eslint-disable-next-line no-control-regex
|
|
305
|
+
const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/
|
|
306
|
+
|
|
307
|
+
const CookieProto = {
|
|
308
|
+
[CookieTypeId]: CookieTypeId,
|
|
309
|
+
...Inspectable.BaseProto,
|
|
310
|
+
toJSON(this: Cookie) {
|
|
311
|
+
return {
|
|
312
|
+
_id: "@effect/platform/Http/Cookies/Cookie",
|
|
313
|
+
name: this.name,
|
|
314
|
+
value: this.value,
|
|
315
|
+
options: this.options
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create a new cookie
|
|
322
|
+
*
|
|
323
|
+
* @since 1.0.0
|
|
324
|
+
* @category constructors
|
|
325
|
+
*/
|
|
326
|
+
export function makeCookie(
|
|
327
|
+
name: string,
|
|
328
|
+
value: string,
|
|
329
|
+
options?: Cookie["options"] | undefined
|
|
330
|
+
): Either.Either<Cookie, CookiesError> {
|
|
331
|
+
if (!fieldContentRegExp.test(name)) {
|
|
332
|
+
return Either.left(new CookiesError({ reason: "InvalidName" }))
|
|
333
|
+
}
|
|
334
|
+
const encodedValue = encodeURIComponent(value)
|
|
335
|
+
if (encodedValue && !fieldContentRegExp.test(encodedValue)) {
|
|
336
|
+
return Either.left(new CookiesError({ reason: "InvalidValue" }))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (options !== undefined) {
|
|
340
|
+
if (options.domain !== undefined && !fieldContentRegExp.test(options.domain)) {
|
|
341
|
+
return Either.left(new CookiesError({ reason: "InvalidDomain" }))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (options.path !== undefined && !fieldContentRegExp.test(options.path)) {
|
|
345
|
+
return Either.left(new CookiesError({ reason: "InvalidPath" }))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (options.maxAge !== undefined && !Duration.isFinite(Duration.decode(options.maxAge))) {
|
|
349
|
+
return Either.left(new CookiesError({ reason: "InfinityMaxAge" }))
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return Either.right(Object.assign(Object.create(CookieProto), {
|
|
354
|
+
name,
|
|
355
|
+
value,
|
|
356
|
+
valueEncoded: encodedValue,
|
|
357
|
+
options
|
|
358
|
+
}))
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Create a new cookie, throwing an error if invalid
|
|
363
|
+
*
|
|
364
|
+
* @since 1.0.0
|
|
365
|
+
* @category constructors
|
|
366
|
+
*/
|
|
367
|
+
export const unsafeMakeCookie = (
|
|
368
|
+
name: string,
|
|
369
|
+
value: string,
|
|
370
|
+
options?: Cookie["options"] | undefined
|
|
371
|
+
): Cookie => Either.getOrThrow(makeCookie(name, value, options))
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Add a cookie to a Cookies object
|
|
375
|
+
*
|
|
376
|
+
* @since 1.0.0
|
|
377
|
+
* @category combinators
|
|
378
|
+
*/
|
|
379
|
+
export const setCookie: {
|
|
380
|
+
(cookie: Cookie): (self: Cookies) => Cookies
|
|
381
|
+
(
|
|
382
|
+
self: Cookies,
|
|
383
|
+
cookie: Cookie
|
|
384
|
+
): Cookies
|
|
385
|
+
} = dual(
|
|
386
|
+
2,
|
|
387
|
+
(self: Cookies, cookie: Cookie) =>
|
|
388
|
+
fromReadonlyRecord(ReadonlyRecord.set(
|
|
389
|
+
self.cookies,
|
|
390
|
+
cookie.name,
|
|
391
|
+
cookie
|
|
392
|
+
))
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Add multiple cookies to a Cookies object
|
|
397
|
+
*
|
|
398
|
+
* @since 1.0.0
|
|
399
|
+
* @category combinators
|
|
400
|
+
*/
|
|
401
|
+
export const setAllCookie: {
|
|
402
|
+
(cookies: Iterable<Cookie>): (self: Cookies) => Cookies
|
|
403
|
+
(
|
|
404
|
+
self: Cookies,
|
|
405
|
+
cookies: Iterable<Cookie>
|
|
406
|
+
): Cookies
|
|
407
|
+
} = dual(2, (self: Cookies, cookies: Iterable<Cookie>) => {
|
|
408
|
+
const record = { ...self.cookies }
|
|
409
|
+
for (const cookie of cookies) {
|
|
410
|
+
record[cookie.name] = cookie
|
|
411
|
+
}
|
|
412
|
+
return fromReadonlyRecord(record)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Combine two Cookies objects, removing duplicates from the first
|
|
417
|
+
*
|
|
418
|
+
* @since 1.0.0
|
|
419
|
+
* @category combinators
|
|
420
|
+
*/
|
|
421
|
+
export const merge: {
|
|
422
|
+
(that: Cookies): (self: Cookies) => Cookies
|
|
423
|
+
(
|
|
424
|
+
self: Cookies,
|
|
425
|
+
that: Cookies
|
|
426
|
+
): Cookies
|
|
427
|
+
} = dual(2, (self: Cookies, that: Cookies) =>
|
|
428
|
+
fromReadonlyRecord({
|
|
429
|
+
...self.cookies,
|
|
430
|
+
...that.cookies
|
|
431
|
+
}))
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Remove a cookie by name
|
|
435
|
+
*
|
|
436
|
+
* @since 1.0.0
|
|
437
|
+
* @category combinators
|
|
438
|
+
*/
|
|
439
|
+
export const remove: {
|
|
440
|
+
(name: string): (self: Cookies) => Cookies
|
|
441
|
+
(
|
|
442
|
+
self: Cookies,
|
|
443
|
+
name: string
|
|
444
|
+
): Cookies
|
|
445
|
+
} = dual(2, (self: Cookies, name: string) => fromReadonlyRecord(ReadonlyRecord.remove(self.cookies, name)))
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Add a cookie to a Cookies object
|
|
449
|
+
*
|
|
450
|
+
* @since 1.0.0
|
|
451
|
+
* @category combinators
|
|
452
|
+
*/
|
|
453
|
+
export const set: {
|
|
454
|
+
(
|
|
455
|
+
name: string,
|
|
456
|
+
value: string,
|
|
457
|
+
options?: Cookie["options"]
|
|
458
|
+
): (self: Cookies) => Either.Either<Cookies, CookiesError>
|
|
459
|
+
(
|
|
460
|
+
self: Cookies,
|
|
461
|
+
name: string,
|
|
462
|
+
value: string,
|
|
463
|
+
options?: Cookie["options"]
|
|
464
|
+
): Either.Either<Cookies, CookiesError>
|
|
465
|
+
} = dual(
|
|
466
|
+
(args) => isCookies(args[0]),
|
|
467
|
+
(self: Cookies, name: string, value: string, options?: Cookie["options"]) =>
|
|
468
|
+
Either.map(
|
|
469
|
+
makeCookie(name, value, options),
|
|
470
|
+
(cookie) => fromReadonlyRecord(ReadonlyRecord.set(self.cookies, name, cookie))
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Add a cookie to a Cookies object
|
|
476
|
+
*
|
|
477
|
+
* @since 1.0.0
|
|
478
|
+
* @category combinators
|
|
479
|
+
*/
|
|
480
|
+
export const unsafeSet: {
|
|
481
|
+
(
|
|
482
|
+
name: string,
|
|
483
|
+
value: string,
|
|
484
|
+
options?: Cookie["options"]
|
|
485
|
+
): (self: Cookies) => Cookies
|
|
486
|
+
(
|
|
487
|
+
self: Cookies,
|
|
488
|
+
name: string,
|
|
489
|
+
value: string,
|
|
490
|
+
options?: Cookie["options"]
|
|
491
|
+
): Cookies
|
|
492
|
+
} = dual(
|
|
493
|
+
(args) => isCookies(args[0]),
|
|
494
|
+
(self: Cookies, name: string, value: string, options?: Cookie["options"]) =>
|
|
495
|
+
fromReadonlyRecord(ReadonlyRecord.set(
|
|
496
|
+
self.cookies,
|
|
497
|
+
name,
|
|
498
|
+
unsafeMakeCookie(name, value, options)
|
|
499
|
+
))
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Add multiple cookies to a Cookies object
|
|
504
|
+
*
|
|
505
|
+
* @since 1.0.0
|
|
506
|
+
* @category combinators
|
|
507
|
+
*/
|
|
508
|
+
export const setAll: {
|
|
509
|
+
(
|
|
510
|
+
cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]>
|
|
511
|
+
): (self: Cookies) => Either.Either<Cookies, CookiesError>
|
|
512
|
+
(
|
|
513
|
+
self: Cookies,
|
|
514
|
+
cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]>
|
|
515
|
+
): Either.Either<Cookies, CookiesError>
|
|
516
|
+
} = dual(
|
|
517
|
+
2,
|
|
518
|
+
(
|
|
519
|
+
self: Cookies,
|
|
520
|
+
cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]>
|
|
521
|
+
): Either.Either<Cookies, CookiesError> => {
|
|
522
|
+
const record: Record<string, Cookie> = { ...self.cookies }
|
|
523
|
+
for (const [name, value, options] of cookies) {
|
|
524
|
+
const either = makeCookie(name, value, options)
|
|
525
|
+
if (Either.isLeft(either)) {
|
|
526
|
+
return either as Either.Left<CookiesError, never>
|
|
527
|
+
}
|
|
528
|
+
record[name] = either.right
|
|
529
|
+
}
|
|
530
|
+
return Either.right(fromReadonlyRecord(record))
|
|
531
|
+
}
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Add multiple cookies to a Cookies object, throwing an error if invalid
|
|
536
|
+
*
|
|
537
|
+
* @since 1.0.0
|
|
538
|
+
* @category combinators
|
|
539
|
+
*/
|
|
540
|
+
export const unsafeSetAll: {
|
|
541
|
+
(
|
|
542
|
+
cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]>
|
|
543
|
+
): (self: Cookies) => Cookies
|
|
544
|
+
(
|
|
545
|
+
self: Cookies,
|
|
546
|
+
cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]>
|
|
547
|
+
): Cookies
|
|
548
|
+
} = dual(
|
|
549
|
+
2,
|
|
550
|
+
(
|
|
551
|
+
self: Cookies,
|
|
552
|
+
cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]>
|
|
553
|
+
): Cookies => Either.getOrThrow(setAll(self, cookies))
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Serialize a cookie into a string
|
|
558
|
+
*
|
|
559
|
+
* Adapted from https://github.com/fastify/fastify-cookie under MIT License
|
|
560
|
+
*
|
|
561
|
+
* @since 1.0.0
|
|
562
|
+
* @category encoding
|
|
563
|
+
*/
|
|
564
|
+
export function serializeCookie(self: Cookie): string {
|
|
565
|
+
let str = self.name + "=" + self.valueEncoded
|
|
566
|
+
|
|
567
|
+
if (self.options === undefined) {
|
|
568
|
+
return str
|
|
569
|
+
}
|
|
570
|
+
const options = self.options
|
|
571
|
+
|
|
572
|
+
if (options.maxAge !== undefined) {
|
|
573
|
+
const maxAge = Duration.toSeconds(options.maxAge)
|
|
574
|
+
str += "; Max-Age=" + Math.trunc(maxAge)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (options.domain !== undefined) {
|
|
578
|
+
str += "; Domain=" + options.domain
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (options.path !== undefined) {
|
|
582
|
+
str += "; Path=" + options.path
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (options.priority !== undefined) {
|
|
586
|
+
switch (options.priority) {
|
|
587
|
+
case "low":
|
|
588
|
+
str += "; Priority=Low"
|
|
589
|
+
break
|
|
590
|
+
case "medium":
|
|
591
|
+
str += "; Priority=Medium"
|
|
592
|
+
break
|
|
593
|
+
case "high":
|
|
594
|
+
str += "; Priority=High"
|
|
595
|
+
break
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (options.expires !== undefined) {
|
|
600
|
+
str += "; Expires=" + options.expires.toUTCString()
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (options.httpOnly) {
|
|
604
|
+
str += "; HttpOnly"
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (options.secure) {
|
|
608
|
+
str += "; Secure"
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Draft implementation to support Chrome from 2024-Q1 forward.
|
|
612
|
+
// See https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1
|
|
613
|
+
if (options.partitioned) {
|
|
614
|
+
str += "; Partitioned"
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (options.sameSite !== undefined) {
|
|
618
|
+
switch (options.sameSite) {
|
|
619
|
+
case "lax":
|
|
620
|
+
str += "; SameSite=Lax"
|
|
621
|
+
break
|
|
622
|
+
case "strict":
|
|
623
|
+
str += "; SameSite=Strict"
|
|
624
|
+
break
|
|
625
|
+
case "none":
|
|
626
|
+
str += "; SameSite=None"
|
|
627
|
+
break
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return str
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Serialize a Cookies object into a Cookie header
|
|
636
|
+
*
|
|
637
|
+
* @since 1.0.0
|
|
638
|
+
* @category encoding
|
|
639
|
+
*/
|
|
640
|
+
export const toCookieHeader = (self: Cookies): string =>
|
|
641
|
+
Object.values(self.cookies).map((cookie) => `${cookie.name}=${cookie.valueEncoded}`).join("; ")
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* To record
|
|
645
|
+
*
|
|
646
|
+
* @since 1.0.0
|
|
647
|
+
* @category encoding
|
|
648
|
+
*/
|
|
649
|
+
export const toRecord = (self: Cookies): Record<string, string> => {
|
|
650
|
+
const record: Record<string, string> = {}
|
|
651
|
+
const cookies = Object.values(self.cookies)
|
|
652
|
+
for (let index = 0; index < cookies.length; index++) {
|
|
653
|
+
const cookie = cookies[index]
|
|
654
|
+
record[cookie.name] = cookie.value
|
|
655
|
+
}
|
|
656
|
+
return record
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Serialize a Cookies object into Headers object containing one or more Set-Cookie headers
|
|
661
|
+
*
|
|
662
|
+
* @since 1.0.0
|
|
663
|
+
* @category encoding
|
|
664
|
+
*/
|
|
665
|
+
export const toSetCookieHeaders = (self: Cookies): Array<string> => Object.values(self.cookies).map(serializeCookie)
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Parse a cookie header into a record of key-value pairs
|
|
669
|
+
*
|
|
670
|
+
* Adapted from https://github.com/fastify/fastify-cookie under MIT License
|
|
671
|
+
*
|
|
672
|
+
* @since 1.0.0
|
|
673
|
+
* @category decoding
|
|
674
|
+
*/
|
|
675
|
+
export function parseHeader(header: string): Record<string, string> {
|
|
676
|
+
const result: Record<string, string> = {}
|
|
677
|
+
|
|
678
|
+
const strLen = header.length
|
|
679
|
+
let pos = 0
|
|
680
|
+
let terminatorPos = 0
|
|
681
|
+
// eslint-disable-next-line no-constant-condition
|
|
682
|
+
while (true) {
|
|
683
|
+
if (terminatorPos === strLen) break
|
|
684
|
+
terminatorPos = header.indexOf(";", pos)
|
|
685
|
+
if (terminatorPos === -1) terminatorPos = strLen // This is the last pair
|
|
686
|
+
|
|
687
|
+
let eqIdx = header.indexOf("=", pos)
|
|
688
|
+
if (eqIdx === -1) break // No key-value pairs left
|
|
689
|
+
if (eqIdx > terminatorPos) {
|
|
690
|
+
// Malformed key-value pair
|
|
691
|
+
pos = terminatorPos + 1
|
|
692
|
+
continue
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const key = header.substring(pos, eqIdx++).trim()
|
|
696
|
+
if (result[key] === undefined) {
|
|
697
|
+
const val = header.charCodeAt(eqIdx) === 0x22
|
|
698
|
+
? header.substring(eqIdx + 1, terminatorPos - 1).trim()
|
|
699
|
+
: header.substring(eqIdx, terminatorPos).trim()
|
|
700
|
+
|
|
701
|
+
result[key] = !(val.indexOf("%") === -1)
|
|
702
|
+
? tryDecodeURIComponent(val)
|
|
703
|
+
: val
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
pos = terminatorPos + 1
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return result
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const tryDecodeURIComponent = (str: string): string => {
|
|
713
|
+
try {
|
|
714
|
+
return decodeURIComponent(str)
|
|
715
|
+
} catch (_) {
|
|
716
|
+
return str
|
|
717
|
+
}
|
|
718
|
+
}
|