first commit

This commit is contained in:
2026-10-03 13:40:26 +02:00
parent 732a5b5a16
commit 566f415fa7
555 changed files with 62704 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
export * from './lib/session';
export * from './lib/ports';

View File

@@ -0,0 +1,41 @@
import { Observable } from 'rxjs';
import { AuthConfig, CurrentUser, Session } from './session';
/** The API's sign-in endpoints (/api/auth/*). */
export abstract class AuthGateway {
abstract config(): Observable<AuthConfig | null>;
abstract exchangeCode(
code: string,
codeVerifier: string,
redirectUri: string,
): Observable<Session>;
abstract password(username: string, password: string): Observable<Session>;
abstract refresh(refreshToken: string): Observable<Session>;
}
/** Who the current token belongs to (GraphQL `me`). */
export abstract class ProfileGateway {
abstract me(accessToken: string): Observable<CurrentUser>;
}
export abstract class SessionStorage {
abstract load(): Session | null;
abstract save(session: Session): void;
abstract clear(): void;
}
export interface AuthorizationResponse {
code: string;
codeVerifier: string;
redirectUri: string;
/** Where the user wanted to go before signing in. */
returnUrl: string;
}
/** Authorization Code + PKCE in the browser (redirect out and back). */
export abstract class AuthorizationFlow {
/** Leaves the app towards the identity server's authorize page. */
abstract start(config: AuthConfig, returnUrl: string): Promise<void>;
/** Validates `state` and returns what is needed to exchange the code. */
abstract complete(params: URLSearchParams): AuthorizationResponse;
}

View File

@@ -0,0 +1,23 @@
import { isExpired, sessionFrom } from './session';
describe('session', () => {
it('computes the expiry from expires_in', () => {
const session = sessionFrom(
{ accessToken: 't', refreshToken: 'r', expiresIn: 3600 },
1_000,
);
expect(session).toEqual({
accessToken: 't',
refreshToken: 'r',
expiresAt: 3_601_000,
});
});
it('treats tokens about to expire as expired', () => {
const session = sessionFrom({ accessToken: 't', expiresIn: 30 }, 0);
expect(isExpired(session, 0)).toBe(true);
expect(
isExpired(sessionFrom({ accessToken: 't', expiresIn: 3600 }, 0), 0),
).toBe(false);
});
});

View File

@@ -0,0 +1,46 @@
/** Tokens issued by Pireon (through the API's /api/auth/token proxy). */
export interface Session {
accessToken: string;
refreshToken: string | null;
/** Epoch milliseconds. */
expiresAt: number;
}
export interface CurrentUser {
id: string;
name: string;
email: string;
appRoles: string[];
}
/** How the browser can sign in (null config = the API has no Pireon client yet). */
export interface AuthConfig {
authorizeUrl: string;
clientId: string;
scopes: string[];
passwordGrant: boolean;
}
/** Refresh a little before the token actually expires. */
export const EXPIRY_MARGIN_MS = 60_000;
export function isExpired(session: Session, now = Date.now()): boolean {
return session.expiresAt - EXPIRY_MARGIN_MS <= now;
}
export function sessionFrom(
token: {
accessToken: string;
refreshToken?: string | null;
expiresIn?: number | null;
},
now = Date.now(),
): Session {
// Personal access tokens are long-lived and come without expiry: one year.
const expiresIn = token.expiresIn ?? 365 * 24 * 3600;
return {
accessToken: token.accessToken,
refreshToken: token.refreshToken ?? null,
expiresAt: now + expiresIn * 1000,
};
}