All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
116 lines
3.0 KiB
TypeScript
116 lines
3.0 KiB
TypeScript
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
|
|
const SESSION_DIR = resolve(homedir(), '.mosaic');
|
|
const SESSION_FILE = resolve(SESSION_DIR, 'session.json');
|
|
|
|
interface StoredSession {
|
|
gatewayUrl: string;
|
|
cookie: string;
|
|
userId: string;
|
|
email: string;
|
|
expiresAt: string;
|
|
}
|
|
|
|
export interface AuthResult {
|
|
cookie: string;
|
|
userId: string;
|
|
email: string;
|
|
}
|
|
|
|
/**
|
|
* Sign in to the gateway and return the session cookie.
|
|
*/
|
|
export async function signIn(
|
|
gatewayUrl: string,
|
|
email: string,
|
|
password: string,
|
|
): Promise<AuthResult> {
|
|
const res = await fetch(`${gatewayUrl}/api/auth/sign-in/email`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Origin: gatewayUrl },
|
|
body: JSON.stringify({ email, password }),
|
|
redirect: 'manual',
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => '');
|
|
throw new Error(`Sign-in failed (${res.status}): ${body}`);
|
|
}
|
|
|
|
// Extract set-cookie header
|
|
const setCookieHeader = res.headers.getSetCookie?.() ?? [];
|
|
const sessionCookie = setCookieHeader
|
|
.map((c) => c.split(';')[0]!)
|
|
.filter((c) => c.startsWith('better-auth.session_token='))
|
|
.join('; ');
|
|
|
|
if (!sessionCookie) {
|
|
throw new Error('No session cookie returned from sign-in');
|
|
}
|
|
|
|
// Parse the response body for user info
|
|
const data = (await res.json()) as { user?: { id: string; email: string } };
|
|
const userId = data.user?.id ?? 'unknown';
|
|
const userEmail = data.user?.email ?? email;
|
|
|
|
return { cookie: sessionCookie, userId, email: userEmail };
|
|
}
|
|
|
|
/**
|
|
* Save session to ~/.mosaic/session.json
|
|
*/
|
|
export function saveSession(gatewayUrl: string, auth: AuthResult): void {
|
|
if (!existsSync(SESSION_DIR)) {
|
|
mkdirSync(SESSION_DIR, { recursive: true });
|
|
}
|
|
|
|
const session: StoredSession = {
|
|
gatewayUrl,
|
|
cookie: auth.cookie,
|
|
userId: auth.userId,
|
|
email: auth.email,
|
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
|
|
};
|
|
|
|
writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), 'utf-8');
|
|
}
|
|
|
|
/**
|
|
* Load a saved session. Returns null if no session, expired, or wrong gateway.
|
|
*/
|
|
export function loadSession(gatewayUrl: string): AuthResult | null {
|
|
if (!existsSync(SESSION_FILE)) return null;
|
|
|
|
try {
|
|
const raw = readFileSync(SESSION_FILE, 'utf-8');
|
|
const session = JSON.parse(raw) as StoredSession;
|
|
|
|
if (session.gatewayUrl !== gatewayUrl) return null;
|
|
if (new Date(session.expiresAt) < new Date()) return null;
|
|
|
|
return {
|
|
cookie: session.cookie,
|
|
userId: session.userId,
|
|
email: session.email,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate that a stored session is still active by hitting get-session.
|
|
*/
|
|
export async function validateSession(gatewayUrl: string, cookie: string): Promise<boolean> {
|
|
try {
|
|
const res = await fetch(`${gatewayUrl}/api/auth/get-session`, {
|
|
headers: { Cookie: cookie, Origin: gatewayUrl },
|
|
});
|
|
return res.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|