This repository has no description
Something went wrong. Try again.
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161// SPDX-License-Identifier: AGPL-3.0-or-later
import {randomInt} from 'node:crypto';import {AdminACLs} from '@fluxer/constants/src/AdminACLs';import {AdminApiKeyNotFoundError} from '@fluxer/errors/src/domains/admin/AdminApiKeyNotFoundError';import {MissingACLError} from '@fluxer/errors/src/domains/core/MissingACLError';import type {CreateAdminApiKeyRequest} from '@fluxer/schema/src/domains/admin/AdminSchemas';import {ms} from 'itty-time';import type {UserID} from '../../BrandedTypes';import type {ISnowflakeService} from '../../infrastructure/ISnowflakeService';import {verifyPassword} from '../../utils/PasswordUtils';import type {IAdminApiKeyRepository} from '../repositories/IAdminApiKeyRepository';
const ADMIN_KEY_PREFIX = 'fa_';const RANDOM_KEY_LENGTH = 32;const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
interface CreateApiKeyResult { key: string; apiKey: { keyId: string; name: string; createdAt: Date; expiresAt: Date | null; acls: Set<string>; };}
export class AdminApiKeyService { constructor( private readonly adminApiKeyRepository: IAdminApiKeyRepository, private readonly snowflakeService: ISnowflakeService, ) {}
private generateRawKey(keyId: bigint): string { const randomChars = Array.from({length: RANDOM_KEY_LENGTH}, () => CHARSET[randomInt(CHARSET.length)]).join(''); return `${ADMIN_KEY_PREFIX}${keyId.toString()}_${randomChars}`; }
private extractKeyId(rawKey: string): bigint | null { if (!rawKey.startsWith(ADMIN_KEY_PREFIX)) { return null; } const remainder = rawKey.slice(ADMIN_KEY_PREFIX.length); const underscoreIdx = remainder.indexOf('_'); if (underscoreIdx <= 0) { return null; } const keyIdStr = remainder.slice(0, underscoreIdx); if (!/^\d+$/.test(keyIdStr)) { return null; } try { return BigInt(keyIdStr); } catch { return null; } }
async createApiKey( request: CreateAdminApiKeyRequest, createdBy: UserID, creatorAcls?: Set<string>, ): Promise<CreateApiKeyResult> { const keyId = await this.snowflakeService.generate(); const rawKey = this.generateRawKey(keyId); const expiresAt = request.expires_in_days ? new Date(Date.now() + request.expires_in_days * ms('1 day')) : null; if (creatorAcls) { const invalidACLs = request.acls.filter((acl) => !creatorAcls.has(acl) && !creatorAcls.has(AdminACLs.WILDCARD)); if (invalidACLs.length > 0) { throw new MissingACLError(invalidACLs[0]); } } const aclsSet = new Set(request.acls); const apiKey = await this.adminApiKeyRepository.create( { name: request.name, expiresAt, acls: aclsSet, }, createdBy, keyId, rawKey, ); return { key: rawKey, apiKey: { keyId: apiKey.keyId.toString(), name: apiKey.name, createdAt: apiKey.createdAt, expiresAt: apiKey.expiresAt, acls: apiKey.acls, }, }; }
async validateApiKey(rawKey: string): Promise<{ keyId: bigint; createdById: UserID; acls: Set<string> | null; } | null> { const keyId = this.extractKeyId(rawKey); if (keyId === null) return null; const apiKey = await this.adminApiKeyRepository.findById(keyId); if (!apiKey) { return null; } if (apiKey.isExpired()) { return null; } const valid = await verifyPassword({password: rawKey, passwordHash: apiKey.keyHash}); if (!valid) { return null; } await this.adminApiKeyRepository.updateLastUsed(apiKey.keyId, apiKey.expiresAt); return { keyId: apiKey.keyId, createdById: apiKey.createdById, acls: apiKey.acls, }; }
async listKeys(createdBy: UserID): Promise< Array<{ keyId: string; name: string; createdAt: Date; lastUsedAt: Date | null; expiresAt: Date | null; createdById: UserID; acls: Set<string>; }> > { const apiKeys = await this.adminApiKeyRepository.listByCreator(createdBy); return apiKeys.map((key) => ({ keyId: key.keyId.toString(), name: key.name, createdAt: key.createdAt, lastUsedAt: key.lastUsedAt, expiresAt: key.expiresAt, createdById: key.createdById, acls: key.acls ?? new Set(), })); }
async revokeKey(keyId: string, createdBy: UserID): Promise<void> { if (!/^\d+$/.test(keyId)) { throw new AdminApiKeyNotFoundError(); } const keyIdBigInt = BigInt(keyId); const apiKey = await this.adminApiKeyRepository.findById(keyIdBigInt); if (!apiKey) { throw new AdminApiKeyNotFoundError(); } if (apiKey.createdById !== createdBy) { throw new AdminApiKeyNotFoundError(); } await this.adminApiKeyRepository.revoke(keyIdBigInt, createdBy); }}