update project

This commit is contained in:
ZOMBIIIIIII
2026-04-14 13:30:26 +03:00
parent a81e29807c
commit 37146f7375
65 changed files with 3782 additions and 629 deletions

View File

@@ -0,0 +1,46 @@
import { importJWK, type JWK, type CryptoKey } from 'jose';
import { env } from '../config/env';
interface CachedKey {
key: CryptoKey | Uint8Array;
fetchedAt: number;
}
const KEY_TTL_MS = 60 * 60 * 1000; // 1 hour
const keyCache = new Map<string, CachedKey>();
async function fetchJwks(): Promise<{ keys: JWK[] }> {
const res = await fetch(env.bitokJwksUrl);
if (!res.ok) {
throw new Error(`Failed to fetch JWKS: ${res.status} ${res.statusText}`);
}
return res.json() as Promise<{ keys: JWK[] }>;
}
async function refreshKeys(): Promise<void> {
const jwks = await fetchJwks();
for (const jwk of jwks.keys) {
if (!jwk.kid) continue;
const key = await importJWK(jwk, 'RS256');
keyCache.set(jwk.kid, { key, fetchedAt: Date.now() });
}
}
export async function getSigningKey(kid: string): Promise<CryptoKey | Uint8Array> {
const cached = keyCache.get(kid);
if (cached && Date.now() - cached.fetchedAt < KEY_TTL_MS) {
return cached.key;
}
// Unknown kid or expired -- force refresh
await refreshKeys();
const refreshed = keyCache.get(kid);
if (!refreshed) {
throw new Error(`No key found for kid: ${kid}`);
}
return refreshed.key;
}

View File

@@ -1,92 +0,0 @@
import * as jose from 'jose';
import { env } from '../config/env';
export interface AccessTokenPayload {
sub: string;
type: string;
sid: string;
iat: number;
nbf: number;
exp: number;
iss?: string;
aud?: string;
}
export interface AuthContext {
userId: string;
sid: string;
token: AccessTokenPayload;
}
let jwks: ReturnType<typeof jose.createRemoteJWKSet> | null = null;
let localKey: Awaited<ReturnType<typeof jose.importSPKI>> | null = null;
function getJWKS(): ReturnType<typeof jose.createRemoteJWKSet> {
if (!jwks && env.jwt.jwksUrl) {
jwks = jose.createRemoteJWKSet(new URL(env.jwt.jwksUrl));
}
if (!jwks) {
throw new Error('JWT_JWKS_URL is not configured');
}
return jwks;
}
async function getLocalKey(): Promise<Awaited<ReturnType<typeof jose.importSPKI>>> {
if (!localKey && env.jwt.publicKey) {
localKey = await jose.importSPKI(env.jwt.publicKey, env.jwt.algorithm);
}
if (!localKey) {
throw new Error('No JWT public key available');
}
return localKey;
}
export async function verifyAccessToken(token: string): Promise<AuthContext> {
let payload: jose.JWTPayload;
try {
const verifyOptions: jose.JWTVerifyOptions = {
algorithms: [env.jwt.algorithm],
clockTolerance: 10,
};
if (env.jwt.issuer) verifyOptions.issuer = env.jwt.issuer;
if (env.jwt.audience) verifyOptions.audience = env.jwt.audience;
if (env.jwt.jwksUrl) {
const result = await jose.jwtVerify(token, getJWKS(), verifyOptions);
payload = result.payload;
} else {
const key = await getLocalKey();
const result = await jose.jwtVerify(token, key, verifyOptions);
payload = result.payload;
}
} catch (err: any) {
if (err.code === 'ERR_JWT_EXPIRED') {
throw Object.assign(new Error('Token expired'), { status: 401 });
}
throw Object.assign(new Error('Invalid token'), { status: 401 });
}
if (payload.type !== 'access') {
throw Object.assign(new Error('Invalid token type'), { status: 401 });
}
if (!payload.sub || !payload.sid) {
throw Object.assign(new Error('Missing token claims'), { status: 401 });
}
return {
userId: payload.sub,
sid: payload.sid as string,
token: {
sub: payload.sub,
type: payload.type as string,
sid: payload.sid as string,
iat: payload.iat!,
nbf: payload.nbf!,
exp: payload.exp!,
iss: payload.iss,
aud: typeof payload.aud === 'string' ? payload.aud : undefined,
},
};
}