Files
stack/packages/cli/src/auth.ts
Jason Woltje ea524a6ba1
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
fix(cli): add Origin header to auth requests (#115)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-15 17:03:42 +00:00

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