Files
electric-cherry/src/lib/server/auth/http.ts
T
2026-10-09 16:34:32 -04:00

112 lines
4.0 KiB
TypeScript

import { error, isHttpError, json, type Cookies } from '@sveltejs/kit';
import { AuthError, SESSION_COOKIE, type AuthResult } from './index.js';
import { PasswordServiceBusyError } from './passwords.js';
import type { AuthUser } from '../../auth.js';
export function sessionCookie(url: URL): string {
return url.protocol === 'https:' ? SESSION_COOKIE : 'ec_session_dev';
}
export function requireUserTransport(url: URL): void {
if (url.protocol === 'https:') return;
if (
process.env.EC_AUTH_ALLOW_HTTP === 'true' &&
['localhost', '127.0.0.1', '[::1]', '::1'].includes(url.hostname)
)
return;
error(
426,
'Operator login requires HTTPS. For local development only, set EC_AUTH_ALLOW_HTTP=true and use localhost.'
);
}
export function requireSameOrigin(request: Request, url: URL): void {
if (
request.headers.get('origin') !== url.origin ||
request.headers.get('sec-fetch-site') === 'cross-site'
) {
error(403, 'This action requires a same-origin request.');
}
}
export function sessionResponse(result: AuthResult, cookies: Cookies, url: URL) {
cookies.set(sessionCookie(url), result.session.token, {
path: '/',
httpOnly: true,
secure: url.protocol === 'https:',
sameSite: 'lax',
expires: new Date(result.session.expiresAt)
});
return json({ user: result.user }, { headers: { 'Cache-Control': 'no-store' } });
}
export function requireUser(user: AuthUser | null): AuthUser {
if (!user) throw new AuthError('Sign in to continue.', 401);
return user;
}
export async function authBody(
request: Request,
maxBytes = 8192
): Promise<Record<string, unknown>> {
if (!request.headers.get('content-type')?.toLowerCase().startsWith('application/json'))
throw new AuthError('Send a JSON request.', 415);
if (Number(request.headers.get('content-length')) > maxBytes)
throw new AuthError('Request is too large.', 413);
const reader = request.body?.getReader();
if (!reader) throw new AuthError('A JSON object is required.');
const chunks: Uint8Array[] = [];
let length = 0;
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
length += value.byteLength;
if (length > maxBytes) {
void reader.cancel().catch(() => {});
throw new AuthError('Request is too large.', 413);
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
let body: unknown;
try {
body = JSON.parse(Buffer.concat(chunks, length).toString('utf8'));
} catch {
throw new AuthError('Invalid JSON.');
}
if (!body || typeof body !== 'object' || Array.isArray(body))
throw new AuthError('A JSON object is required.');
return body as Record<string, unknown>;
}
export function authError(cause: unknown): Response {
if (isHttpError(cause))
return json(
{ error: cause.body.message },
{ status: cause.status, headers: { 'Cache-Control': 'no-store' } }
);
const message = cause instanceof Error ? cause.message : 'Authentication request failed.';
const status =
cause instanceof AuthError
? cause.status
: cause instanceof PasswordServiceBusyError
? 429
: message.startsWith('Use a password')
? 400
: 500;
return json(
{ error: status === 500 ? 'The authentication request could not be completed.' : message },
{
status,
headers: {
'Cache-Control': 'no-store',
...(status === 429
? {
'Retry-After':
cause instanceof PasswordServiceBusyError
? String(cause.retryAfterSeconds)
: '900'
}
: {})
}
}
);
}