@worker-tools/middleware
Version:
A suite of standalone HTTP server middlewares for Worker Runtimes.
180 lines (155 loc) • 6.02 kB
text/typescript
import { CookieStore, RequestCookieStore } from "@worker-tools/request-cookie-store";
import { SignedCookieStore, DeriveOptions } from "@worker-tools/signed-cookie-store";
import { EncryptedCookieStore } from "@worker-tools/encrypted-cookie-store";
import { ResolvablePromise } from '@worker-tools/resolvable-promise';
import { forbidden } from "@worker-tools/response-creators";
import { Awaitable } from "./utils/common-types.js";
import { MiddlewareCookieStore } from "./utils/middleware-cookie-store.js";
import { headersSetCookieFix } from './utils/headers-set-cookie-fix.js'
import { unsettle } from "./utils/unsettle.js";
import { Context } from "./index.js";
export async function cookiesFrom(cookieStore: CookieStore): Promise<Cookies> {
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 <X extends Context>(ax: Awaitable<X>): Promise<X & UnsignedCookiesContext> => {
const x = await ax;
const cookieStore = new RequestCookieStore(x.request);
const requestDuration = new ResolvablePromise<void>();
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 <X extends Context>(ax: Awaitable<X>): Promise<X & SignedCookiesContext> => {
const x = await ax;
const request = x.request;
const cookieStore = new RequestCookieStore(request);
const requestDuration = new ResolvablePromise<void>();
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 <X extends Context>(ax: Awaitable<X>): Promise<X & EncryptedCookiesContext> => {
const x = await ax;
const request = x.request;
const cookieStore = new RequestCookieStore(request);
const requestDuration = new ResolvablePromise<void>();
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;
};
}