Files
scriptc/tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js
T
Malte UblandClaude Opus 5.5 dddcbed335 Project zod 4 and @hono/zod-validator values at the declaration boundary
The declaration-typed boundary already reaches zod 4 schemas and hono's
c.req.valid(): zod 4's z.infer and hono's validator Input generics resolve
to plain object types in the declaration world. Three gaps remained:

- Boolean literal types printed as `false` (the TS7 facade leaves
  intrinsicName unset), so `{ ok: true } | { ok: false }` lost its true
  arm and `boolean | undefined` became `undefined | false`. The printer
  reads the literal's value.
- `r.data` after `const r = S.safeParse(x); if (r.success)` never
  crossed: zod 4's result union is not data (its error arm is a class),
  and reads on projectable receivers were skipped on the assumption that
  something else types the receiver. Such reads now cross when the
  receiver is a package call whose type does not project, or a const
  bound to one.
- Reads through bindings annotated with a projected type no longer get a
  redundant cast, and aliases are named only for casts that survive.

New npm cases zod4-boundary and hono-zod-validator run real zod 4.6.7,
hono 4.13.13 and @hono/zod-validator 0.9.1 (vendored file closures) under
Node and natively; decl-boundary.test.ts checks that c.req.valid() is a
checked cast with a path TypeError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:09:39 -07:00

126 lines
5.2 KiB
JavaScript

import { tryDecodeURIComponent } from "./url.js";
//#region src/utils/cookie.ts
/**
* @module
* Cookie utility.
*/
const algorithm = {
name: "HMAC",
hash: "SHA-256"
};
const getCryptoKey = async (secret) => {
const secretBuf = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
return await crypto.subtle.importKey("raw", secretBuf, algorithm, false, ["sign", "verify"]);
};
const makeSignature = async (value, secret) => {
const key = await getCryptoKey(secret);
const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value));
return btoa(String.fromCharCode(...new Uint8Array(signature)));
};
const verifySignature = async (base64Signature, value, secret) => {
try {
const signatureBinStr = atob(base64Signature);
const signature = new Uint8Array(signatureBinStr.length);
for (let i = 0, len = signatureBinStr.length; i < len; i++) signature[i] = signatureBinStr.charCodeAt(i);
return await crypto.subtle.verify(algorithm, secret, signature, new TextEncoder().encode(value));
} catch {
return false;
}
};
const validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/;
const relaxedCookieNameRegEx = /^[!#-:<>-[\]-~]+$/;
const validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/;
const trimCookieWhitespace = (value) => {
let start = 0;
let end = value.length;
while (start < end) {
const charCode = value.charCodeAt(start);
if (charCode !== 32 && charCode !== 9) break;
start++;
}
while (end > start) {
const charCode = value.charCodeAt(end - 1);
if (charCode !== 32 && charCode !== 9) break;
end--;
}
return start === 0 && end === value.length ? value : value.slice(start, end);
};
const parse = (cookie, name) => {
if (name && cookie.indexOf(name) === -1) return {};
const pairs = cookie.split(";");
const parsedCookie = Object.create(null);
for (const pairStr of pairs) {
const valueStartPos = pairStr.indexOf("=");
if (valueStartPos === -1) continue;
const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos));
if (name && name !== cookieName || !relaxedCookieNameRegEx.test(cookieName) || cookieName in parsedCookie) continue;
let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1));
if (cookieValue.startsWith("\"") && cookieValue.endsWith("\"")) cookieValue = cookieValue.slice(1, -1);
if (validCookieValueRegEx.test(cookieValue)) {
parsedCookie[cookieName] = tryDecodeURIComponent(cookieValue);
if (name) break;
}
}
return parsedCookie;
};
const parseSigned = async (cookie, secret, name) => {
const parsedCookie = Object.create(null);
const secretKey = await getCryptoKey(secret);
for (const [key, value] of Object.entries(parse(cookie, name))) {
const signatureStartPos = value.lastIndexOf(".");
if (signatureStartPos < 0) continue;
const signedValue = value.substring(0, signatureStartPos);
const signature = value.substring(signatureStartPos + 1);
if (signature.length !== 44 || !signature.endsWith("=")) continue;
parsedCookie[key] = await verifySignature(signature, signedValue, secretKey) ? signedValue : false;
}
return parsedCookie;
};
const _serialize = (name, value, opt = {}) => {
if (!validCookieNameRegEx.test(name)) throw new Error("Invalid cookie name");
let cookie = `${name}=${value}`;
if (name.startsWith("__Secure-") && !opt.secure) throw new Error("__Secure- Cookie must have Secure attributes");
if (name.startsWith("__Host-")) {
if (!opt.secure) throw new Error("__Host- Cookie must have Secure attributes");
if (opt.path !== "/") throw new Error("__Host- Cookie must have Path attributes with \"/\"");
if (opt.domain) throw new Error("__Host- Cookie must not have Domain attributes");
}
for (const key of [
"domain",
"path",
"sameSite",
"priority"
]) if (opt[key] && /[;\r\n]/.test(opt[key])) throw new Error(`${key} must not contain ";", "\\r", or "\\n"`);
if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) {
if (opt.maxAge > 3456e4) throw new Error("Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration.");
cookie += `; Max-Age=${opt.maxAge | 0}`;
}
if (opt.domain && opt.prefix !== "host") cookie += `; Domain=${opt.domain}`;
if (opt.path) cookie += `; Path=${opt.path}`;
if (opt.expires) {
if (opt.expires.getTime() - Date.now() > 3456e7) throw new Error("Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future.");
cookie += `; Expires=${opt.expires.toUTCString()}`;
}
if (opt.httpOnly) cookie += "; HttpOnly";
if (opt.secure) cookie += "; Secure";
if (opt.sameSite) cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`;
if (opt.priority) cookie += `; Priority=${opt.priority.charAt(0).toUpperCase() + opt.priority.slice(1)}`;
if (opt.partitioned) {
if (!opt.secure) throw new Error("Partitioned Cookie must have Secure attributes");
cookie += "; Partitioned";
}
return cookie;
};
const serialize = (name, value, opt) => {
value = encodeURIComponent(value);
return _serialize(name, value, opt);
};
const serializeSigned = async (name, value, secret, opt = {}) => {
const signature = await makeSignature(value, secret);
value = `${value}.${signature}`;
value = encodeURIComponent(value);
return _serialize(name, value, opt);
};
//#endregion
export { parse, parseSigned, serialize, serializeSigned };