// ---------------------------------------------------------------------------// 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);};
// ---------------------------------------------------------------------------
// 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);
};