import jwt from 'jsonwebtoken';
import axios from 'axios';
import db, { schema } from '../db/database.js';
import analytics, { AnalyticsTypes } from '../utils/analytics.js';
import { updateAction as updateActivityLogAction, createActivityLogMessage, createActivityLogMessageAndEnd, updateProvider as updateProviderActivityLog, updateSuccess as updateSuccessActivityLog } from '../services/activity/activity.service.js';
import { LogActionEnum } from '../models/Activity.js';
import providerClient from '../clients/provider.client.js';
import configService from '../services/config.service.js';
import syncOrchestrator from './sync/orchestrator.service.js';
import environmentService from '../services/environment.service.js';
import { getFreshOAuth2Credentials } from '../clients/oauth2.client.js';
import { NangoError } from '../utils/error.js';
import { getLogger, stringifyError } from '@nangohq/utils';
import encryptionManager from '../utils/encryption.manager.js';
import telemetry, { LogTypes } from '../utils/telemetry.js';
import { AuthModes as ProviderAuthModes, AuthOperation } from '../models/Auth.js';
import { interpolateStringFromObject, parseTokenExpirationDate, isTokenExpired, getRedisUrl } from '../utils/utils.js';
import { connectionCreated as connectionCreatedHook } from '../hooks/hooks.js';
import { Locking } from '../utils/lock/locking.js';
import { InMemoryKVStore } from '../utils/kvstore/InMemoryStore.js';
import { RedisKVStore } from '../utils/kvstore/RedisStore.js';
import { CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT } from '../constants.js';
const logger = getLogger('Connection');
class ConnectionService {
    locking;
    constructor(locking) {
        this.locking = locking;
    }
    async upsertConnection(connectionId, providerConfigKey, provider, parsedRawCredentials, connectionConfig, environment_id, accountId, metadata) {
        const storedConnection = await this.checkIfConnectionExists(connectionId, providerConfigKey, environment_id);
        const config_id = await configService.getIdByProviderConfigKey(environment_id, providerConfigKey);
        if (storedConnection) {
            const encryptedConnection = encryptionManager.encryptConnection({
                connection_id: connectionId,
                provider_config_key: providerConfigKey,
                credentials: parsedRawCredentials,
                connection_config: connectionConfig,
                environment_id: environment_id,
                config_id: config_id,
                metadata: metadata || storedConnection.metadata || null
            });
            encryptedConnection.updated_at = new Date();
            await db.knex.from(`_nango_connections`).where({ id: storedConnection.id, deleted: false }).update(encryptedConnection);
            void analytics.track(AnalyticsTypes.CONNECTION_UPDATED, accountId, { provider });
            return [{ id: storedConnection.id, operation: AuthOperation.OVERRIDE }];
        }
        const [id] = await db.knex.from(`_nango_connections`).insert(encryptionManager.encryptConnection({
            connection_id: connectionId,
            provider_config_key: providerConfigKey,
            config_id: config_id,
            credentials: parsedRawCredentials,
            connection_config: connectionConfig,
            environment_id: environment_id,
            metadata: metadata || null
        }), ['id']);
        void analytics.track(AnalyticsTypes.CONNECTION_INSERTED, accountId, { provider });
        return [{ id: id.id, operation: AuthOperation.CREATION }];
    }
    async upsertApiConnection(connectionId, providerConfigKey, provider, credentials, connectionConfig, environment_id, accountId) {
        const storedConnection = await this.checkIfConnectionExists(connectionId, providerConfigKey, environment_id);
        const config_id = await configService.getIdByProviderConfigKey(environment_id, providerConfigKey);
        if (storedConnection) {
            const encryptedConnection = encryptionManager.encryptConnection({
                connection_id: connectionId,
                config_id: config_id,
                provider_config_key: providerConfigKey,
                credentials,
                connection_config: connectionConfig,
                environment_id
            });
            encryptedConnection.updated_at = new Date();
            await db.knex.from(`_nango_connections`).where({ id: storedConnection.id, deleted: false }).update(encryptedConnection);
            void analytics.track(AnalyticsTypes.API_CONNECTION_UPDATED, accountId, { provider });
            return [{ id: storedConnection.id, operation: AuthOperation.OVERRIDE }];
        }
        const [id] = await db.knex.from(`_nango_connections`).insert(encryptionManager.encryptApiConnection({
            connection_id: connectionId,
            provider_config_key: providerConfigKey,
            config_id: config_id,
            credentials,
            connection_config: connectionConfig,
            environment_id
        }), ['id']);
        void analytics.track(AnalyticsTypes.API_CONNECTION_INSERTED, accountId, { provider });
        return [{ id: id.id, operation: AuthOperation.CREATION }];
    }
    async upsertUnauthConnection(connectionId, providerConfigKey, provider, environment_id, accountId) {
        const storedConnection = await this.checkIfConnectionExists(connectionId, providerConfigKey, environment_id);
        const config_id = await configService.getIdByProviderConfigKey(environment_id, providerConfigKey);
        if (storedConnection) {
            await db.knex
                .from(`_nango_connections`)
                .where({ id: storedConnection.id, deleted: false })
                .update({
                connection_id: connectionId,
                provider_config_key: providerConfigKey,
                config_id: config_id,
                updated_at: new Date()
            });
            void analytics.track(AnalyticsTypes.UNAUTH_CONNECTION_UPDATED, accountId, { provider });
            return [{ id: storedConnection.id, operation: AuthOperation.OVERRIDE }];
        }
        const [id] = await db.knex.from(`_nango_connections`).insert({
            connection_id: connectionId,
            provider_config_key: providerConfigKey,
            credentials: {},
            connection_config: {},
            environment_id,
            config_id: config_id
        }, ['id']);
        void analytics.track(AnalyticsTypes.UNAUTH_CONNECTION_INSERTED, accountId, { provider });
        return [{ id: id.id, operation: AuthOperation.CREATION }];
    }
    async importOAuthConnection(connection_id, provider_config_key, provider, environmentId, accountId, parsedRawCredentials, logContextGetter) {
        const { connection_config, metadata } = parsedRawCredentials;
        const [importedConnection] = await this.upsertConnection(connection_id, provider_config_key, provider, parsedRawCredentials, connection_config || {}, environmentId, accountId, metadata || undefined);
        if (importedConnection) {
            void connectionCreatedHook({
                id: importedConnection?.id,
                connection_id,
                provider_config_key,
                environment_id: environmentId,
                auth_mode: ProviderAuthModes.OAuth2,
                operation: importedConnection?.operation
            }, provider, logContextGetter, null);
        }
        return [importedConnection];
    }
    async importApiAuthConnection(connection_id, provider_config_key, provider, environmentId, accountId, credentials, logContextGetter) {
        const connection = await this.checkIfConnectionExists(connection_id, provider_config_key, environmentId);
        if (connection) {
            throw new NangoError('connection_already_exists');
        }
        const [importedConnection] = await this.upsertApiConnection(connection_id, provider_config_key, provider, credentials, {}, environmentId, accountId);
        if (importedConnection) {
            void connectionCreatedHook({
                id: importedConnection.id,
                connection_id,
                provider_config_key,
                environment_id: environmentId,
                auth_mode: ProviderAuthModes.ApiKey,
                operation: importedConnection.operation
            }, provider, logContextGetter, null);
        }
        return [importedConnection];
    }
    async getConnectionById(id) {
        const result = await schema()
            .select('id', 'connection_id', 'provider_config_key', 'environment_id', 'connection_config', 'metadata')
            .from('_nango_connections')
            .where({ id: id, deleted: false });
        if (!result || result.length == 0 || !result[0]) {
            return null;
        }
        return result[0];
    }
    async checkIfConnectionExists(connection_id, provider_config_key, environment_id) {
        const result = await schema().select('id', 'metadata').from('_nango_connections').where({
            connection_id,
            provider_config_key,
            environment_id,
            deleted: false
        });
        if (!result || result.length == 0 || !result[0]) {
            return null;
        }
        return result[0];
    }
    async getConnection(connectionId, providerConfigKey, environment_id) {
        if (!environment_id) {
            const error = new NangoError('missing_environment');
            return { success: false, error, response: null };
        }
        if (!connectionId) {
            const error = new NangoError('missing_connection');
            await telemetry.log(LogTypes.GET_CONNECTION_FAILURE, error.message, LogActionEnum.AUTH, {
                environmentId: String(environment_id),
                connectionId,
                providerConfigKey,
                level: 'error'
            });
            return { success: false, error, response: null };
        }
        if (!providerConfigKey) {
            const error = new NangoError('missing_provider_config');
            await telemetry.log(LogTypes.GET_CONNECTION_FAILURE, error.message, LogActionEnum.AUTH, {
                environmentId: String(environment_id),
                connectionId,
                providerConfigKey,
                level: 'error'
            });
            return { success: false, error, response: null };
        }
        const result = (await schema()
            .select('*')
            .from(`_nango_connections`)
            .where({ connection_id: connectionId, provider_config_key: providerConfigKey, environment_id, deleted: false }));
        const storedConnection = result == null || result.length == 0 ? null : result[0] || null;
        if (!storedConnection) {
            const environmentName = await environmentService.getEnvironmentName(environment_id);
            const error = new NangoError('unknown_connection', { connectionId, providerConfigKey, environmentName });
            await telemetry.log(LogTypes.GET_CONNECTION_FAILURE, error.message, LogActionEnum.AUTH, {
                environmentId: String(environment_id),
                connectionId,
                providerConfigKey,
                level: 'error'
            });
            return { success: false, error, response: null };
        }
        const connection = encryptionManager.decryptConnection(storedConnection);
        // Parse the token expiration date.
        if (connection != null) {
            const credentials = connection.credentials;
            if (credentials.type && credentials.type === ProviderAuthModes.OAuth2) {
                const creds = credentials;
                creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined;
                connection.credentials = creds;
            }
            if (credentials.type && credentials.type === ProviderAuthModes.App) {
                const creds = credentials;
                creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined;
                connection.credentials = creds;
            }
            if (credentials.type && credentials.type === ProviderAuthModes.OAuth2CC) {
                const creds = credentials;
                creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined;
                connection.credentials = creds;
            }
        }
        await this.updateLastFetched(connection?.id);
        return { success: true, error: null, response: connection };
    }
    async updateConnection(connection) {
        await db.knex
            .from(`_nango_connections`)
            .where({
            connection_id: connection.connection_id,
            provider_config_key: connection.provider_config_key,
            environment_id: connection.environment_id,
            deleted: false
        })
            .update(encryptionManager.encryptConnection(connection));
    }
    async getMetadata(connection) {
        const result = await db.knex.from(`_nango_connections`).select('metadata').where({
            connection_id: connection.connection_id,
            provider_config_key: connection.provider_config_key,
            environment_id: connection.environment_id,
            deleted: false
        });
        if (!result || result.length == 0 || !result[0]) {
            return {};
        }
        return result[0].metadata;
    }
    async getConnectionConfig(connection) {
        const result = await db.knex.from(`_nango_connections`).select('connection_config').where({
            connection_id: connection.connection_id,
            provider_config_key: connection.provider_config_key,
            environment_id: connection.environment_id,
            deleted: false
        });
        if (!result || result.length == 0 || !result[0]) {
            return {};
        }
        return result[0].connection_config;
    }
    async getConnectionsByEnvironmentAndConfig(environment_id, providerConfigKey) {
        const result = await db.knex
            .from(`_nango_connections`)
            .select('id', 'connection_id', 'provider_config_key', 'environment_id', 'connection_config')
            .where({ environment_id, provider_config_key: providerConfigKey, deleted: false });
        if (!result || result.length == 0 || !result[0]) {
            return [];
        }
        return result;
    }
    async getOldConnections({ days, limit }) {
        const dateThreshold = new Date();
        dateThreshold.setDate(dateThreshold.getDate() - days);
        const result = await db
            .knex('_nango_connections')
            .join('_nango_configs', '_nango_connections.config_id', '_nango_configs.id')
            .join('_nango_environments', '_nango_connections.environment_id', '_nango_environments.id')
            .select('connection_id', '_nango_connections.environment_id', 'unique_key as provider_config_key', 'account_id')
            .where('last_fetched_at', '<', dateThreshold)
            .orWhere('last_fetched_at', null)
            .andWhere('_nango_connections.deleted', false)
            .limit(limit);
        if (!result || result.length === 0) {
            return [];
        }
        return result;
    }
    async replaceMetadata(connection, metadata) {
        await db.knex
            .from(`_nango_connections`)
            .where({ id: connection.id, deleted: false })
            .update({ metadata });
    }
    async replaceConnectionConfig(connection, config) {
        await db.knex
            .from(`_nango_connections`)
            .where({ id: connection.id, deleted: false })
            .update({ connection_config: config });
    }
    async updateMetadata(connection, metadata) {
        const existingMetadata = await this.getMetadata(connection);
        const newMetadata = { ...existingMetadata, ...metadata };
        await this.replaceMetadata(connection, newMetadata);
        return newMetadata;
    }
    async updateConnectionConfig(connection, config) {
        const existingConfig = await this.getConnectionConfig(connection);
        const newConfig = { ...existingConfig, ...config };
        await this.replaceConnectionConfig(connection, newConfig);
        return newConfig;
    }
    async findConnectionsByConnectionConfigValue(key, value, environmentId) {
        const result = await db.knex
            .from(`_nango_connections`)
            .select('*')
            .where({ environment_id: environmentId })
            .whereRaw(`connection_config->>:key = :value AND deleted = false`, { key, value });
        if (!result || result.length == 0) {
            return null;
        }
        return result.map((connection) => encryptionManager.decryptConnection(connection));
    }
    async findConnectionsByMultipleConnectionConfigValues(keyValuePairs, environmentId) {
        let query = db.knex.from(`_nango_connections`).select('*').where({ environment_id: environmentId });
        Object.entries(keyValuePairs).forEach(([key, value]) => {
            query = query.andWhereRaw(`connection_config->>:key = :value AND deleted = false`, { key, value });
        });
        const result = await query;
        if (!result || result.length == 0) {
            return null;
        }
        return result.map((connection) => encryptionManager.decryptConnection(connection));
    }
    async listConnections(environment_id, connectionId) {
        const queryBuilder = db.knex
            .from(`_nango_connections`)
            .select({ id: 'id' }, { connection_id: 'connection_id' }, { provider: 'provider_config_key' }, { created: 'created_at' }, 'metadata')
            .where({ environment_id, deleted: false });
        if (connectionId) {
            queryBuilder.where({ connection_id: connectionId });
        }
        return queryBuilder;
    }
    async getAllNames(environment_id) {
        const connections = await this.listConnections(environment_id);
        return [...new Set(connections.map((config) => config.connection_id))];
    }
    async deleteConnection(connection, providerConfigKey, environment_id) {
        const del = await db.knex
            .from(`_nango_connections`)
            .where({
            connection_id: connection.connection_id,
            provider_config_key: providerConfigKey,
            environment_id,
            deleted: false
        })
            .update({ deleted: true, credentials: {}, credentials_iv: null, credentials_tag: null, deleted_at: new Date() });
        await syncOrchestrator.softDeleteSyncsByConnection(connection);
        return del;
    }
    async getConnectionCredentials(accountId, environmentId, connectionId, providerConfigKey, logContextGetter, activityLogId, logCtx, action, instantRefresh = false) {
        if (connectionId === null) {
            const error = new NangoError('missing_connection');
            return { success: false, error, response: null };
        }
        if (providerConfigKey === null) {
            const error = new NangoError('missing_provider_config');
            return { success: false, error, response: null };
        }
        const { success, error, response: connection } = await this.getConnection(connectionId, providerConfigKey, environmentId);
        if (!success) {
            return { success, error, response: null };
        }
        if (connection === null) {
            if (activityLogId) {
                await createActivityLogMessageAndEnd({
                    level: 'error',
                    environment_id: environmentId,
                    activity_log_id: activityLogId,
                    content: `Connection not found using connectionId: ${connectionId} and providerConfigKey: ${providerConfigKey} and the environment: ${environmentId}`,
                    timestamp: Date.now()
                });
            }
            const environmentName = await environmentService.getEnvironmentName(environmentId);
            const error = new NangoError('unknown_connection', { connectionId, providerConfigKey, environmentName });
            return { success: false, error, response: null };
        }
        const config = await configService.getProviderConfig(connection?.provider_config_key, environmentId);
        if (activityLogId && config) {
            await updateProviderActivityLog(activityLogId, config.provider);
            await logCtx?.enrichOperation({ configId: config.id, configName: config.unique_key });
        }
        if (config === null) {
            if (activityLogId) {
                await createActivityLogMessageAndEnd({
                    level: 'error',
                    environment_id: environmentId,
                    activity_log_id: activityLogId,
                    content: `Configuration not found using the providerConfigKey: ${providerConfigKey}, the account id: ${accountId} and the environment: ${environmentId}`,
                    timestamp: Date.now()
                });
                await logCtx?.error(`Configuration not found using the providerConfigKey: ${providerConfigKey}, the account id: ${accountId} and the environment: ${environmentId}`);
            }
            const error = new NangoError('unknown_provider_config');
            return { success: false, error, response: null };
        }
        const template = configService.getTemplate(config?.provider);
        if (connection?.credentials?.type === ProviderAuthModes.OAuth2 ||
            connection?.credentials?.type === ProviderAuthModes.App ||
            connection?.credentials?.type === ProviderAuthModes.OAuth2CC) {
            const { success, error, response: credentials } = await this.refreshCredentialsIfNeeded({
                connection,
                providerConfig: config,
                template: template,
                activityLogId,
                environment_id: environmentId,
                instantRefresh,
                logAction: action,
                logContextGetter
            });
            if (!success) {
                return { success, error, response: null };
            }
            connection.credentials = credentials;
        }
        return { success: true, error: null, response: connection };
    }
    async updateLastFetched(id) {
        await db.knex.from(`_nango_connections`).where({ id, deleted: false }).update({ last_fetched_at: new Date() });
    }
    // Parses and arbitrary object (e.g. a server response or a user provided auth object) into AuthCredentials.
    // Throws if values are missing/missing the input is malformed.
    parseRawCredentials(rawCredentials, authMode) {
        const rawCreds = rawCredentials;
        switch (authMode) {
            case ProviderAuthModes.OAuth2: {
                if (!rawCreds['access_token']) {
                    throw new NangoError(`incomplete_raw_credentials`);
                }
                let expiresAt;
                if (rawCreds['expires_at']) {
                    expiresAt = parseTokenExpirationDate(rawCreds['expires_at']);
                }
                else if (rawCreds['expires_in']) {
                    expiresAt = new Date(Date.now() + Number.parseInt(rawCreds['expires_in'], 10) * 1000);
                }
                const oauth2Creds = {
                    type: ProviderAuthModes.OAuth2,
                    access_token: rawCreds['access_token'],
                    refresh_token: rawCreds['refresh_token'],
                    expires_at: expiresAt,
                    raw: rawCreds
                };
                return oauth2Creds;
            }
            case ProviderAuthModes.OAuth1: {
                if (!rawCreds['oauth_token'] || !rawCreds['oauth_token_secret']) {
                    throw new NangoError(`incomplete_raw_credentials`);
                }
                const oauth1Creds = {
                    type: ProviderAuthModes.OAuth1,
                    oauth_token: rawCreds['oauth_token'],
                    oauth_token_secret: rawCreds['oauth_token_secret'],
                    raw: rawCreds
                };
                return oauth1Creds;
            }
            case ProviderAuthModes.OAuth2CC: {
                if (!rawCreds['token']) {
                    throw new NangoError(`incomplete_raw_credentials`);
                }
                let expiresAt;
                if (rawCreds['expires_at']) {
                    expiresAt = parseTokenExpirationDate(rawCreds['expires_at']);
                }
                else if (rawCreds['expires_in']) {
                    expiresAt = new Date(Date.now() + Number.parseInt(rawCreds['expires_in'], 10) * 1000);
                }
                const oauth2Creds = {
                    type: ProviderAuthModes.OAuth2CC,
                    token: rawCreds['token'],
                    client_id: '',
                    client_secret: '',
                    expires_at: expiresAt,
                    raw: rawCreds
                };
                return oauth2Creds;
            }
            default:
                throw new NangoError(`Cannot parse credentials, unknown credentials type: ${JSON.stringify(rawCreds, undefined, 2)}`);
        }
    }
    async refreshCredentialsIfNeeded({ connection, providerConfig, template, activityLogId = null, environment_id, instantRefresh = false, logAction = 'token', logContextGetter }) {
        const connectionId = connection.connection_id;
        const credentials = connection.credentials;
        const providerConfigKey = connection.provider_config_key;
        const shouldRefresh = await this.shouldRefreshCredentials(connection, credentials, providerConfig, template, instantRefresh);
        if (shouldRefresh) {
            await telemetry.log(LogTypes.AUTH_TOKEN_REFRESH_START, 'Token refresh is being started', LogActionEnum.AUTH, {
                environmentId: String(environment_id),
                connectionId,
                providerConfigKey,
                provider: providerConfig.provider
            });
            // We must ensure that only one refresh is running at a time accross all instances.
            // Using a simple redis entry as a lock with a TTL to ensure it is always released.
            // NOTES:
            // - This is not a distributed lock and will not work in a multi-redis environment.
            // - It could also be unsafe in case of a Redis crash.
            // We are using this for now as it is a simple solution that should work for most cases.
            const lockKey = `lock:refresh:${environment_id}:${providerConfigKey}:${connectionId}`;
            try {
                const ttlInMs = 10000;
                const acquitistionTimeoutMs = ttlInMs * 1.2; // giving some extra time for the lock to be released
                await this.locking.tryAcquire(lockKey, ttlInMs, acquitistionTimeoutMs);
                const { success, error, response: newCredentials } = await this.getNewCredentials(connection, providerConfig, template);
                if (!success || !newCredentials) {
                    await telemetry.log(LogTypes.AUTH_TOKEN_REFRESH_FAILURE, `Token refresh failed, ${error?.message}`, LogActionEnum.AUTH, {
                        environmentId: String(environment_id),
                        connectionId,
                        providerConfigKey,
                        provider: providerConfig.provider,
                        level: 'error'
                    });
                    return { success, error, response: null };
                }
                connection.credentials = newCredentials;
                await this.updateConnection(connection);
                await telemetry.log(LogTypes.AUTH_TOKEN_REFRESH_SUCCESS, 'Token refresh was successful', LogActionEnum.AUTH, {
                    environmentId: String(environment_id),
                    connectionId,
                    providerConfigKey,
                    provider: providerConfig.provider
                });
                return { success: true, error: null, response: newCredentials };
            }
            catch (e) {
                if (activityLogId && logAction === 'token') {
                    await this.logErrorActivity(activityLogId, environment_id, `Refresh oauth2 token call failed`, logContextGetter);
                }
                const errorMessage = e.message || 'Unknown error';
                const errorDetails = {
                    message: errorMessage,
                    name: e.name || 'Error',
                    stack: e.stack || 'No stack trace'
                };
                const errorString = JSON.stringify(errorDetails);
                await telemetry.log(LogTypes.AUTH_TOKEN_REFRESH_FAILURE, `Token refresh failed, ${errorString}`, LogActionEnum.AUTH, {
                    environmentId: String(environment_id),
                    connectionId,
                    providerConfigKey,
                    provider: providerConfig.provider,
                    level: 'error'
                });
                const error = new NangoError('refresh_token_external_error', e);
                return { success: false, error, response: null };
            }
            finally {
                this.locking.release(lockKey);
            }
        }
        return { success: true, error: null, response: credentials };
    }
    async getAppStoreCredentials(template, connectionConfig, privateKey) {
        const templateTokenUrl = typeof template.token_url === 'string' ? template.token_url : template.token_url[ProviderAuthModes.AppStore];
        const tokenUrl = interpolateStringFromObject(templateTokenUrl, { connectionConfig });
        const now = Math.floor(Date.now() / 1000);
        const expiration = now + 15 * 60;
        const payload = {
            iat: now,
            exp: expiration,
            iss: connectionConfig['issuerId']
        };
        if (template.authorization_params && template.authorization_params['audience']) {
            payload['aud'] = template.authorization_params['audience'];
        }
        if (connectionConfig['scope']) {
            payload['scope'] = connectionConfig['scope'];
        }
        const { success, error, response: rawCredentials } = await this.getJWTCredentials(privateKey, tokenUrl, payload, null, {
            header: {
                alg: 'ES256',
                kid: connectionConfig['privateKeyId'],
                typ: 'JWT'
            }
        });
        if (!success || !rawCredentials) {
            return { success, error, response: null };
        }
        const credentials = {
            type: ProviderAuthModes.AppStore,
            access_token: rawCredentials?.token,
            private_key: Buffer.from(privateKey).toString('base64'),
            expires_at: rawCredentials?.expires_at,
            raw: rawCredentials
        };
        return { success: true, error: null, response: credentials };
    }
    async getAppCredentialsAndFinishConnection(connectionId, integration, template, connectionConfig, activityLogId, logCtx, logContextGetter) {
        const { success, error, response: credentials } = await this.getAppCredentials(template, integration, connectionConfig);
        if (!success || !credentials) {
            logger.error(error);
            return;
        }
        const accountId = await environmentService.getAccountIdFromEnvironment(integration.environment_id);
        const [updatedConnection] = await this.upsertConnection(connectionId, integration.unique_key, integration.provider, credentials, connectionConfig, integration.environment_id, accountId);
        if (updatedConnection) {
            void connectionCreatedHook({
                id: updatedConnection.id,
                connection_id: connectionId,
                provider_config_key: integration.unique_key,
                environment_id: integration.environment_id,
                auth_mode: ProviderAuthModes.App,
                operation: updatedConnection.operation
            }, integration.provider, logContextGetter, activityLogId, 
            // the connection is complete so we want to initiate syncs
            // the post connection script has run already because we needed to get the github handle
            { initiateSync: true, runPostConnectionScript: false }, logCtx);
        }
        await createActivityLogMessageAndEnd({
            level: 'info',
            environment_id: integration.environment_id,
            activity_log_id: Number(activityLogId),
            content: 'App connection was approved and credentials were saved',
            timestamp: Date.now()
        });
        await logCtx.info('App connection was approved and credentials were saved');
        await updateSuccessActivityLog(Number(activityLogId), true);
    }
    async getAppCredentials(template, config, connectionConfig) {
        const templateTokenUrl = typeof template.token_url === 'string' ? template.token_url : template.token_url[ProviderAuthModes.App];
        const tokenUrl = interpolateStringFromObject(templateTokenUrl, { connectionConfig });
        const privateKeyBase64 = config?.custom ? config.custom['private_key'] : config.oauth_client_secret;
        const privateKey = Buffer.from(privateKeyBase64, 'base64').toString('utf8');
        const headers = {
            Accept: 'application/vnd.github.v3+json'
        };
        const now = Math.floor(Date.now() / 1000);
        const expiration = now + 10 * 60;
        const payload = {
            iat: now,
            exp: expiration,
            iss: (config?.custom ? config.custom['app_id'] : config.oauth_client_id)
        };
        const { success, error, response: rawCredentials } = await this.getJWTCredentials(privateKey, tokenUrl, payload, headers, { algorithm: 'RS256' });
        if (!success || !rawCredentials) {
            return { success, error, response: null };
        }
        const credentials = {
            type: ProviderAuthModes.App,
            access_token: rawCredentials?.token,
            expires_at: rawCredentials?.expires_at,
            raw: rawCredentials
        };
        return { success: true, error: null, response: credentials };
    }
    async getOauthClientCredentials(template, client_id, client_secret) {
        const url = template.authorization_url;
        let authorizationParams = '';
        if (template.authorization_params && Object.keys(template.authorization_params).length > 0) {
            authorizationParams = new URLSearchParams(template.authorization_params).toString();
        }
        try {
            const params = new URLSearchParams();
            params.append('client_id', client_id);
            params.append('client_secret', client_secret);
            if (authorizationParams) {
                const authorizationParamsEntries = new URLSearchParams(authorizationParams).entries();
                for (const [key, value] of authorizationParamsEntries) {
                    params.append(key, value);
                }
            }
            const fullUrl = `${url}?${params}`;
            const response = await axios.post(fullUrl);
            const { data } = response;
            if (!data || !data.success) {
                return { success: false, error: new NangoError('invalid_client_credentials'), response: null };
            }
            const parsedCreds = this.parseRawCredentials(data.data, ProviderAuthModes.OAuth2CC);
            parsedCreds.client_id = client_id;
            parsedCreds.client_secret = client_secret;
            return { success: true, error: null, response: parsedCreds };
        }
        catch (e) {
            const errorPayload = {
                message: e.message || 'Unknown error',
                name: e.name || 'Error'
            };
            logger.error(`Error fetching client credentials ${stringifyError(e)}`);
            const error = new NangoError('client_credentials_fetch_error', errorPayload);
            return { success: false, error, response: null };
        }
    }
    async shouldCapUsage({ providerConfigKey, environmentId, type }) {
        const connections = await this.getConnectionsByEnvironmentAndConfig(environmentId, providerConfigKey);
        if (!connections) {
            return false;
        }
        if (connections.length > CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT) {
            logger.info(`Reached cap for providerConfigKey: ${providerConfigKey} and environmentId: ${environmentId}`);
            if (type === 'deploy') {
                void analytics.trackByEnvironmentId(AnalyticsTypes.RESOURCE_CAPPED_SCRIPT_DEPLOY_IS_DISABLED, environmentId);
            }
            else {
                void analytics.trackByEnvironmentId(AnalyticsTypes.RESOURCE_CAPPED_SCRIPT_ACTIVATE, environmentId);
            }
            return true;
        }
        return false;
    }
    async getJWTCredentials(privateKey, url, payload, additionalApiHeaders, options) {
        const hasLineBreak = /^-----BEGIN RSA PRIVATE KEY-----\n/.test(privateKey);
        if (!hasLineBreak) {
            privateKey = privateKey.replace('-----BEGIN RSA PRIVATE KEY-----', '-----BEGIN RSA PRIVATE KEY-----\n');
            privateKey = privateKey.replace('-----END RSA PRIVATE KEY-----', '\n-----END RSA PRIVATE KEY-----');
        }
        try {
            const token = jwt.sign(payload, privateKey, options);
            const headers = {
                Authorization: `Bearer ${token}`
            };
            if (additionalApiHeaders) {
                Object.assign(headers, additionalApiHeaders);
            }
            const tokenResponse = await axios.post(url, {}, {
                headers
            });
            return { success: true, error: null, response: tokenResponse.data };
        }
        catch (e) {
            const errorPayload = {
                message: e.message || 'Unknown error',
                name: e.name || 'Error'
            };
            const error = new NangoError('refresh_token_external_error', errorPayload);
            return { success: false, error, response: null };
        }
    }
    async shouldRefreshCredentials(connection, credentials, providerConfig, template, instantRefresh) {
        const refreshCondition = instantRefresh ||
            (providerClient.shouldIntrospectToken(providerConfig.provider) && (await providerClient.introspectedTokenExpired(providerConfig, connection)));
        let tokenExpirationCondition = refreshCondition || (credentials.expires_at && isTokenExpired(credentials.expires_at, template.token_expiration_buffer || 15 * 60));
        if ((template.auth_mode === ProviderAuthModes.OAuth2 || credentials?.type === ProviderAuthModes.OAuth2) && providerConfig.provider !== 'facebook') {
            tokenExpirationCondition = Boolean(credentials.refresh_token && tokenExpirationCondition);
        }
        return Boolean(tokenExpirationCondition);
    }
    async getNewCredentials(connection, providerConfig, template) {
        if (providerClient.shouldUseProviderClient(providerConfig.provider)) {
            const rawCreds = await providerClient.refreshToken(template, providerConfig, connection);
            const parsedCreds = this.parseRawCredentials(rawCreds, ProviderAuthModes.OAuth2);
            return { success: true, error: null, response: parsedCreds };
        }
        else if (template.auth_mode === ProviderAuthModes.OAuth2CC) {
            const { client_id, client_secret } = connection.credentials;
            const { success, error, response: credentials } = await this.getOauthClientCredentials(template, client_id, client_secret);
            if (!success || !credentials) {
                return { success, error, response: null };
            }
            return { success: true, error: null, response: credentials };
        }
        else if (template.auth_mode === ProviderAuthModes.AppStore) {
            const { private_key } = connection.credentials;
            const { success, error, response: credentials } = await this.getAppStoreCredentials(template, connection.connection_config, private_key);
            if (!success || !credentials) {
                return { success, error, response: null };
            }
            return { success: true, error: null, response: credentials };
        }
        else if (template.auth_mode === ProviderAuthModes.App ||
            (template.auth_mode === ProviderAuthModes.Custom && connection?.credentials?.type !== ProviderAuthModes.OAuth2)) {
            const { success, error, response: credentials } = await this.getAppCredentials(template, providerConfig, connection.connection_config);
            if (!success || !credentials) {
                return { success, error, response: null };
            }
            return { success: true, error: null, response: credentials };
        }
        else {
            const { success, error, response: creds } = await getFreshOAuth2Credentials(connection, providerConfig, template);
            return { success, error, response: success ? creds : null };
        }
    }
    async logErrorActivity(activityLogId, environment_id, message, logContextGetter) {
        await updateActivityLogAction(activityLogId, 'token');
        await createActivityLogMessage({
            level: 'error',
            environment_id,
            activity_log_id: activityLogId,
            content: message,
            timestamp: Date.now()
        });
        const logCtx = logContextGetter.get({ id: String(activityLogId) });
        await logCtx.error(message);
    }
}
const locking = await (async () => {
    let store;
    const url = getRedisUrl();
    if (url) {
        store = new RedisKVStore(url);
        await store.connect();
    }
    else {
        store = new InMemoryKVStore();
    }
    return new Locking(store);
})();
export default new ConnectionService(locking);
//# sourceMappingURL=connection.service.js.map