UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

211 lines (183 loc) 5.47 kB
import { $inject, createPrimitive, KIND, Primitive, type Static, type TSchema, } from "alepha"; import type { DurationLike } from "alepha/datetime"; import { ServerCookiesProvider } from "../providers/ServerCookiesProvider.ts"; /** * Declares a type-safe, configurable HTTP cookie. * This primitive provides methods to get, set, and delete the cookie * within the server request/response cycle. */ export const $cookie = <T extends TSchema>( options: CookiePrimitiveOptions<T>, extendedOptions?: Omit<CookiePrimitiveOptions<T>, "schema">, ): AbstractCookiePrimitive<T> => { if (extendedOptions) { options = { key: options.key, schema: options.schema, ...extendedOptions, }; } return createPrimitive(CookiePrimitive<T>, options); }; // --------------------------------------------------------------------------------------------------------------------- export interface CookiePrimitiveOptions<T extends TSchema> { /** * The schema for the cookie's value, used for validation and type safety. */ schema: T; /** * The name of the cookie. */ name?: string; /** * The cookie's path. Defaults to "/". */ path?: string; /** * Time-to-live for the cookie. Maps to `Max-Age`. */ ttl?: DurationLike; /** * If true, the cookie is only sent over HTTPS. Defaults to true in production. */ secure?: boolean; /** * If true, the cookie cannot be accessed by client-side scripts. */ httpOnly?: boolean; /** * SameSite policy for the cookie. Defaults to "lax". */ sameSite?: "strict" | "lax" | "none"; /** * The domain for the cookie. */ domain?: string; /** * If true, the cookie value will be compressed using zlib. */ compress?: boolean; /** * If true, the cookie value will be encrypted. Requires `COOKIE_SECRET` env var. */ encrypt?: boolean; /** * If true, the cookie will be signed to prevent tampering. Requires `COOKIE_SECRET` env var. */ sign?: boolean; /** * Optional key to link this cookie to an Atom, enabling automatic synchronization between the cookie and the Atom's state. */ key?: string; /** * Internal escape hatch: when explicitly set to `false`, the server skips * the `APP_NAME` cookie-name namespace applied by * `ServerCookiesProvider.prefixName()`. * * Used by `AtomCookiePersistence` so the cookie name the server * reads/writes for a `persist: "cookie"` atom matches the bare name its * browser variant uses — `APP_NAME` is not reachable client-side (not * baked into the client bundle, not part of SSR hydration), so the * browser can only ever use the bare atom key. * * Application code declaring `$cookie(...)` directly should leave this * unset — it defaults to `true` (prefixed), matching every other cookie. */ prefix?: boolean; } export interface AbstractCookiePrimitive<T extends TSchema> { readonly name: string; readonly options: CookiePrimitiveOptions<T>; set( value: Static<T>, options?: { cookies?: Cookies; ttl?: DurationLike }, ): void; get(options?: { cookies?: Cookies }): Static<T> | undefined; del(options?: { cookies?: Cookies }): void; } export class CookiePrimitive<T extends TSchema> extends Primitive<CookiePrimitiveOptions<T>> implements AbstractCookiePrimitive<T> { protected readonly serverCookiesProvider = $inject(ServerCookiesProvider); protected onInit() { if (this.options.key) { this.alepha.events.on("state:mutate", ({ key, value }) => { if (key === this.options.key) { this.set(value); } }); this.alepha.events.on("server:onRequest", ({ request }) => { try { const value = this.get(request); if (value !== undefined) { this.alepha.store.set(this.options.key as any, value, { skipEvents: true, }); } } catch {} }); } } public get schema(): T { return this.options.schema; } public get name(): string { return this.options.name ?? `${this.config.propertyKey}`; } /** * Sets the cookie with the given value in the current request's response. */ public set( value: Static<T>, options?: { cookies?: Cookies; ttl?: DurationLike }, ): void { this.serverCookiesProvider.setCookie( this.name, { ...this.options, ttl: options?.ttl ?? this.options.ttl, }, value, options?.cookies, ); } /** * Gets the cookie value from the current request. Returns undefined if not found or invalid. */ public get(options?: { cookies?: Cookies }): Static<T> | undefined { return this.serverCookiesProvider.getCookie( this.name, this.options, options?.cookies, ); } /** * Deletes the cookie in the current request's response. */ public del(options?: { cookies?: Cookies }): void { this.serverCookiesProvider.deleteCookie(this.name, options?.cookies); } } $cookie[KIND] = CookiePrimitive; // --------------------------------------------------------------------------------------------------------------------- export interface Cookies { req: Record<string, string>; res: Record<string, Cookie | null>; } export interface Cookie { value: string; path?: string; maxAge?: number; secure?: boolean; httpOnly?: boolean; sameSite?: "strict" | "lax" | "none"; domain?: string; }