import { CookieStore, RequestCookieStore } from "https://ghuc.cc/worker-tools/request-cookie-store/index.ts"; import { SignedCookieStore, DeriveOptions } from "https://ghuc.cc/worker-tools/signed-cookie-store/index.ts"; import { EncryptedCookieStore } from "https://ghuc.cc/worker-tools/encrypted-cookie-store/index.ts"; import { ResolvablePromise } from 'https://ghuc.cc/worker-tools/resolvable-promise/index.ts'; import { forbidden } from "https://ghuc.cc/worker-tools/response-creators/index.ts"; import { Awaitable } from "./utils/common-types.ts"; import { MiddlewareCookieStore } from "./utils/middleware-cookie-store.ts"; import { headersSetCookieFix } from './utils/headers-set-cookie-fix.ts' import { unsettle } from "./utils/unsettle.ts"; import { Context } from "./index.ts"; export async function cookiesFrom(cookieStore: CookieStore): Promise { return Object.fromEntries((await cookieStore.getAll()).map(({ name, value }) => [name, value])); } /** * An object of the cookies sent with this request. * It is for reading convenience only. * To make changes, use the associated cookie store instead (provided by the middleware along with this object) */ export type Cookies = { readonly [key: string]: string }; export interface CookiesContext { cookieStore: CookieStore, cookies: Cookies, } export interface UnsignedCookiesContext extends CookiesContext { unsignedCookieStore: CookieStore, unsignedCookies: Cookies } export interface SignedCookiesContext extends CookiesContext { signedCookieStore: CookieStore, signedCookies: Cookies, } export interface EncryptedCookiesContext extends CookiesContext { encryptedCookieStore: CookieStore, encryptedCookies: Cookies, } export interface CookiesOptions extends DeriveOptions { keyring?: readonly CryptoKey[]; } export const plainCookies = () => async (ax: Awaitable): Promise => { const x = await ax; const cookieStore = new RequestCookieStore(x.request); const requestDuration = new ResolvablePromise(); const unsignedCookieStore = new MiddlewareCookieStore(cookieStore, requestDuration) const unsignedCookies = await cookiesFrom(unsignedCookieStore); const nx = Object.assign(x, { cookieStore: unsignedCookieStore, cookies: unsignedCookies, unsignedCookieStore, unsignedCookies, }) x.effects.push(response => { requestDuration.resolve(); const { headers: cookieHeaders } = cookieStore if (cookieHeaders.length) response.headers.append('VARY', 'Cookie') const { status, statusText, headers, body } = response return new Response(body, { status, statusText, headers: [ ...headersSetCookieFix(headers), ...cookieHeaders, ], }); }) return nx; } export { plainCookies as unsignedCookies } export const signedCookies = (opts: CookiesOptions) => { // TODO: options to provide own cryptokey?? // TODO: What if secret isn't known at initialization (e.g. Cloudflare Workers) if (!opts.secret) throw TypeError('Secret missing'); const keyPromise = SignedCookieStore.deriveCryptoKey(opts); return async (ax: Awaitable): Promise => { const x = await ax; const request = x.request; const cookieStore = new RequestCookieStore(request); const requestDuration = new ResolvablePromise(); const signedCookieStore = new MiddlewareCookieStore(new SignedCookieStore(cookieStore, await keyPromise, { keyring: opts.keyring }), requestDuration); let signedCookies: Cookies; try { signedCookies = await cookiesFrom(signedCookieStore); } catch { throw forbidden(); } const nx = Object.assign(x, { cookieStore: signedCookieStore, cookies: signedCookies, signedCookieStore, signedCookies, }) x.effects.push(async response => { // Wait for all set cookie promises to settle requestDuration.resolve(); await unsettle(signedCookieStore.allSettledPromise); const { headers: cookieHeaders } = cookieStore if (cookieHeaders.length) response.headers.append('VARY', 'Cookie') const { status, statusText, headers, body } = response return new Response(body, { status, statusText, headers: [ ...headersSetCookieFix(headers), ...cookieHeaders, ], }) }) return nx; }; } export const encryptedCookies = (opts: CookiesOptions) => { // TODO: options to provide own cryptokey?? // TODO: What if secret isn't known at initialization (e.g. Cloudflare Workers) if (!opts.secret) throw TypeError('Secret missing'); const keyPromise = EncryptedCookieStore.deriveCryptoKey(opts); return async (ax: Awaitable): Promise => { const x = await ax; const request = x.request; const cookieStore = new RequestCookieStore(request); const requestDuration = new ResolvablePromise(); const encryptedCookieStore = new MiddlewareCookieStore(new EncryptedCookieStore(cookieStore, await keyPromise, { keyring: opts.keyring }), requestDuration); let encryptedCookies: Cookies; try { encryptedCookies = await cookiesFrom(encryptedCookieStore); } catch { throw forbidden(); } const nx = Object.assign(x, { cookieStore: encryptedCookieStore, cookies: encryptedCookies, encryptedCookieStore, encryptedCookies, }) x.effects.push(async response => { // Wait for all set cookie promises to settle requestDuration.resolve(); await unsettle(encryptedCookieStore.allSettledPromise); const { headers: cookieHeaders } = cookieStore if (cookieHeaders.length) response.headers.append('VARY', 'Cookie') const { status, statusText, headers, body } = response return new Response(body, { status, statusText, headers: [ ...headersSetCookieFix(headers), ...cookieHeaders, ], }) }) return nx; }; }