alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
254 lines (253 loc) • 8.59 kB
JavaScript
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