feat: Security

This commit is contained in:
2026-06-05 18:34:01 +02:00
parent ee293d7daf
commit 45675192a5
134 changed files with 4128 additions and 446 deletions
@@ -0,0 +1,53 @@
import {
describe,
it,
expect
} from 'vitest';
import type { User } from '../../../../shared-kernel';
import {
SESSION_EXPIRED_ERROR_CODE,
collectSessionTokenLookupUrls,
hasValidPersistedSession
} from './auth-session.rules';
describe('auth-session.rules', () => {
const user = {
homeSignalServerUrl: 'https://signal.example.com'
} as Pick<User, 'homeSignalServerUrl'>;
it('collects home and active server urls without duplicates', () => {
expect(collectSessionTokenLookupUrls(user, 'https://signal.example.com')).toEqual([
'https://signal.example.com'
]);
expect(collectSessionTokenLookupUrls(user, 'http://localhost:3001')).toEqual([
'http://localhost:3001',
'https://signal.example.com'
]);
});
it('requires a valid token for a known server url', () => {
const getToken = (url: string) => (url === 'https://signal.example.com' ? 'token-1' : null);
expect(hasValidPersistedSession(user, 'https://signal.example.com', getToken)).toBe(true);
expect(hasValidPersistedSession(user, 'http://localhost:3001', getToken)).toBe(true);
});
it('rejects persisted users without any valid session token', () => {
expect(hasValidPersistedSession(user, 'https://signal.example.com', () => null)).toBe(false);
});
it('falls back to any stored token when preferred urls are missing', () => {
expect(
hasValidPersistedSession(
{} as Pick<User, 'homeSignalServerUrl'>,
null,
() => null,
() => true
)
).toBe(true);
});
it('exports a stable session-expired error code', () => {
expect(SESSION_EXPIRED_ERROR_CODE).toBe('SESSION_EXPIRED');
});
});
@@ -0,0 +1,46 @@
import type { User } from '../../../../shared-kernel';
export const SESSION_EXPIRED_ERROR_CODE = 'SESSION_EXPIRED';
function normalizeServerUrl(serverUrl: string): string {
return serverUrl.trim().replace(/\/+$/, '');
}
export function collectSessionTokenLookupUrls(
user: Pick<User, 'homeSignalServerUrl'>,
activeServerUrl?: string | null
): string[] {
const urls = new Set<string>();
if (activeServerUrl?.trim()) {
urls.add(normalizeServerUrl(activeServerUrl));
}
if (user.homeSignalServerUrl?.trim()) {
urls.add(normalizeServerUrl(user.homeSignalServerUrl));
}
return [...urls];
}
export function hasValidSessionTokenForUrls(
urls: readonly string[],
getToken: (serverUrl: string) => string | null
): boolean {
return urls.some((url) => !!getToken(url));
}
export function hasValidPersistedSession(
user: Pick<User, 'homeSignalServerUrl'>,
activeServerUrl: string | null | undefined,
getToken: (serverUrl: string) => string | null,
hasAnyValidToken?: () => boolean
): boolean {
const preferredUrls = collectSessionTokenLookupUrls(user, activeServerUrl);
if (preferredUrls.length > 0 && hasValidSessionTokenForUrls(preferredUrls, getToken)) {
return true;
}
return hasAnyValidToken?.() ?? false;
}
@@ -8,4 +8,8 @@ export interface LoginResponse {
username: string;
/** Human-readable display name. */
displayName: string;
/** Opaque session token for authenticated API and WebSocket identify calls. */
token: string;
/** Unix timestamp (ms) when the session token expires. */
expiresAt: number;
}