UNPKG

alepha

Version:

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

254 lines (253 loc) 8.59 kB
import { $hook, $inject, $module, Alepha, AlephaError, KIND, Primitive, createPrimitive } from "alepha"; import { AlephaServer } from "alepha/server"; import { DateTimeProvider } from "alepha/datetime"; //#region ../../src/server/cookies/services/CookieParser.ts var CookieParser = class { parseRequestCookies(header) { const cookies = {}; const parts = header.split(";"); for (const part of parts) { const eqIndex = part.indexOf("="); if (eqIndex === -1) continue; const key = part.slice(0, eqIndex).trim(); const value = part.slice(eqIndex + 1).trim(); if (!key || !value) continue; cookies[key] = value; } return cookies; } serializeResponseCookies(cookies, isHttps) { const headers = []; for (const [name, cookie] of Object.entries(cookies)) { if (cookie == null) { headers.push(`${name}=; Path=/; Max-Age=0`); continue; } if (!cookie.value) continue; headers.push(this.cookieToString(name, cookie, isHttps)); } return headers; } cookieToString(name, cookie, isHttps) { const parts = []; parts.push(`${name}=${cookie.value}`); if (cookie.path) parts.push(`Path=${cookie.path}`); if (cookie.maxAge != null) parts.push(`Max-Age=${cookie.maxAge}`); if (cookie.secure !== false && isHttps) parts.push("Secure"); if (cookie.httpOnly) parts.push("HttpOnly"); if (cookie.sameSite) parts.push(`SameSite=${cookie.sameSite}`); if (cookie.domain) parts.push(`Domain=${cookie.domain}`); return parts.join("; "); } }; //#endregion //#region ../../src/server/cookies/providers/AtomCookiePersistence.browser.ts /** * Browser variant of AtomCookiePersistence: reads `document.cookie` for every * `persist: "cookie"` atom, and writes it back on every mutation. * * Like the server variant, atoms are resolved from the `StateManager` * registry (`alepha.store.listAtoms()`) rather than from a map fed by * `state:register` — that event fires once, is never replayed, and * `$module.register()` registers `atoms[]` before the module's `services[]` * exist, so an event-sourced map would miss every atom declared the * documented way. The `configure` hook sweeps everything registered up to * boot; `state:register` covers atoms that first register later (lazily, on * first read). * * Cookie names are NOT APP_NAME-prefixed here, because `APP_NAME` is not * reachable in the browser: `BuildClientTask` only `define`s * `process.env.NODE_ENV` for the client bundle, and the SSR hydration * payload (`ReactServerTemplateProvider.buildHydrationData`) only carries * registered atom values + router layers — never the raw `env` store key. * The server variant (`AtomCookiePersistence.ts`) matches this by passing * `prefix: false` to `ServerCookiesProvider`, specifically for atom-cookie * traffic — every other server cookie ($cookie primitive) still gets the * APP_NAME namespace. See `ServerCookiesProvider.prefixName`. */ var AtomCookiePersistence = class { alepha = $inject(Alepha); cookieParser = $inject(CookieParser); dateTime = $inject(DateTimeProvider); onConfigure = $hook({ on: "configure", handler: () => { for (const atom of this.cookieAtoms()) this.read(atom); } }); onRegister = $hook({ on: "state:register", handler: ({ atom }) => { if (atom.options.persist !== "cookie") return; this.read(atom); } }); onMutate = $hook({ on: "state:mutate", handler: ({ key, value }) => { const atom = this.alepha.store.getAtom(String(key)); if (!atom || atom.options.persist !== "cookie") return; if (value === void 0) { this.clearCookie(atom.key); return; } const cookie = { value: encodeURIComponent(JSON.stringify(value)), path: "/", sameSite: "lax", maxAge: this.dateTime.duration([365, "days"]).as("seconds") }; document.cookie = this.cookieParser.cookieToString(atom.key, cookie); } }); /** * Every registered atom declared with `persist: "cookie"`, read live from * the state manager's registry. */ cookieAtoms() { return this.alepha.store.listAtoms().filter((atom) => atom.options.persist === "cookie"); } /** * Seed an atom's state from `document.cookie`, if a cookie exists for it. * Idempotent: re-reading an atom that already holds the cookie's value is * a no-op write of the same data. */ read(atom) { const raw = this.cookieParser.parseRequestCookies(document.cookie)[atom.key]; if (!raw) return; try { this.alepha.store.set(atom.key, JSON.parse(decodeURIComponent(raw)), { skipEvents: true }); } catch { this.clearCookie(atom.key); } } /** * Expire a cookie immediately, mirroring * `ServerCookiesProvider.deleteCookie`'s `Max-Age=0` semantics. */ clearCookie(name) { document.cookie = this.cookieParser.cookieToString(name, { value: "", path: "/", sameSite: "lax", maxAge: 0 }); } }; //#endregion //#region ../../src/server/cookies/primitives/$cookie.browser.ts /** * Creates a browser-side cookie primitive for client-side cookie management. * * Browser-specific version of $cookie that uses document.cookie API. Supports type-safe * cookie operations with schema validation but excludes encryption/signing (use server-side * $cookie for secure operations). * * **Note**: This is the browser version - encryption, signing, and compression are not supported. * * @example * ```ts * class ClientCookies { * preferences = $cookie({ * name: "user-prefs", * schema: z.object({ theme: z.text(), language: z.text() }), * ttl: [30, "days"] * }); * * savePreferences() { * this.preferences.set({ theme: "dark", language: "en" }); * } * * getPreferences() { * return this.preferences.get() ?? { theme: "light", language: "en" }; * } * } * ``` */ const $cookie = (options, extendedOptions) => { if (extendedOptions) options = { key: options.key, schema: options.schema, ...extendedOptions }; return createPrimitive(BrowserCookiePrimitive, options); }; var BrowserCookiePrimitive = class extends Primitive { cookieParser = $inject(CookieParser); alepha = $inject(Alepha); dateTimeProvider = $inject(DateTimeProvider); cookie; 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("configure", () => { try { const value = this.get(); if (value !== void 0) this.alepha.set(this.options.key, value); } catch {} }); } } get schema() { return this.options.schema; } get name() { return this.options.name ?? `${this.config.propertyKey}`; } set(data) { const value = JSON.stringify(this.alepha.codec.decode(this.schema, data)); const options = this.options; if (options.compress) throw new AlephaError("Compression is not supported in browser cookies."); if (options.encrypt) throw new AlephaError("Encryption is not supported in browser cookies."); if (options.sign) throw new AlephaError("Signing is not supported in browser cookies."); const cookie = { value: encodeURIComponent(value), path: options.path ?? "/", sameSite: options.sameSite ?? "lax", secure: false, httpOnly: false, domain: options.domain }; if (options.ttl) cookie.maxAge = this.dateTimeProvider.duration(options.ttl).as("seconds"); document.cookie = this.cookieParser.cookieToString(this.name, cookie); } get(options) { const cookie = this.cookieParser.parseRequestCookies(document.cookie)[this.name]; if (!cookie) return; const rawValue = decodeURIComponent(cookie); if (this.options.compress) throw new AlephaError("Compression is not supported in browser cookies."); if (this.options.encrypt) throw new AlephaError("Encryption is not supported in browser cookies."); if (this.options.sign) throw new AlephaError("Signing is not supported in browser cookies."); return this.alepha.codec.decode(this.schema, JSON.parse(rawValue)); } del() { const options = this.options; const cookie = { value: "", path: options.path ?? "/", sameSite: options.sameSite ?? "lax", secure: false, httpOnly: false, domain: options.domain, maxAge: 0 }; document.cookie = this.cookieParser.cookieToString(this.name, cookie); } }; $cookie[KIND] = BrowserCookiePrimitive; //#endregion //#region ../../src/server/cookies/index.browser.ts const AlephaServerCookies = $module({ name: "alepha.server.cookies", primitives: [], services: [ AlephaServer, CookieParser, AtomCookiePersistence ] }); //#endregion export { $cookie, AlephaServerCookies, AtomCookiePersistence, BrowserCookiePrimitive }; //# sourceMappingURL=index.browser.js.map