import * as plugins from './plugins.js'; export interface IIdpCliConfig { idpBaseUrl: string; configDir?: string; } export interface IStoredCredentials { refreshToken?: string; jwt?: string; userId?: string; } /** * IdpCli - A Node.js CLI client for idp.global * Uses file-based storage instead of browser webstore */ export class IdpCli { public config: IIdpCliConfig; public configDir: string; public credentialsPath: string; public typedsocket: plugins.typedsocket.TypedSocket; public typedrouter = new plugins.typedrequest.TypedRouter(); private typedsocketDeferred = plugins.smartpromise.defer(); constructor(configArg: IIdpCliConfig) { this.config = configArg; this.configDir = configArg.configDir || plugins.path.join(plugins.os.homedir(), '.idp-global'); this.credentialsPath = plugins.path.join(this.configDir, 'credentials.json'); } /** * Ensure config directory exists */ private ensureConfigDir(): void { if (!plugins.fs.existsSync(this.configDir)) { plugins.fs.mkdirSync(this.configDir, { recursive: true }); } } /** * Store credentials to file */ public storeCredentials(credentials: IStoredCredentials): void { this.ensureConfigDir(); plugins.fs.writeFileSync(this.credentialsPath, JSON.stringify(credentials, null, 2), 'utf8'); } /** * Load stored credentials */ public loadCredentials(): IStoredCredentials | null { try { if (!plugins.fs.existsSync(this.credentialsPath)) { return null; } const content = plugins.fs.readFileSync(this.credentialsPath, 'utf8'); return JSON.parse(content); } catch { return null; } } /** * Delete stored credentials (logout) */ public deleteCredentials(): void { try { if (plugins.fs.existsSync(this.credentialsPath)) { plugins.fs.unlinkSync(this.credentialsPath); } } catch { // ignore if file doesn't exist } } /** * Connect to IDP server via WebSocket */ public async connect(): Promise { if (this.typedsocketDeferred.status === 'fulfilled') { return; } let baseUrl = this.config.idpBaseUrl; if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } if (!baseUrl.endsWith('/typedrequest')) { baseUrl = `${baseUrl}/typedrequest`; } console.log(`Connecting to ${baseUrl}...`); this.typedsocket = await plugins.typedsocket.TypedSocket.createClient( this.typedrouter, baseUrl ); this.typedsocketDeferred.resolve(this.typedsocket); console.log('Connected!'); } /** * Disconnect from IDP server */ public async disconnect(): Promise { if (this.typedsocket) { await this.typedsocket.stop(); } } // ============================================ // Authentication Commands // ============================================ /** * Login with email and password */ public async loginWithPassword(email: string, password: string): Promise { await this.connect(); const loginRequest = this.typedsocket.createTypedRequest( 'loginWithEmailOrUsernameAndPassword' ); const response = await loginRequest.fire({ username: email, password: password, }); if (response.refreshToken) { this.storeCredentials({ refreshToken: response.refreshToken, }); console.log('Login successful!'); return true; } else if (response.twoFaNeeded) { console.log('Two-factor authentication required.'); return false; } else { console.log('Login failed.'); return false; } } /** * Login with API token */ public async loginWithApiToken(apiToken: string): Promise { await this.connect(); const loginRequest = this.typedsocket.createTypedRequest( 'loginWithApiToken' ); const response = await loginRequest.fire({ apiToken, }); if (response.jwt) { this.storeCredentials({ jwt: response.jwt, }); console.log('Login successful!'); return true; } else { console.log('Login failed.'); return false; } } /** * Refresh JWT from stored refresh token */ public async refreshJwt(): Promise { const credentials = this.loadCredentials(); if (!credentials?.refreshToken) { console.error('No refresh token stored. Please login first.'); return null; } await this.connect(); const refreshRequest = this.typedsocket.createTypedRequest( 'refreshJwt' ); const response = await refreshRequest.fire({ refreshToken: credentials.refreshToken, }); if (response.jwt) { this.storeCredentials({ ...credentials, jwt: response.jwt, }); return response.jwt; } return null; } /** * Logout - clear stored credentials */ public async logout(): Promise { const credentials = this.loadCredentials(); if (credentials?.refreshToken) { try { await this.connect(); const logoutRequest = this.typedsocket.createTypedRequest( 'logout' ); await logoutRequest.fire({ refreshToken: credentials.refreshToken, }); } catch (e) { // Ignore errors during server-side logout } } this.deleteCredentials(); console.log('Logged out successfully.'); } // ============================================ // User Commands // ============================================ /** * Get current user info */ public async whoami(): Promise { const jwt = await this.ensureAuthenticated(); if (!jwt) return null; await this.connect(); const whoIsRequest = this.typedsocket.createTypedRequest( 'whoIs' ); const response = await whoIsRequest.fire({ jwt }); return response.user; } /** * Get user sessions */ public async getSessions(): Promise { const jwt = await this.ensureAuthenticated(); if (!jwt) return null; await this.connect(); const sessionsRequest = this.typedsocket.createTypedRequest( 'getUserSessions' ); const response = await sessionsRequest.fire({ jwt }); return response.sessions; } /** * Revoke a session */ public async revokeSession(sessionId: string): Promise { const jwt = await this.ensureAuthenticated(); if (!jwt) return false; await this.connect(); const revokeRequest = this.typedsocket.createTypedRequest( 'revokeSession' ); const response = await revokeRequest.fire({ jwt, sessionId }); return response.success; } // ============================================ // Organization Commands // ============================================ /** * Get organizations for current user */ public async getOrganizations(): Promise<{ roles: plugins.idpInterfaces.data.IRole[]; organizations: plugins.idpInterfaces.data.IOrganization[]; } | null> { const jwt = await this.ensureAuthenticated(); if (!jwt) return null; const user = await this.whoami(); if (!user) return null; await this.connect(); const orgsRequest = this.typedsocket.createTypedRequest( 'getRolesAndOrganizationsForUserId' ); const response = await orgsRequest.fire({ jwt, userId: user.id, }); return response; } /** * Create a new organization */ public async createOrganization( name: string, slug: string, mode: 'checkAvailability' | 'manifest' = 'manifest' ): Promise { const jwt = await this.ensureAuthenticated(); if (!jwt) return null; const user = await this.whoami(); if (!user) return null; await this.connect(); const createRequest = this.typedsocket.createTypedRequest( 'createOrganization' ); const response = await createRequest.fire({ jwt, userId: user.id, organizationName: name, organizationSlug: slug, action: mode, }); return response; } /** * Get organization members */ public async getOrgMembers( organizationId: string ): Promise { const jwt = await this.ensureAuthenticated(); if (!jwt) return null; await this.connect(); const membersRequest = this.typedsocket.createTypedRequest( 'getOrgMembers' ); const response = await membersRequest.fire({ jwt, organizationId, }); return response.members; } /** * Invite a user to organization */ public async inviteMember( organizationId: string, email: string, roles: string[] = ['member'] ): Promise { const jwt = await this.ensureAuthenticated(); if (!jwt) return null; await this.connect(); const inviteRequest = this.typedsocket.createTypedRequest( 'createInvitation' ); const response = await inviteRequest.fire({ jwt, organizationId, email, roles, }); return response; } // ============================================ // Admin Commands // ============================================ /** * Check if current user is global admin */ public async checkGlobalAdmin(): Promise { const jwt = await this.ensureAuthenticated(); if (!jwt) return false; await this.connect(); const adminRequest = this.typedsocket.createTypedRequest( 'checkGlobalAdmin' ); const response = await adminRequest.fire({ jwt }); return response.isGlobalAdmin; } /** * Get global app statistics (admin only) */ public async getGlobalAppStats(): Promise { const jwt = await this.ensureAuthenticated(); if (!jwt) return null; await this.connect(); const statsRequest = this.typedsocket.createTypedRequest( 'getGlobalAppStats' ); const response = await statsRequest.fire({ jwt }); return response.apps; } /** * Suspend a user (admin only) */ public async suspendUser(userId: string): Promise { const jwt = await this.ensureAuthenticated(); if (!jwt) return false; await this.connect(); const suspendRequest = this.typedsocket.createTypedRequest( 'suspendUser' ); await suspendRequest.fire({ jwt, userId }); return true; } // ============================================ // Helpers // ============================================ /** * Ensure user is authenticated, refresh JWT if needed */ private async ensureAuthenticated(): Promise { let credentials = this.loadCredentials(); if (!credentials) { console.error('Not logged in. Please run: idp login'); return null; } // If we have a JWT, return it if (credentials.jwt) { return credentials.jwt; } // Otherwise, try to get a new JWT from refresh token if (credentials.refreshToken) { const jwt = await this.refreshJwt(); return jwt; } console.error('No valid credentials. Please run: idp login'); return null; } }