|
import { parse, serialize } from 'cookie'; |
|
import { add_data_suffix, normalize_path, resolve } from '../../utils/url.js'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
const cookie_paths = {}; |
|
|
|
|
|
|
|
|
|
|
|
const MAX_COOKIE_SIZE = 4129; |
|
|
|
|
|
|
|
function validate_options(options) { |
|
if (options?.path === undefined) { |
|
throw new Error('You must specify a `path` when setting, deleting or serializing cookies'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function get_cookies(request, url, trailing_slash) { |
|
const header = request.headers.get('cookie') ?? ''; |
|
const initial_cookies = parse(header, { decode: (value) => value }); |
|
|
|
const normalized_url = normalize_path(url.pathname, trailing_slash); |
|
|
|
|
|
const new_cookies = {}; |
|
|
|
|
|
const defaults = { |
|
httpOnly: true, |
|
sameSite: 'lax', |
|
secure: url.hostname === 'localhost' && url.protocol === 'http:' ? false : true |
|
}; |
|
|
|
|
|
const cookies = { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get(name, opts) { |
|
const c = new_cookies[name]; |
|
if ( |
|
c && |
|
domain_matches(url.hostname, c.options.domain) && |
|
path_matches(url.pathname, c.options.path) |
|
) { |
|
return c.value; |
|
} |
|
|
|
const decoder = opts?.decode || decodeURIComponent; |
|
const req_cookies = parse(header, { decode: decoder }); |
|
const cookie = req_cookies[name]; |
|
|
|
|
|
|
|
|
|
if (__SVELTEKIT_DEV__ && !cookie) { |
|
const paths = Array.from(cookie_paths[name] ?? []).filter((path) => { |
|
|
|
return path_matches(path, url.pathname) && path !== url.pathname; |
|
}); |
|
|
|
if (paths.length > 0) { |
|
console.warn( |
|
|
|
`'${name}' cookie does not exist for ${url.pathname}, but was previously set at ${conjoin([...paths])}. Did you mean to set its 'path' to '/' instead?` |
|
); |
|
} |
|
} |
|
|
|
return cookie; |
|
}, |
|
|
|
|
|
|
|
|
|
getAll(opts) { |
|
const decoder = opts?.decode || decodeURIComponent; |
|
const cookies = parse(header, { decode: decoder }); |
|
|
|
for (const c of Object.values(new_cookies)) { |
|
if ( |
|
domain_matches(url.hostname, c.options.domain) && |
|
path_matches(url.pathname, c.options.path) |
|
) { |
|
cookies[c.name] = c.value; |
|
} |
|
} |
|
|
|
return Object.entries(cookies).map(([name, value]) => ({ name, value })); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
set(name, value, options) { |
|
validate_options(options); |
|
set_internal(name, value, { ...defaults, ...options }); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
delete(name, options) { |
|
validate_options(options); |
|
cookies.set(name, '', { ...options, maxAge: 0 }); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
serialize(name, value, options) { |
|
validate_options(options); |
|
|
|
let path = options.path; |
|
|
|
if (!options.domain || options.domain === url.hostname) { |
|
path = resolve(normalized_url, path); |
|
} |
|
|
|
return serialize(name, value, { ...defaults, ...options, path }); |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
function get_cookie_header(destination, header) { |
|
|
|
const combined_cookies = { |
|
|
|
...initial_cookies |
|
}; |
|
|
|
|
|
for (const key in new_cookies) { |
|
const cookie = new_cookies[key]; |
|
if (!domain_matches(destination.hostname, cookie.options.domain)) continue; |
|
if (!path_matches(destination.pathname, cookie.options.path)) continue; |
|
|
|
const encoder = cookie.options.encode || encodeURIComponent; |
|
combined_cookies[cookie.name] = encoder(cookie.value); |
|
} |
|
|
|
|
|
if (header) { |
|
const parsed = parse(header, { decode: (value) => value }); |
|
for (const name in parsed) { |
|
combined_cookies[name] = parsed[name]; |
|
} |
|
} |
|
|
|
return Object.entries(combined_cookies) |
|
.map(([name, value]) => `${name}=${value}`) |
|
.join('; '); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function set_internal(name, value, options) { |
|
let path = options.path; |
|
|
|
if (!options.domain || options.domain === url.hostname) { |
|
path = resolve(normalized_url, path); |
|
} |
|
|
|
new_cookies[name] = { name, value, options: { ...options, path } }; |
|
|
|
if (__SVELTEKIT_DEV__) { |
|
const serialized = serialize(name, value, new_cookies[name].options); |
|
if (new TextEncoder().encode(serialized).byteLength > MAX_COOKIE_SIZE) { |
|
throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`); |
|
} |
|
|
|
cookie_paths[name] ??= new Set(); |
|
|
|
if (!value) { |
|
cookie_paths[name].delete(path); |
|
} else { |
|
cookie_paths[name].add(path); |
|
} |
|
} |
|
} |
|
|
|
return { cookies, new_cookies, get_cookie_header, set_internal }; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function domain_matches(hostname, constraint) { |
|
if (!constraint) return true; |
|
|
|
const normalized = constraint[0] === '.' ? constraint.slice(1) : constraint; |
|
|
|
if (hostname === normalized) return true; |
|
return hostname.endsWith('.' + normalized); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function path_matches(path, constraint) { |
|
if (!constraint) return true; |
|
|
|
const normalized = constraint.endsWith('/') ? constraint.slice(0, -1) : constraint; |
|
|
|
if (path === normalized) return true; |
|
return path.startsWith(normalized + '/'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function add_cookies_to_headers(headers, cookies) { |
|
for (const new_cookie of cookies) { |
|
const { name, value, options } = new_cookie; |
|
headers.append('set-cookie', serialize(name, value, options)); |
|
|
|
|
|
|
|
|
|
if (options.path.endsWith('.html')) { |
|
const path = add_data_suffix(options.path); |
|
headers.append('set-cookie', serialize(name, value, { ...options, path })); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function conjoin(array) { |
|
if (array.length <= 2) return array.join(' and '); |
|
return `${array.slice(0, -1).join(', ')} and ${array.at(-1)}`; |
|
} |
|
|