better-auth
Version:
The most comprehensive authentication framework for TypeScript.
204 lines (203 loc) • 7.1 kB
JavaScript
//#region src/cookies/cookie-utils.ts
function tryDecode(str) {
if (str.indexOf("%") === -1) return str;
try {
return decodeURIComponent(str);
} catch {
return str;
}
}
const SECURE_COOKIE_PREFIX = "__Secure-";
const HOST_COOKIE_PREFIX = "__Host-";
/**
* Remove __Secure- or __Host- prefix from cookie name.
*/
function stripSecureCookiePrefix(cookieName) {
if (cookieName.startsWith("__Secure-")) return cookieName.slice(9);
if (cookieName.startsWith("__Host-")) return cookieName.slice(7);
return cookieName;
}
/**
* Split a comma-joined `Set-Cookie` header string into individual cookies.
*/
function splitSetCookieHeader(setCookie) {
if (!setCookie) return [];
const result = [];
let start = 0;
let i = 0;
while (i < setCookie.length) {
if (setCookie[i] === ",") {
let j = i + 1;
while (j < setCookie.length && setCookie[j] === " ") j++;
while (j < setCookie.length && setCookie[j] !== "=" && setCookie[j] !== ";" && setCookie[j] !== ",") j++;
if (j < setCookie.length && setCookie[j] === "=") {
const part = setCookie.slice(start, i).trim();
if (part) result.push(part);
start = i + 1;
while (start < setCookie.length && setCookie[start] === " ") start++;
i = start;
continue;
}
}
i++;
}
const last = setCookie.slice(start).trim();
if (last) result.push(last);
return result;
}
function parseSetCookieHeader(setCookie) {
const cookies = /* @__PURE__ */ new Map();
splitSetCookieHeader(setCookie).forEach((cookieString) => {
const [nameValue, ...attributes] = cookieString.split(";").map((part) => part.trim());
const [name, ...valueParts] = (nameValue || "").split("=");
const value = unquoteCookieValue(valueParts.join("="));
if (!name) return;
const attrObj = { value: tryDecode(value) };
attributes.forEach((attribute) => {
const [attrName, ...attrValueParts] = attribute.split("=");
const attrValue = attrValueParts.join("=");
const normalizedAttrName = attrName.trim().toLowerCase();
switch (normalizedAttrName) {
case "max-age":
attrObj["max-age"] = attrValue ? parseInt(attrValue.trim(), 10) : void 0;
break;
case "expires":
attrObj.expires = attrValue ? new Date(attrValue.trim()) : void 0;
break;
case "domain":
attrObj.domain = attrValue ? attrValue.trim() : void 0;
break;
case "path":
attrObj.path = attrValue ? attrValue.trim() : void 0;
break;
case "secure":
attrObj.secure = true;
break;
case "httponly":
attrObj.httponly = true;
break;
case "samesite":
attrObj.samesite = attrValue ? attrValue.trim().toLowerCase() : void 0;
break;
case "partitioned":
attrObj.partitioned = true;
break;
default:
attrObj[normalizedAttrName] = attrValue ? attrValue.trim() : true;
break;
}
});
cookies.set(name, attrObj);
});
return cookies;
}
function toCookieOptions(attributes) {
return {
maxAge: attributes["max-age"],
expires: attributes.expires,
domain: attributes.domain,
path: attributes.path,
secure: attributes.secure,
httpOnly: attributes.httponly,
sameSite: attributes.samesite,
partitioned: attributes.partitioned
};
}
/**
* Cookie-name token char set per RFC 7230 §3.2.6.
*
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
*/
const cookieNameRegex = /^[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E\x5F\x60\x61-\x7A\x7C\x7E]+$/;
/**
* Cookie-value char set per RFC 6265 §4.1.1, plus space and comma.
*
* @see https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
* @see https://github.com/golang/go/issues/7243
*/
const cookieValueRegex = /^[\x20\x21\x23-\x3A\x3C-\x5B\x5D-\x7E]*$/;
/**
* Strip surrounding double-quotes per RFC 6265 §4.1.1 quoted-string form.
*
* @see https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
*/
function unquoteCookieValue(value) {
if (value.length < 2 || !value.startsWith("\"") || !value.endsWith("\"")) return value;
return value.slice(1, -1);
}
/**
* Trim leading/trailing OWS (space / horizontal tab) per RFC 7230 §3.2.3.
* Narrower than `String.prototype.trim()`, which strips CR/LF and other
* whitespace and would let CTLs escape `cookieValueRegex`.
*
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3
*/
function trimOWS(s) {
let start = 0;
let end = s.length;
while (start < end) {
const c = s.charCodeAt(start);
if (c !== 32 && c !== 9) break;
start++;
}
while (end > start) {
const c = s.charCodeAt(end - 1);
if (c !== 32 && c !== 9) break;
end--;
}
return start === 0 && end === s.length ? s : s.slice(start, end);
}
/**
* Tolerates `;` separators without the SP that RFC 6265 §4.2.1 mandates,
* since proxies and runtimes commonly strip it. Silently drops entries
* whose name violates RFC 7230 token or whose value violates RFC 6265
* cookie-octet (plus space and comma). Strips optional surrounding
* double-quotes per RFC 6265 §4.1.1.
*/
function parseCookies(cookie) {
const cookieMap = /* @__PURE__ */ new Map();
if (cookie.length < 2) return cookieMap;
for (const chunk of cookie.split(";")) {
const eq = chunk.indexOf("=");
if (eq === -1) continue;
const key = trimOWS(chunk.slice(0, eq));
const val = unquoteCookieValue(trimOWS(chunk.slice(eq + 1)));
if (cookieNameRegex.test(key) && cookieValueRegex.test(val)) cookieMap.set(key, tryDecode(val));
}
return cookieMap;
}
/**
* Add or replace a cookie in the request `Cookie` header.
*
* Cookie pairs are joined with `; `, but `headers.append("cookie", ...)`
* joins with `, ` in some runtimes (e.g. Deno, Cloudflare Workers) and
* breaks downstream cookie parsing. This builds the header value via
* parse-mutate-serialize.
*/
function setRequestCookie(headers, name, value) {
const cookieMap = parseCookies(headers.get("cookie") || "");
if (cookieNameRegex.test(name)) cookieMap.set(name, value);
headers.set("cookie", Array.from(cookieMap, ([k, v]) => `${k}=${encodeURIComponent(v)}`).join("; "));
}
/**
* Merge `Set-Cookie` header values into the target's `Cookie` header.
* Mutates `target`.
*
* Name/value-level merge only. RFC 6265 §5 user-agent semantics
* (expiration, domain/path scoping, ordering) are out of scope. Suitable
* for single-request proxy, middleware, and test contexts.
*/
function applySetCookies(target, setCookieValues) {
const cookieMap = parseCookies(target.get("cookie") || "");
for (const setCookie of setCookieValues) for (const [name, attr] of parseSetCookieHeader(setCookie)) if (cookieNameRegex.test(name)) cookieMap.set(name, attr.value);
target.set("cookie", Array.from(cookieMap, ([k, v]) => `${k}=${encodeURIComponent(v)}`).join("; "));
}
function setCookieToHeader(headers) {
return (context) => {
const setCookieHeader = context.response.headers.get("set-cookie");
if (!setCookieHeader) return;
applySetCookies(headers, [setCookieHeader]);
};
}
//#endregion
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, parseCookies, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };