| /** | |
| * Models HTTP cookies and cookie collections for requests and responses. | |
| * | |
| * A `Cookie` stores a name, value, encoded value, and standard cookie | |
| * attributes. A `Cookies` value is an immutable collection keyed by cookie | |
| * name. This module parses request `Cookie` headers, builds response | |
| * `Set-Cookie` headers, and provides helpers for adding, removing, merging, and | |
| * expiring cookies. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Data from "../../Data.ts" | |
| import * as Duration from "../../Duration.ts" | |
| import { dual } from "../../Function.ts" | |
| import * as Inspectable from "../../Inspectable.ts" | |
| import * as Option from "../../Option.ts" | |
| import { type Pipeable, pipeArguments } from "../../Pipeable.ts" | |
| import * as Predicate from "../../Predicate.ts" | |
| import * as Record from "../../Record.ts" | |
| import * as Result from "../../Result.ts" | |
| import * as Schema from "../../Schema.ts" | |
| import * as SchemaTransformation from "../../SchemaTransformation.ts" | |
| import type * as Types from "../../Types.ts" | |
| const TypeId = "~effect/http/Cookies" | |
| /** | |
| * Returns `true` when a value is a `Cookies` collection. | |
| * | |
| * @category refinements | |
| * @since 4.0.0 | |
| */ | |
| export const isCookies = (u: unknown): u is Cookies => Predicate.hasProperty(u, TypeId) | |
| /** | |
| * Immutable collection of HTTP cookies keyed by cookie name. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Cookies extends Pipeable, Inspectable.Inspectable { | |
| readonly [TypeId]: typeof TypeId | |
| readonly cookies: Record.ReadonlyRecord<string, Cookie> | |
| } | |
| /** | |
| * Schema interface for validating and encoding `Cookies` collections. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export interface CookiesSchema extends Schema.declare<Cookies, Record.ReadonlyRecord<string, Cookie>> {} | |
| /** | |
| * Schema for `Cookies` collections. | |
| * | |
| * **Details** | |
| * | |
| * JSON encoding uses `Set-Cookie` header strings, while isomorphic encoding uses | |
| * a readonly record of cookie values. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const CookiesSchema: CookiesSchema = Schema.declare( | |
| isCookies, | |
| { | |
| typeConstructor: { | |
| _tag: "effect/http/Cookies" | |
| }, | |
| generation: { | |
| runtime: `Cookies.CookiesSchema`, | |
| Type: `Cookies.Cookies`, | |
| Encoded: `typeof Cookies.CookiesSchema["Encoded"]`, | |
| importDeclaration: `import * as Cookies from "effect/unstable/http/Cookies"` | |
| }, | |
| expected: "Cookies", | |
| toCodecJson: () => | |
| Schema.link<Cookies>()( | |
| Schema.Array(Schema.String), | |
| SchemaTransformation.transform({ | |
| decode: (input) => fromSetCookie(input), | |
| encode: (cookies) => toSetCookieHeaders(cookies) | |
| }) | |
| ), | |
| toCodecIso: () => | |
| Schema.link<Cookies>()( | |
| Schema.Record(Schema.String, CookieSchema), | |
| SchemaTransformation.transform({ | |
| decode: (input) => fromReadonlyRecord(input), | |
| encode: (cookies) => cookies.cookies | |
| }) | |
| ) | |
| } | |
| ) | |
| const CookieTypeId = "~effect/http/Cookies/Cookie" | |
| /** | |
| * HTTP cookie value with its decoded value, encoded value, and optional cookie | |
| * attributes such as domain, path, expiration, security, and same-site settings. | |
| * | |
| * @category cookies | |
| * @since 4.0.0 | |
| */ | |
| export interface Cookie extends Inspectable.Inspectable { | |
| readonly [CookieTypeId]: typeof CookieTypeId | |
| readonly name: string | |
| readonly value: string | |
| readonly valueEncoded: string | |
| readonly options?: { | |
| readonly domain?: string | undefined | |
| readonly expires?: Date | undefined | |
| readonly maxAge?: Duration.Input | undefined | |
| readonly path?: string | undefined | |
| readonly priority?: "low" | "medium" | "high" | undefined | |
| readonly httpOnly?: boolean | undefined | |
| readonly secure?: boolean | undefined | |
| readonly partitioned?: boolean | undefined | |
| readonly sameSite?: "lax" | "strict" | "none" | undefined | |
| } | undefined | |
| } | |
| /** | |
| * Returns `true` when a value is a `Cookie`. | |
| * | |
| * @category guards | |
| * @since 4.0.0 | |
| */ | |
| export const isCookie = (u: unknown): u is Cookie => Predicate.hasProperty(u, CookieTypeId) | |
| /** | |
| * Schema interface for validating `Cookie` values. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export interface CookieSchema extends Schema.declare<Cookie> {} | |
| /** | |
| * Schema for `Cookie` values. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const CookieSchema: CookieSchema = Schema.declare( | |
| isCookie, | |
| { | |
| typeConstructor: { | |
| _tag: "effect/http/Cookie" | |
| }, | |
| generation: { | |
| runtime: `Cookies.CookieSchema`, | |
| Type: `Cookies.Cookie`, | |
| importDeclaration: `import * as Cookie from "effect/unstable/http/Cookies"` | |
| }, | |
| expected: "Cookie" | |
| } | |
| ) | |
| const CookieErrorTypeId = "~effect/http/Cookies/CookieError" | |
| /** | |
| * Error reason describing why cookie construction failed, such as invalid name, | |
| * value, domain, path, or infinite max-age. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class CookiesErrorReason extends Data.Error<{ | |
| readonly _tag: | |
| | "InvalidCookieName" | |
| | "InvalidCookieValue" | |
| | "InvalidCookieDomain" | |
| | "InvalidCookiePath" | |
| | "CookieInfinityMaxAge" | |
| readonly cause?: unknown | |
| }> {} | |
| /** | |
| * Error returned when a cookie name, value, domain, path, or max-age option is invalid. | |
| * | |
| * **Details** | |
| * | |
| * Inspect `reason` to determine the specific validation failure. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class CookiesError extends Data.TaggedError("CookieError")<{ | |
| readonly reason: CookiesErrorReason | |
| }> { | |
| /** | |
| * Creates a cookie error from a reason tag and optional cause. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| static fromReason(reason: CookiesError["reason"]["_tag"], cause?: unknown): CookiesError { | |
| return new CookiesError({ reason: new CookiesErrorReason({ _tag: reason, cause }) }) | |
| } | |
| /** | |
| * Marks this value as a cookie validation error for runtime guards. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| readonly [CookieErrorTypeId] = CookieErrorTypeId | |
| /** | |
| * Uses the concrete cookie error reason as the public message. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| override get message() { | |
| return this.reason._tag | |
| } | |
| } | |
| const Proto: Omit<Cookies, "cookies"> = { | |
| [TypeId]: TypeId, | |
| ...Inspectable.BaseProto, | |
| toJSON(this: Cookies) { | |
| return { | |
| _id: "effect/Cookies", | |
| cookies: Record.map(this.cookies, (cookie) => cookie.toJSON()) | |
| } | |
| }, | |
| pipe() { | |
| return pipeArguments(this, arguments) | |
| } | |
| } | |
| /** | |
| * Creates a `Cookies` collection from an existing readonly record of cookies keyed by cookie name. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const fromReadonlyRecord = (cookies: Record.ReadonlyRecord<string, Cookie>): Cookies => { | |
| const self = Object.create(Proto) | |
| self.cookies = cookies | |
| return self | |
| } | |
| /** | |
| * Create a Cookies object from an Iterable | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const fromIterable = (cookies: Iterable<Cookie>): Cookies => { | |
| const record: Record<string, Cookie> = {} | |
| for (const cookie of cookies) { | |
| record[cookie.name] = cookie | |
| } | |
| return fromReadonlyRecord(record) | |
| } | |
| /** | |
| * Create a Cookies object from a set of Set-Cookie headers | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const fromSetCookie = (headers: Iterable<string> | string): Cookies => { | |
| const arrayHeaders = typeof headers === "string" ? [headers] : headers | |
| const cookies: Array<Cookie> = [] | |
| for (const header of arrayHeaders) { | |
| const cookie = parseSetCookie(header.trim()) | |
| if (cookie) { | |
| cookies.push(cookie) | |
| } | |
| } | |
| return fromIterable(cookies) | |
| } | |
| function parseSetCookie(header: string): Cookie | undefined { | |
| const parts = header.split(";").map((_) => _.trim()).filter((_) => _ !== "") | |
| if (parts.length === 0) { | |
| return undefined | |
| } | |
| const firstEqual = parts[0].indexOf("=") | |
| if (firstEqual === -1) { | |
| return undefined | |
| } | |
| const name = parts[0].slice(0, firstEqual) | |
| if (!fieldContentRegExp.test(name)) { | |
| return undefined | |
| } | |
| const valueEncoded = parts[0].slice(firstEqual + 1) | |
| const value = tryDecodeURIComponent(valueEncoded) | |
| if (parts.length === 1) { | |
| return Object.assign(Object.create(CookieProto), { | |
| name, | |
| value, | |
| valueEncoded | |
| }) | |
| } | |
| const options: Types.Mutable<Cookie["options"]> = {} | |
| for (let i = 1; i < parts.length; i++) { | |
| const part = parts[i] | |
| const equalIndex = part.indexOf("=") | |
| const key = equalIndex === -1 ? part : part.slice(0, equalIndex).trim() | |
| const value = equalIndex === -1 ? undefined : part.slice(equalIndex + 1).trim() | |
| switch (key.toLowerCase()) { | |
| case "domain": { | |
| if (value === undefined) { | |
| break | |
| } | |
| const domain = value.trim().replace(/^\./, "") | |
| if (domain) { | |
| options.domain = domain | |
| } | |
| break | |
| } | |
| case "expires": { | |
| if (value === undefined) { | |
| break | |
| } | |
| const date = new Date(value) | |
| if (!isNaN(date.getTime())) { | |
| options.expires = date | |
| } | |
| break | |
| } | |
| case "max-age": { | |
| if (value === undefined) { | |
| break | |
| } | |
| const maxAge = parseInt(value, 10) | |
| if (!isNaN(maxAge)) { | |
| options.maxAge = Duration.seconds(maxAge) | |
| } | |
| break | |
| } | |
| case "path": { | |
| if (value === undefined) { | |
| break | |
| } | |
| if (value[0] === "/") { | |
| options.path = value | |
| } | |
| break | |
| } | |
| case "priority": { | |
| if (value === undefined) { | |
| break | |
| } | |
| switch (value.toLowerCase()) { | |
| case "low": | |
| options.priority = "low" | |
| break | |
| case "medium": | |
| options.priority = "medium" | |
| break | |
| case "high": | |
| options.priority = "high" | |
| break | |
| } | |
| break | |
| } | |
| case "httponly": { | |
| options.httpOnly = true | |
| break | |
| } | |
| case "secure": { | |
| options.secure = true | |
| break | |
| } | |
| case "partitioned": { | |
| options.partitioned = true | |
| break | |
| } | |
| case "samesite": { | |
| if (value === undefined) { | |
| break | |
| } | |
| switch (value.toLowerCase()) { | |
| case "lax": | |
| options.sameSite = "lax" | |
| break | |
| case "strict": | |
| options.sameSite = "strict" | |
| break | |
| case "none": | |
| options.sameSite = "none" | |
| break | |
| } | |
| break | |
| } | |
| } | |
| } | |
| return Object.assign(Object.create(CookieProto), { | |
| name, | |
| value, | |
| valueEncoded, | |
| options: Object.keys(options).length > 0 ? options : undefined | |
| }) | |
| } | |
| /** | |
| * An empty Cookies object | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const empty: Cookies = fromIterable([]) | |
| /** | |
| * Returns `true` when the `Cookies` collection contains no cookies. | |
| * | |
| * @category refinements | |
| * @since 4.0.0 | |
| */ | |
| export const isEmpty = (self: Cookies): boolean => Record.isEmptyRecord(self.cookies) | |
| // oxlint-disable-next-line no-control-regex | |
| const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ | |
| const CookieProto = { | |
| [CookieTypeId]: CookieTypeId, | |
| ...Inspectable.BaseProto, | |
| toJSON(this: Cookie) { | |
| return { | |
| _id: "effect/Cookies/Cookie", | |
| name: this.name, | |
| value: this.value, | |
| options: this.options | |
| } | |
| } | |
| } | |
| /** | |
| * Creates a cookie, validating the name, encoded value, domain, path, and finite `maxAge`. | |
| * | |
| * **Details** | |
| * | |
| * Returns a `CookiesError` in the `Result` failure channel when validation fails. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export function makeCookie( | |
| name: string, | |
| value: string, | |
| options?: Cookie["options"] | undefined | |
| ): Result.Result<Cookie, CookiesError> { | |
| if (!fieldContentRegExp.test(name)) { | |
| return Result.fail(CookiesError.fromReason("InvalidCookieName")) | |
| } | |
| const encodedValue = encodeURIComponent(value) | |
| if (encodedValue && !fieldContentRegExp.test(encodedValue)) { | |
| return Result.fail(CookiesError.fromReason("InvalidCookieValue")) | |
| } | |
| if (options !== undefined) { | |
| if (options.domain !== undefined && !fieldContentRegExp.test(options.domain)) { | |
| return Result.fail(CookiesError.fromReason("InvalidCookieDomain")) | |
| } | |
| if (options.path !== undefined && !fieldContentRegExp.test(options.path)) { | |
| return Result.fail(CookiesError.fromReason("InvalidCookiePath")) | |
| } | |
| if (options.maxAge !== undefined && !Duration.isFinite(Duration.fromInputUnsafe(options.maxAge))) { | |
| return Result.fail(CookiesError.fromReason("CookieInfinityMaxAge")) | |
| } | |
| } | |
| return Result.succeed(Object.assign(Object.create(CookieProto), { | |
| name, | |
| value, | |
| valueEncoded: encodedValue, | |
| options | |
| })) | |
| } | |
| /** | |
| * Create a new cookie, throwing an error if invalid | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const makeCookieUnsafe = ( | |
| name: string, | |
| value: string, | |
| options?: Cookie["options"] | undefined | |
| ): Cookie => Result.getOrThrow(makeCookie(name, value, options)) | |
| /** | |
| * Adds a cookie to a Cookies object | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const setCookie: { | |
| /** | |
| * Adds a cookie to a Cookies object | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (cookie: Cookie): (self: Cookies) => Cookies | |
| /** | |
| * Adds a cookie to a Cookies object | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (self: Cookies, cookie: Cookie): Cookies | |
| } = dual( | |
| 2, | |
| (self: Cookies, cookie: Cookie) => | |
| fromReadonlyRecord(Record.set( | |
| self.cookies, | |
| cookie.name, | |
| cookie | |
| )) | |
| ) | |
| /** | |
| * Adds multiple cookies to a Cookies object | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const setAllCookie: { | |
| /** | |
| * Adds multiple cookies to a Cookies object | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (cookies: Iterable<Cookie>): (self: Cookies) => Cookies | |
| /** | |
| * Adds multiple cookies to a Cookies object | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (self: Cookies, cookies: Iterable<Cookie>): Cookies | |
| } = dual(2, (self: Cookies, cookies: Iterable<Cookie>) => { | |
| const record = { ...self.cookies } | |
| for (const cookie of cookies) { | |
| record[cookie.name] = cookie | |
| } | |
| return fromReadonlyRecord(record) | |
| }) | |
| /** | |
| * Combines two Cookies objects, removing duplicates from the first | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const merge: { | |
| /** | |
| * Combines two Cookies objects, removing duplicates from the first | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (that: Cookies): (self: Cookies) => Cookies | |
| /** | |
| * Combines two Cookies objects, removing duplicates from the first | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (self: Cookies, that: Cookies): Cookies | |
| } = dual(2, (self: Cookies, that: Cookies) => | |
| fromReadonlyRecord({ | |
| ...self.cookies, | |
| ...that.cookies | |
| })) | |
| /** | |
| * Removes a cookie by name | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const remove: { | |
| /** | |
| * Removes a cookie by name | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (name: string): (self: Cookies) => Cookies | |
| /** | |
| * Removes a cookie by name | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (self: Cookies, name: string): Cookies | |
| } = dual(2, (self: Cookies, name: string) => fromReadonlyRecord(Record.remove(self.cookies, name))) | |
| /** | |
| * Gets a cookie from a Cookies object safely. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const get: { | |
| /** | |
| * Gets a cookie from a Cookies object safely. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (name: string): (self: Cookies) => Option.Option<Cookie> | |
| /** | |
| * Gets a cookie from a Cookies object safely. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (self: Cookies, name: string): Option.Option<Cookie> | |
| } = dual( | |
| (args) => isCookies(args[0]), | |
| (self: Cookies, name: string): Option.Option<Cookie> => Option.fromUndefinedOr(self.cookies[name]) | |
| ) | |
| /** | |
| * Gets the decoded value of a cookie by name safely. | |
| * | |
| * **Details** | |
| * | |
| * Returns `Option.none()` when the cookie is not present. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const getValue: { | |
| /** | |
| * Gets the decoded value of a cookie by name safely. | |
| * | |
| * **Details** | |
| * | |
| * Returns `Option.none()` when the cookie is not present. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (name: string): (self: Cookies) => Option.Option<string> | |
| /** | |
| * Gets the decoded value of a cookie by name safely. | |
| * | |
| * **Details** | |
| * | |
| * Returns `Option.none()` when the cookie is not present. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (self: Cookies, name: string): Option.Option<string> | |
| } = dual( | |
| (args) => isCookies(args[0]), | |
| (self: Cookies, name: string): Option.Option<string> => Option.map(get(self, name), (cookie) => cookie.value) | |
| ) | |
| /** | |
| * Creates and adds a cookie safely by name and value. | |
| * | |
| * **Details** | |
| * | |
| * The cookie fields are validated first; invalid input returns a `CookiesError` in the `Result` failure channel. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const set: { | |
| /** | |
| * Creates and adds a cookie safely by name and value. | |
| * | |
| * **Details** | |
| * | |
| * The cookie fields are validated first; invalid input returns a `CookiesError` in the `Result` failure channel. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (name: string, value: string, options?: Cookie["options"]): (self: Cookies) => Result.Result<Cookies, CookiesError> | |
| /** | |
| * Creates and adds a cookie safely by name and value. | |
| * | |
| * **Details** | |
| * | |
| * The cookie fields are validated first; invalid input returns a `CookiesError` in the `Result` failure channel. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (self: Cookies, name: string, value: string, options?: Cookie["options"]): Result.Result<Cookies, CookiesError> | |
| } = dual( | |
| (args) => isCookies(args[0]), | |
| (self: Cookies, name: string, value: string, options?: Cookie["options"]) => | |
| Result.map( | |
| makeCookie(name, value, options), | |
| (cookie) => fromReadonlyRecord(Record.set(self.cookies, name, cookie)) | |
| ) | |
| ) | |
| /** | |
| * Creates and adds a cookie by name and value, throwing if the cookie fields are invalid. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const setUnsafe: { | |
| /** | |
| * Creates and adds a cookie by name and value, throwing if the cookie fields are invalid. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (name: string, value: string, options?: Cookie["options"]): (self: Cookies) => Cookies | |
| /** | |
| * Creates and adds a cookie by name and value, throwing if the cookie fields are invalid. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| (self: Cookies, name: string, value: string, options?: Cookie["options"]): Cookies | |
| } = dual( | |
| (args) => isCookies(args[0]), | |
| (self: Cookies, name: string, value: string, options?: Cookie["options"]) => | |
| fromReadonlyRecord(Record.set( | |
| self.cookies, | |
| name, | |
| makeCookieUnsafe(name, value, options) | |
| )) | |
| ) | |
| /** | |
| * Adds an expired cookie safely with an empty value, `Max-Age=0`, and an epoch `Expires` value. | |
| * | |
| * **Details** | |
| * | |
| * Returns a `CookiesError` in the `Result` failure channel when the name or options are invalid. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const expireCookie: { | |
| /** | |
| * Adds an expired cookie safely with an empty value, `Max-Age=0`, and an epoch `Expires` value. | |
| * | |
| * **Details** | |
| * | |
| * Returns a `CookiesError` in the `Result` failure channel when the name or options are invalid. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| ( | |
| name: string, | |
| options?: Omit<NonNullable<Cookie["options"]>, "expires" | "maxAge"> | |
| ): (self: Cookies) => Result.Result<Cookies, CookiesError> | |
| /** | |
| * Adds an expired cookie safely with an empty value, `Max-Age=0`, and an epoch `Expires` value. | |
| * | |
| * **Details** | |
| * | |
| * Returns a `CookiesError` in the `Result` failure channel when the name or options are invalid. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| ( | |
| self: Cookies, | |
| name: string, | |
| options?: Omit<NonNullable<Cookie["options"]>, "expires" | "maxAge"> | |
| ): Result.Result<Cookies, CookiesError> | |
| } = dual( | |
| (args) => isCookies(args[0]), | |
| ( | |
| self: Cookies, | |
| name: string, | |
| options?: Omit<NonNullable<Cookie["options"]>, "expires" | "maxAge"> | |
| ) => | |
| set(self, name, "", { | |
| ...options, | |
| maxAge: 0, | |
| expires: new Date(0) | |
| }) | |
| ) | |
| /** | |
| * Adds an expired cookie to a Cookies object, throwing an error if invalid | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const expireCookieUnsafe: { | |
| /** | |
| * Adds an expired cookie to a Cookies object, throwing an error if invalid | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| ( | |
| name: string, | |
| options?: Omit<NonNullable<Cookie["options"]>, "expires" | "maxAge"> | |
| ): (self: Cookies) => Cookies | |
| /** | |
| * Adds an expired cookie to a Cookies object, throwing an error if invalid | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| ( | |
| self: Cookies, | |
| name: string, | |
| options?: Omit<NonNullable<Cookie["options"]>, "expires" | "maxAge"> | |
| ): Cookies | |
| } = dual( | |
| (args) => isCookies(args[0]), | |
| ( | |
| self: Cookies, | |
| name: string, | |
| options?: Omit<NonNullable<Cookie["options"]>, "expires" | "maxAge"> | |
| ): Cookies => | |
| setUnsafe(self, name, "", { | |
| ...options, | |
| maxAge: 0, | |
| expires: new Date(0) | |
| }) | |
| ) | |
| /** | |
| * Creates and adds multiple cookies safely from name/value/options tuples. | |
| * | |
| * **Details** | |
| * | |
| * If any tuple is invalid, returns the first `CookiesError` and leaves the original collection unchanged. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const setAll: { | |
| /** | |
| * Creates and adds multiple cookies safely from name/value/options tuples. | |
| * | |
| * **Details** | |
| * | |
| * If any tuple is invalid, returns the first `CookiesError` and leaves the original collection unchanged. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| ( | |
| cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]> | |
| ): (self: Cookies) => Result.Result<Cookies, CookiesError> | |
| /** | |
| * Creates and adds multiple cookies safely from name/value/options tuples. | |
| * | |
| * **Details** | |
| * | |
| * If any tuple is invalid, returns the first `CookiesError` and leaves the original collection unchanged. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| ( | |
| self: Cookies, | |
| cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]> | |
| ): Result.Result<Cookies, CookiesError> | |
| } = dual( | |
| 2, | |
| ( | |
| self: Cookies, | |
| cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]> | |
| ): Result.Result<Cookies, CookiesError> => { | |
| const record: Record<string, Cookie> = { ...self.cookies } | |
| for (const [name, value, options] of cookies) { | |
| const result = makeCookie(name, value, options) | |
| if (Result.isFailure(result)) { | |
| return result as Result.Failure<never, CookiesError> | |
| } | |
| record[name] = result.success | |
| } | |
| return Result.succeed(fromReadonlyRecord(record)) | |
| } | |
| ) | |
| /** | |
| * Adds multiple cookies to a Cookies object, throwing an error if invalid | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const setAllUnsafe: { | |
| /** | |
| * Adds multiple cookies to a Cookies object, throwing an error if invalid | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| ( | |
| cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]> | |
| ): (self: Cookies) => Cookies | |
| /** | |
| * Adds multiple cookies to a Cookies object, throwing an error if invalid | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| ( | |
| self: Cookies, | |
| cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]> | |
| ): Cookies | |
| } = dual( | |
| 2, | |
| ( | |
| self: Cookies, | |
| cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]> | |
| ): Cookies => Result.getOrThrow(setAll(self, cookies)) | |
| ) | |
| /** | |
| * Serializes a cookie into a string. | |
| * | |
| * **Details** | |
| * | |
| * Adapted from https://github.com/fastify/fastify-cookie under MIT License | |
| * | |
| * @category encoding | |
| * @since 4.0.0 | |
| */ | |
| export function serializeCookie(self: Cookie): string { | |
| let str = self.name + "=" + self.valueEncoded | |
| if (self.options === undefined) { | |
| return str | |
| } | |
| const options = self.options | |
| if (options.maxAge !== undefined) { | |
| const maxAge = Duration.toSeconds(Duration.fromInputUnsafe(options.maxAge)) | |
| str += "; Max-Age=" + Math.trunc(maxAge) | |
| } | |
| if (options.domain !== undefined) { | |
| str += "; Domain=" + options.domain | |
| } | |
| if (options.path !== undefined) { | |
| str += "; Path=" + options.path | |
| } | |
| if (options.priority !== undefined) { | |
| switch (options.priority) { | |
| case "low": | |
| str += "; Priority=Low" | |
| break | |
| case "medium": | |
| str += "; Priority=Medium" | |
| break | |
| case "high": | |
| str += "; Priority=High" | |
| break | |
| } | |
| } | |
| if (options.expires !== undefined) { | |
| str += "; Expires=" + options.expires.toUTCString() | |
| } | |
| if (options.httpOnly) { | |
| str += "; HttpOnly" | |
| } | |
| if (options.secure) { | |
| str += "; Secure" | |
| } | |
| // Draft implementation to support Chrome from 2024-Q1 forward. | |
| // See https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1 | |
| if (options.partitioned) { | |
| str += "; Partitioned" | |
| } | |
| if (options.sameSite !== undefined) { | |
| switch (options.sameSite) { | |
| case "lax": | |
| str += "; SameSite=Lax" | |
| break | |
| case "strict": | |
| str += "; SameSite=Strict" | |
| break | |
| case "none": | |
| str += "; SameSite=None" | |
| break | |
| } | |
| } | |
| return str | |
| } | |
| /** | |
| * Serializes a `Cookies` object into a Cookie header. | |
| * | |
| * @category encoding | |
| * @since 4.0.0 | |
| */ | |
| export const toCookieHeader = (self: Cookies): string => | |
| Object.values(self.cookies).map((cookie) => `${cookie.name}=${cookie.valueEncoded}`).join("; ") | |
| /** | |
| * Converts a `Cookies` collection to a record of decoded cookie values keyed by cookie name. | |
| * | |
| * @category encoding | |
| * @since 4.0.0 | |
| */ | |
| export const toRecord = (self: Cookies): Record<string, string> => { | |
| const record: Record<string, string> = {} | |
| const cookies = Object.values(self.cookies) | |
| for (let index = 0; index < cookies.length; index++) { | |
| const cookie = cookies[index] | |
| record[cookie.name] = cookie.value | |
| } | |
| return record | |
| } | |
| /** | |
| * Schema for transforming `Cookies` into records of decoded string values keyed | |
| * by cookie name. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const schemaRecord = CookiesSchema.pipe( | |
| Schema.decodeTo( | |
| Schema.Record(Schema.String, Schema.String), | |
| SchemaTransformation.transform({ | |
| decode: toRecord, | |
| encode: (self) => fromIterable(Object.entries(self).map(([name, value]) => makeCookieUnsafe(name, value))) | |
| }) | |
| ) | |
| ) | |
| /** | |
| * Serializes a `Cookies` collection into an array of `Set-Cookie` header values. | |
| * | |
| * @category encoding | |
| * @since 4.0.0 | |
| */ | |
| export const toSetCookieHeaders = (self: Cookies): Array<string> => Object.values(self.cookies).map(serializeCookie) | |
| /** | |
| * Parses a cookie header into a record of key-value pairs | |
| * | |
| * **Details** | |
| * | |
| * Adapted from https://github.com/fastify/fastify-cookie under MIT License | |
| * | |
| * @category decoding | |
| * @since 4.0.0 | |
| */ | |
| export function parseHeader(header: string): Record<string, string> { | |
| const result: Record<string, string> = {} | |
| const strLen = header.length | |
| let pos = 0 | |
| let terminatorPos = 0 | |
| while (true) { | |
| if (terminatorPos === strLen) break | |
| terminatorPos = header.indexOf(";", pos) | |
| if (terminatorPos === -1) terminatorPos = strLen // This is the last pair | |
| let eqIdx = header.indexOf("=", pos) | |
| if (eqIdx === -1) break // No key-value pairs left | |
| if (eqIdx > terminatorPos) { | |
| // Malformed key-value pair | |
| pos = terminatorPos + 1 | |
| continue | |
| } | |
| const key = header.substring(pos, eqIdx++).trim() | |
| if (result[key] === undefined) { | |
| const val = header.charCodeAt(eqIdx) === 0x22 | |
| ? header.substring(eqIdx + 1, terminatorPos - 1).trim() | |
| : header.substring(eqIdx, terminatorPos).trim() | |
| result[key] = !(val.indexOf("%") === -1) | |
| ? tryDecodeURIComponent(val) | |
| : val | |
| } | |
| pos = terminatorPos + 1 | |
| } | |
| return result | |
| } | |
| const tryDecodeURIComponent = (str: string): string => { | |
| try { | |
| return decodeURIComponent(str) | |
| // oxlint-disable-next-line @typescript-eslint/no-unused-vars | |
| } catch (_) { | |
| return str | |
| } | |
| } | |
Xet Storage Details
- Size:
- 28.4 kB
- Xet hash:
- b088b20d674d15ce0188fdcdccbaf2fb247cdc9a126bcf888351cf1e3edaa79b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.