EdgeAIG's picture
download
raw
19 kB
/**
* 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.js";
import * as Duration from "../../Duration.js";
import { dual } from "../../Function.js";
import * as Inspectable from "../../Inspectable.js";
import * as Option from "../../Option.js";
import { pipeArguments } from "../../Pipeable.js";
import * as Predicate from "../../Predicate.js";
import * as Record from "../../Record.js";
import * as Result from "../../Result.js";
import * as Schema from "../../Schema.js";
import * as SchemaTransformation from "../../SchemaTransformation.js";
const TypeId = "~effect/http/Cookies";
/**
* Returns `true` when a value is a `Cookies` collection.
*
* @category refinements
* @since 4.0.0
*/
export const isCookies = u => Predicate.hasProperty(u, TypeId);
/**
* 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 = /*#__PURE__*/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()(Schema.Array(Schema.String), SchemaTransformation.transform({
decode: input => fromSetCookie(input),
encode: cookies => toSetCookieHeaders(cookies)
})),
toCodecIso: () => Schema.link()(Schema.Record(Schema.String, CookieSchema), SchemaTransformation.transform({
decode: input => fromReadonlyRecord(input),
encode: cookies => cookies.cookies
}))
});
const CookieTypeId = "~effect/http/Cookies/Cookie";
/**
* Returns `true` when a value is a `Cookie`.
*
* @category guards
* @since 4.0.0
*/
export const isCookie = u => Predicate.hasProperty(u, CookieTypeId);
/**
* Schema for `Cookie` values.
*
* @category schemas
* @since 4.0.0
*/
export const CookieSchema = /*#__PURE__*/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 {}
/**
* 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 /*#__PURE__*/Data.TaggedError("CookieError") {
/**
* Creates a cookie error from a reason tag and optional cause.
*
* @since 4.0.0
*/
static fromReason(reason, cause) {
return new CookiesError({
reason: new CookiesErrorReason({
_tag: reason,
cause
})
});
}
/**
* Marks this value as a cookie validation error for runtime guards.
*
* @since 4.0.0
*/
[CookieErrorTypeId] = CookieErrorTypeId;
/**
* Uses the concrete cookie error reason as the public message.
*
* @since 4.0.0
*/
get message() {
return this.reason._tag;
}
}
const Proto = {
[TypeId]: TypeId,
...Inspectable.BaseProto,
toJSON() {
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 => {
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 => {
const record = {};
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 => {
const arrayHeaders = typeof headers === "string" ? [headers] : headers;
const cookies = [];
for (const header of arrayHeaders) {
const cookie = parseSetCookie(header.trim());
if (cookie) {
cookies.push(cookie);
}
}
return fromIterable(cookies);
};
function parseSetCookie(header) {
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 = {};
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 = /*#__PURE__*/fromIterable([]);
/**
* Returns `true` when the `Cookies` collection contains no cookies.
*
* @category refinements
* @since 4.0.0
*/
export const isEmpty = self => 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() {
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, value, options) {
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, value, options) => Result.getOrThrow(makeCookie(name, value, options));
/**
* Adds a cookie to a Cookies object
*
* @category combinators
* @since 4.0.0
*/
export const setCookie = /*#__PURE__*/dual(2, (self, 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 = /*#__PURE__*/dual(2, (self, cookies) => {
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 = /*#__PURE__*/dual(2, (self, that) => fromReadonlyRecord({
...self.cookies,
...that.cookies
}));
/**
* Removes a cookie by name
*
* @category combinators
* @since 4.0.0
*/
export const remove = /*#__PURE__*/dual(2, (self, name) => fromReadonlyRecord(Record.remove(self.cookies, name)));
/**
* Gets a cookie from a Cookies object safely.
*
* @category combinators
* @since 4.0.0
*/
export const get = /*#__PURE__*/dual(args => isCookies(args[0]), (self, name) => 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 = /*#__PURE__*/dual(args => isCookies(args[0]), (self, name) => 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 = /*#__PURE__*/dual(args => isCookies(args[0]), (self, name, value, 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 = /*#__PURE__*/dual(args => isCookies(args[0]), (self, name, value, 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 = /*#__PURE__*/dual(args => isCookies(args[0]), (self, name, options) => 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 = /*#__PURE__*/dual(args => isCookies(args[0]), (self, name, options) => 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 = /*#__PURE__*/dual(2, (self, cookies) => {
const record = {
...self.cookies
};
for (const [name, value, options] of cookies) {
const result = makeCookie(name, value, options);
if (Result.isFailure(result)) {
return result;
}
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 = /*#__PURE__*/dual(2, (self, 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) {
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 => 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 => {
const record = {};
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 = /*#__PURE__*/CookiesSchema.pipe(/*#__PURE__*/Schema.decodeTo(/*#__PURE__*/Schema.Record(Schema.String, Schema.String), /*#__PURE__*/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 => 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) {
const result = {};
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 => {
try {
return decodeURIComponent(str);
// oxlint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
return str;
}
};
//# sourceMappingURL=Cookies.js.map

Xet Storage Details

Size:
19 kB
·
Xet hash:
bd22fd8b50b8a33fd754742d629eaee3d3bfde92a64de76e492273f7cf761337

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.