79 lines · 3.4 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Auth-hint cookie — the client-readable "probably signed in" signal.//// The session cookie itself (cloud's `wos-session`, self-host's Better Auth// cookie) is HttpOnly, so on a fresh page load the SPA can't know it's signed// in until `/account/me` resolves. This cookie is the non-HttpOnly companion:// a display-only snapshot of the authenticated identity that `AuthProvider`// writes whenever `/account/me` confirms a session and reads back at the next// page load to render the app shell immediately, while `/account/me`// reconciles in the background.//// It is a HINT, never an authority: every API call is still authenticated by// the real session cookie server-side, and a stale or forged hint can only// change which placeholder the user briefly sees. Keep the payload to// display data the user already knows about themselves.//// The encode/decode half is pure (no DOM) so a server that wants to write or// clear the hint (cloud's logout does) shares one encoding with the client.// --------------------------------------------------------------------------- import { Option, Schema } from "effect"; export const AUTH_HINT_COOKIE = "executor-auth-hint"; /** Matches the session cookie lifetime (7 days). */export const AUTH_HINT_MAX_AGE_SECONDS = 60 * 60 * 24 * 7; const AuthHintSchema = Schema.Struct({  v: Schema.Literal(1),  user: Schema.Struct({    id: Schema.String,    email: Schema.String,    name: Schema.NullOr(Schema.String),    avatarUrl: Schema.NullOr(Schema.String),  }),  organization: Schema.NullOr(    Schema.Struct({ id: Schema.String, name: Schema.String, slug: Schema.String }),  ),}); export type AuthHint = typeof AuthHintSchema.Type; const AuthHintFromJson = Schema.fromJsonString(AuthHintSchema);const decodeAuthHintJson = Schema.decodeUnknownOption(AuthHintFromJson);// The cookie value arrives percent-encoded; a truncated/corrupted one can// make decodeURIComponent itself throw, which must read as "no hint".const decodeUriComponentOption = Option.liftThrowable(decodeURIComponent); const encodeAuthHintJson = Schema.encodeSync(AuthHintFromJson); /** Encode a hint as a wire-ready cookie VALUE (URI-encoded JSON). */export const encodeAuthHint = (hint: AuthHint): string =>  encodeURIComponent(encodeAuthHintJson(hint)); /** Decode a cookie VALUE back into a hint; null for anything malformed. */export const decodeAuthHint = (value: string | null | undefined): AuthHint | null => {  if (!value) return null;  return decodeUriComponentOption(value).pipe(Option.flatMap(decodeAuthHintJson), Option.getOrNull);}; // ── Browser-side cookie maintenance (AuthProvider) ─────────────────────────── export const writeAuthHintCookie = (hint: AuthHint): void => {  document.cookie = `${AUTH_HINT_COOKIE}=${encodeAuthHint(hint)}; Path=/; Max-Age=${AUTH_HINT_MAX_AGE_SECONDS}; SameSite=Lax${    window.location.protocol === "https:" ? "; Secure" : ""  }`;}; export const clearAuthHintCookie = (): void => {  document.cookie = `${AUTH_HINT_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax`;}; export const readAuthHintCookie = (): AuthHint | null => {  const match = document.cookie    .split(";")    .map((c) => c.trim())    .find((c) => c.startsWith(`${AUTH_HINT_COOKIE}=`));  return decodeAuthHint(match ? match.slice(AUTH_HINT_COOKIE.length + 1) : null);};