import { Injectable } from '@nestjs/common'; import { createHash } from 'crypto'; import { OAuthSession, OAuthUserProfile, } from '../providers/oauth-provider.interface'; import type { AuthorizationCode, IOAuthStore, OAuthClient, } from './oauth-store.interface'; // In-memory storage (in production, use a database) @Injectable() export class MemoryStore implements IOAuthStore { private clients = new Map(); private authCodes = new Map(); private oauthSessions = new Map(); private userProfiles = new Map< string, OAuthUserProfile & { profile_id: string; provider: string } >(); // profile_id -> profile private providerUserIndex = new Map(); // key(provider:userId) -> profile_id async storeClient(client: OAuthClient): Promise { this.clients.set(client.client_id, client); return client; } async getClient(client_id: string): Promise { return this.clients.get(client_id); } async findClient(client_name: string): Promise { for (const client of this.clients.values()) { if (client.client_name === client_name) { return client; } } return undefined; } async storeAuthCode(code: AuthorizationCode): Promise { this.authCodes.set(code.code, code); } async getAuthCode(code: string): Promise { return this.authCodes.get(code); } async removeAuthCode(code: string): Promise { this.authCodes.delete(code); } // New OAuth session methods for provider-agnostic flow async storeOAuthSession( sessionId: string, session: OAuthSession, ): Promise { this.oauthSessions.set(sessionId, session); } async getOAuthSession(sessionId: string): Promise { const session = this.oauthSessions.get(sessionId); if (session && session.expiresAt < Date.now()) { this.oauthSessions.delete(sessionId); return undefined; } return session; } async removeOAuthSession(sessionId: string): Promise { this.oauthSessions.delete(sessionId); } generateClientId(client: OAuthClient): string { // Create deterministic client ID based on entire client object const normalizedClient = this.normalizeClientObject(client); const clientString = JSON.stringify(normalizedClient); const hash = createHash('sha256').update(clientString).digest('hex'); // Use first 16 characters of hash with client name prefix for readability const normalizedName = client.client_name .toLowerCase() .replace(/[^a-z0-9]/g, ''); return `${normalizedName}_${hash.substring(0, 16)}`; } private normalizeClientObject(client: OAuthClient): any { // Create a normalized version of the client object for consistent hashing const normalized: any = {}; // Sort object keys to ensure consistent ordering const sortedKeys = Object.keys(client).sort(); for (const key of sortedKeys) { const value = (client as any)[key]; if (Array.isArray(value)) { // Sort arrays to ensure consistent ordering normalized[key] = [...value].sort(); } else { normalized[key] = value; } } return normalized; } // User profile management async upsertUserProfile( profile: OAuthUserProfile, provider: string, ): Promise { const key = `${provider}:${profile.id}`; let profileId = this.providerUserIndex.get(key); if (!profileId) { // create new profileId = this.generateProfileId(provider, profile.id); this.providerUserIndex.set(key, profileId); } this.userProfiles.set(profileId, { profile_id: profileId, provider, ...profile, }); return profileId; } async getUserProfileById( profileId: string, ): Promise< (OAuthUserProfile & { profile_id: string; provider: string }) | undefined > { return this.userProfiles.get(profileId); } private generateProfileId(provider: string, providerUserId: string): string { const input = `${provider}:${providerUserId}`; return createHash('sha256').update(input).digest('hex').slice(0, 24); } }