diff --git a/docs-v2/reference/api/connection/get.mdx b/docs-v2/reference/api/connection/get.mdx index b287a76720d..a0331801f21 100644 --- a/docs-v2/reference/api/connection/get.mdx +++ b/docs-v2/reference/api/connection/get.mdx @@ -3,43 +3,9 @@ title: 'Get connection & credentials' openapi: 'GET /connection/{connectionId}' --- - - ```json Example Response -{ - "id": 18393, // Nango internal connection id - "created_at": "2023-03-08T09:43:03.725Z", // Creation timestamp - "updated_at": "2023-03-08T09:43:03.725Z", // Last updated timestamp (e.g. last token refresh) - "provider_config_key": "github", // - "connection_id": "1", // - "credentials": { - "type": "OAUTH2", // OAUTH2 or OAUTH1 - "access_token": "gho_tsXLG73f....", // The current access token (refreshed if needed) - "refresh_token": "gho_fjofu84u9....", // Refresh token (Only returned if the REFRESH_TOKEN boolean parameter is set to true and the refresh token is available) - "expires_at": "2024-03-08T09:43:03.725Z", // Expiration date of access token (only if refresh token is present, otherwise missing) - "raw": { // Raw token response from the OAuth provider: Contents vary! - "access_token": "gho_tsXLG73f....", - "refresh_token": "gho_fjofu84u9....", // Refresh token (Only returned if the REFRESH_TOKEN boolean parameter is set to true and the refresh token is available) - "token_type": "bearer", - "scope": "public_repo,user" - } - }, - "connection_config": { // Additional API Configuration, see OAuth guide - "subdomain": "myshop", - "realmId": "XXXXX", - "instance_id": "YYYYYYY" - }, - "account_id": 0, // ID of your Nango account (Nango Cloud only) - "metadata": { // Custom metadata stored by you - "myProperty": "yes", - "filter": "closed=true" - } -} - ``` - - -The response content depends on the API authentication type (OAuth 2, OAuth 1, API key, Basic auth). +The response content depends on the API authentication type (e.g: OAuth 2, OAuth 1, API key, etc.). If you do not want to deal with collecting & injecting credentials in requests for multiple authentication types, use the Proxy([step-by-step guide](/guides/proxy-requests-to-an-api)). diff --git a/docs-v2/reference/sdks/node.mdx b/docs-v2/reference/sdks/node.mdx index 24486ba266b..ff0cf12c1c7 100644 --- a/docs-v2/reference/sdks/node.mdx +++ b/docs-v2/reference/sdks/node.mdx @@ -463,7 +463,6 @@ We recommend not caching tokens for longer than 5 minutes to ensure they are fre "realmId": "XXXXX", "instance_id": "YYYYYYY" }, - "account_id": 0, "metadata": { "myProperty": "yes", "filter": "closed=true" @@ -939,7 +938,7 @@ await nango.startSync('', ['SYNC_NAME1', 'SYNC_NAME2'], ' - The connection ID. If ommitted, the sync will trigger for all relevant connections. + The connection ID. If omitted, the sync will trigger for all relevant connections. @@ -965,7 +964,7 @@ await nango.startSync('', ['SYNC_NAME1', 'SYNC_NAME2'], ' - The connection ID. If ommitted, the sync will pause for all relevant connections. + The connection ID. If omitted, the sync will pause for all relevant connections. @@ -1108,7 +1107,7 @@ await nango.triggerAction('', '', '' The name of the action to trigger. - + The necessary input for your action's `runAction` function. @@ -1171,7 +1170,7 @@ await nango.delete(config); // DELETE request Array of additional status codes to retry a request in addition to the 5xx, 429, ECONNRESET, ETIMEDOUT, and ECONNABORTED - The API base URL. Can be ommitted if the base URL is configured for this API in the [providers.yaml](https://nango.dev/providers.yaml). + The API base URL. Can be omitted if the base URL is configured for this API in the [providers.yaml](https://nango.dev/providers.yaml). Override the decompress option when making requests. Optional, defaults to false diff --git a/docs-v2/spec.yaml b/docs-v2/spec.yaml index 32a20c1cf9a..f7dd6590520 100644 --- a/docs-v2/spec.yaml +++ b/docs-v2/spec.yaml @@ -463,21 +463,14 @@ paths: errors: type: array items: - type: object - properties: - type: - type: string - example: "'auth' or 'sync'" - log_id: - type: string - example: 'VrnbtykXJFckCm3HP93t' + $ref: '#/components/schemas/ConnectionError' description: | List of connection errors. Ex: - Connection credentials are invalid (type=auth) - Last sync for the connection has failed (type=sync) end_user: nullable: true - $ref: '#/components/schemas/ConnectSessionInput/properties/end_user' + $ref: '#/components/schemas/ConnectionEndUser' post: description: Adds a connection for which you already have credentials. @@ -581,6 +574,10 @@ paths: responses: '200': description: Successfully returned a connection + content: + application/json: + schema: + $ref: '#/components/schemas/ConnectionFull' '400': description: Invalid request content: @@ -2180,6 +2177,8 @@ components: type: string organization: type: object + required: + - id properties: id: type: string @@ -2239,3 +2238,423 @@ components: type: string description: When the token expires format: date-time + + AllAuthCredentials: + anyOf: + - $ref: '#/components/schemas/OAuth1Credentials' + - $ref: '#/components/schemas/OAuth2Credentials' + - $ref: '#/components/schemas/BasicApiCredentials' + - $ref: '#/components/schemas/ApiKeyCredentials' + - $ref: '#/components/schemas/AppCredentials' + - $ref: '#/components/schemas/JwtCredentials' + - $ref: '#/components/schemas/OAuth2ClientCredentials' + - $ref: '#/components/schemas/AppStoreCredentials' + - $ref: '#/components/schemas/UnauthCredentials' + - $ref: '#/components/schemas/CustomCredentials' + - $ref: '#/components/schemas/TbaCredentials' + - $ref: '#/components/schemas/TableauCredentials' + - $ref: '#/components/schemas/BillCredentials' + - $ref: '#/components/schemas/TwoStepCredentials' + - $ref: '#/components/schemas/SignatureCredentials' + + ## ---------------------- Credentials + ApiKeyCredentials: + type: object + title: Api Key + additionalProperties: false + properties: + apiKey: + type: string + type: + type: string + enum: [API_KEY] + required: + - type + - apiKey + AppCredentials: + type: object + title: GitHub App + additionalProperties: false + properties: + access_token: + type: string + expires_at: + format: date-time + type: string + raw: + type: object + type: + type: string + enum: [APP] + required: + - type + - access_token + - raw + AppStoreCredentials: + type: object + title: App Store + additionalProperties: false + properties: + access_token: + type: string + expires_at: + type: string + format: date-time + private_key: + type: string + raw: + type: object + type: + type: string + enum: [APP_STORE] + required: + - access_token + - raw + - private_key + BasicApiCredentials: + type: object + title: Basic Auth + additionalProperties: false + properties: + password: + type: string + type: + enum: [BASIC] + type: string + username: + type: string + required: + - type + - username + - password + BillCredentials: + type: object + title: Bill + additionalProperties: false + properties: + dev_key: + type: string + expires_at: + type: string + format: date-time + organization_id: + type: string + password: + type: string + raw: + type: object + session_id: + type: string + type: + type: string + enum: [BILL] + user_id: + type: string + username: + type: string + required: + - dev_key + - organization_id + - password + - raw + - type + - username + CustomCredentials: + type: object + title: Custom + additionalProperties: false + properties: + raw: + type: object + type: + type: string + enum: [CUSTOM] + required: + - raw + - type + JwtCredentials: + type: object + title: JWT + additionalProperties: false + properties: + expires_at: + format: date-time + type: string + issuerId: + type: string + privateKey: + anyOf: + - additionalProperties: false + properties: + id: + type: string + secret: + type: string + required: + - id + - secret + type: object + - type: string + privateKeyId: + type: string + token: + type: string + type: + type: string + enum: [JWT] + required: + - type + - privateKey + OAuth1Credentials: + type: object + title: OAuth1 + additionalProperties: false + properties: + oauth_token: + type: string + oauth_token_secret: + type: string + raw: + type: object + type: + type: string + enum: [OAUTH1] + required: + - oauth_token + - oauth_token_secret + - raw + - type + OAuth2ClientCredentials: + type: object + title: OAuth2 Client + additionalProperties: false + properties: + client_id: + type: string + client_secret: + type: string + expires_at: + type: string + format: date-time + raw: + type: object + token: + type: string + type: + type: string + enum: [OAUTH2_CC] + required: + - client_id + - client_secret + - raw + - token + - type + OAuth2Credentials: + type: object + title: OAuth2 + additionalProperties: false + properties: + access_token: + type: string + config_override: + additionalProperties: false + properties: + client_id: + type: string + client_secret: + type: string + type: object + expires_at: + format: date-time + type: string + raw: + type: object + refresh_token: + type: string + type: + enum: [OAUTH2] + type: string + required: + - access_token + - raw + - type + SignatureCredentials: + type: object + title: Signature + additionalProperties: false + properties: + expires_at: + type: string + format: date-time + password: + type: string + token: + type: string + type: + type: string + enum: [SIGNATURE] + username: + type: string + required: + - type + - username + - password + TableauCredentials: + type: object + title: Tableau + additionalProperties: false + properties: + content_url: + type: string + expires_at: + type: string + format: date-time + pat_name: + type: string + pat_secret: + type: string + raw: + type: object + token: + type: string + type: + type: string + enum: [TABLEAU] + required: + - pat_name + - pat_secret + - raw + - type + TbaCredentials: + type: object + title: TBA + additionalProperties: false + properties: + config_override: + additionalProperties: false + properties: + client_id: + type: string + client_secret: + type: string + type: object + token_id: + type: string + token_secret: + type: string + type: + type: string + enum: [TBA] + required: + - type + - token_id + - token_secret + - config_override + TwoStepCredentials: + type: object + title: Two Step + additionalProperties: false + properties: + expires_at: + format: date-time + type: string + raw: + type: object + token: + type: string + type: + type: string + enum: [TWO_STEP] + required: + - raw + - type + UnauthCredentials: + type: object + title: Unauthenticated + additionalProperties: false + + ## ---------------------- Connection + ConnectionEndUser: + type: object + required: + - id + properties: + id: + type: string + description: Uniquely identifies the end user. + email: + nullable: true + type: string + display_name: + nullable: true + type: string + organization: + type: object + nullable: true + required: + - id + properties: + id: + type: string + description: Uniquely identifies the organization the end user belongs to + display_name: + type: string + nullable: true + ConnectionError: + type: object + required: + - type + - log_id + properties: + type: + type: string + enum: [auth, sync] + example: auth + log_id: + type: string + example: VrnbtykXJFckCm3HP93t + ConnectionFull: + type: object + required: + - id + - connection_id + - provider_config_key + - provider + - errors + - end_user + - metadata + - connection_config + - created_at + - updated_at + - last_fetched_at + - credentials + properties: + id: + type: integer + connection_id: + type: string + provider_config_key: + type: string + provider: + type: string + errors: + type: array + items: + $ref: '#/components/schemas/ConnectionError' + end_user: + nullable: true + $ref: '#/components/schemas/ConnectionEndUser' + metadata: + type: object + additionalProperties: true + connection_config: + type: object + additionalProperties: true + created_at: + type: string + updated_at: + type: string + last_fetched_at: + type: string + credentials: + $ref: '#/components/schemas/AllAuthCredentials' diff --git a/packages/cli/lib/services/__snapshots__/model.service.unit.test.ts.snap b/packages/cli/lib/services/__snapshots__/model.service.unit.test.ts.snap index b1098c9f0e2..e822cd4111a 100644 --- a/packages/cli/lib/services/__snapshots__/model.service.unit.test.ts.snap +++ b/packages/cli/lib/services/__snapshots__/model.service.unit.test.ts.snap @@ -45,7 +45,7 @@ import { Nango } from '@nangohq/node'; import type { AxiosInstance, AxiosInterceptorManager, AxiosRequestConfig, AxiosResponse } from 'axios'; import { AxiosError } from 'axios'; import type { SyncConfig } from '../models/Sync.js'; -import type { DBTeam, GetPublicIntegration, RunnerFlags } from '@nangohq/types'; +import type { ApiEndUser, DBTeam, GetPublicIntegration, RunnerFlags } from '@nangohq/types'; export declare const oldLevelToNewLevel: { readonly debug: "debug"; readonly info: "info"; @@ -245,18 +245,21 @@ interface MetadataChangeResponse { connection_id: string | string[]; } interface Connection { - id?: number; - created_at: Date; - updated_at: Date; + id: number; provider_config_key: string; connection_id: string; connection_config: Record; - environment_id: number; - metadata?: Metadata | null; - credentials_iv?: string | null; - credentials_tag?: string | null; + created_at: string; + updated_at: string; + last_fetched_at: string; + metadata: Record | null; + provider: string; + errors: { + type: string; + log_id: string; + }[]; + end_user: ApiEndUser | null; credentials: AuthCredentials; - end_user_id: number | null; } export declare class ActionError> extends Error { type: string; diff --git a/packages/node-client/lib/index.ts b/packages/node-client/lib/index.ts index 1d4ebb04da0..6572c4b0ba3 100644 --- a/packages/node-client/lib/index.ts +++ b/packages/node-client/lib/index.ts @@ -25,10 +25,10 @@ import type { TwoStepCredentials, GetPublicConnections, SignatureCredentials, - PostPublicConnectSessionsReconnect + PostPublicConnectSessionsReconnect, + GetPublicConnection } from '@nangohq/types'; import type { - Connection, CreateConnectionOAuth1, CreateConnectionOAuth2, Integration, @@ -308,7 +308,12 @@ export class Nango { * @param refreshToken - Optional. When set to true, this returns the refresh token as part of the response * @returns A promise that resolves with a connection object */ - public async getConnection(providerConfigKey: string, connectionId: string, forceRefresh?: boolean, refreshToken?: boolean): Promise { + public async getConnection( + providerConfigKey: string, + connectionId: string, + forceRefresh?: boolean, + refreshToken?: boolean + ): Promise { const response = await this.getConnectionDetails(providerConfigKey, connectionId, forceRefresh, refreshToken); return response.data; } @@ -960,7 +965,7 @@ export class Nango { forceRefresh: boolean = false, refreshToken: boolean = false, additionalHeader: Record = {} - ): Promise> { + ): Promise> { const url = `${this.serverUrl}/connection/${connectionId}`; const headers = { diff --git a/packages/node-client/lib/types.ts b/packages/node-client/lib/types.ts index 9a9dc681abc..54a2f94a30b 100644 --- a/packages/node-client/lib/types.ts +++ b/packages/node-client/lib/types.ts @@ -38,6 +38,7 @@ import type { GetPublicListIntegrationsLegacy, GetPublicIntegration, GetPublicConnections, + GetPublicConnection, PostConnectSessions, PostPublicConnectSessionsReconnect } from '@nangohq/types'; @@ -83,6 +84,7 @@ export type { GetPublicListIntegrationsLegacy, GetPublicIntegration, GetPublicConnections, + GetPublicConnection, PostConnectSessions, PostPublicConnectSessionsReconnect }; @@ -156,21 +158,6 @@ export interface MetadataChangeResponse { connection_id: string | string[]; } -export interface Connection { - id?: number; - end_user_id: number | null; - created_at: Date; - updated_at: Date; - provider_config_key: string; - connection_id: string; - connection_config: Record; - environment_id: number; - metadata?: Metadata | null; - credentials_iv?: string | null; - credentials_tag?: string | null; - credentials: AllAuthCredentials; -} - export interface IntegrationWithCreds extends Integration { client_id: string; client_secret: string; diff --git a/packages/server/lib/controllers/connection.controller.ts b/packages/server/lib/controllers/connection.controller.ts index 86c26f23f89..092f818d140 100644 --- a/packages/server/lib/controllers/connection.controller.ts +++ b/packages/server/lib/controllers/connection.controller.ts @@ -4,15 +4,9 @@ import db from '@nangohq/database'; import type { TbaCredentials, ApiKeyCredentials, BasicApiCredentials, ConnectionConfig, OAuth1Credentials, OAuth2ClientCredentials } from '@nangohq/types'; import { configService, connectionService, errorManager, NangoError, accountService, SlackService, getProvider } from '@nangohq/shared'; import { NANGO_ADMIN_UUID } from './account.controller.js'; -import { metrics } from '@nangohq/utils'; import { logContextGetter } from '@nangohq/logs'; import type { RequestLocals } from '../utils/express.js'; -import { - connectionCreated as connectionCreatedHook, - connectionCreationStartCapCheck as connectionCreationStartCapCheckHook, - connectionRefreshSuccess as connectionRefreshSuccessHook, - connectionRefreshFailed as connectionRefreshFailedHook -} from '../hooks/hooks.js'; +import { connectionCreated as connectionCreatedHook, connectionCreationStartCapCheck as connectionCreationStartCapCheckHook } from '../hooks/hooks.js'; import { getOrchestrator } from '../utils/utils.js'; import { preConnectionDeletion } from '../hooks/connection/on/connection-deleted.js'; @@ -21,83 +15,6 @@ export type { ConnectionList }; const orchestrator = getOrchestrator(); class ConnectionController { - /** - * CLI/SDK/API - */ - - async getConnectionCreds(req: Request, res: Response>, next: NextFunction) { - try { - const { environment, account } = res.locals; - const connectionId = req.params['connectionId'] as string; - const providerConfigKey = req.query['provider_config_key'] as string; - const returnRefreshToken = req.query['refresh_token'] === 'true'; - const instantRefresh = req.query['force_refresh'] === 'true'; - const isSync = (req.get('Nango-Is-Sync') as string) === 'true'; - - if (!providerConfigKey) { - res.status(400).send({ error: 'Missing providerConfigKey' }); - return; - } - - if (!isSync) { - metrics.increment(metrics.Types.GET_CONNECTION, 1, { accountId: account.id }); - } - - const integration = await configService.getProviderConfig(providerConfigKey, environment.id); - if (!integration) { - res.status(404).send({ - error: { - code: 'unknown_provider_config', - message: - 'Provider config not found for the given provider config key. Please make sure the provider config exists in the Nango dashboard.' - } - }); - return; - } - - const connectionRes = await connectionService.getConnection(connectionId, providerConfigKey, environment.id); - if (connectionRes.error || !connectionRes.response) { - errorManager.errResFromNangoErr(res, connectionRes.error); - return; - } - - const credentialResponse = await connectionService.refreshOrTestCredentials({ - account, - environment, - connection: connectionRes.response, - integration, - logContextGetter, - instantRefresh, - onRefreshSuccess: connectionRefreshSuccessHook, - onRefreshFailed: connectionRefreshFailedHook - }); - - if (credentialResponse.isErr()) { - errorManager.errResFromNangoErr(res, credentialResponse.error); - return; - } - - const { value: connection } = credentialResponse; - - if (connection && connection.credentials && connection.credentials.type === 'OAUTH2' && !returnRefreshToken) { - if (connection.credentials.refresh_token) { - delete connection.credentials.refresh_token; - } - - if (connection.credentials.raw && connection.credentials.raw['refresh_token']) { - const rawCreds = { ...connection.credentials.raw }; // Properties from 'raw' are not mutable so we need to create a new object. - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete rawCreds['refresh_token']; - connection.credentials.raw = rawCreds; - } - } - - res.status(200).send(connection); - } catch (err) { - next(err); - } - } - async deleteAdminConnection(req: Request, res: Response>, next: NextFunction) { try { const { environment, account: team } = res.locals; diff --git a/packages/server/lib/controllers/connection/connectionId/getConnection.integration.test.ts b/packages/server/lib/controllers/connection/connectionId/getConnection.integration.test.ts new file mode 100644 index 00000000000..e2613575263 --- /dev/null +++ b/packages/server/lib/controllers/connection/connectionId/getConnection.integration.test.ts @@ -0,0 +1,155 @@ +import { afterAll, beforeAll, describe, it, expect } from 'vitest'; +import { runServer, shouldBeProtected, isSuccess, isError } from '../../../utils/tests.js'; +import { seeders } from '@nangohq/shared'; + +let api: Awaited>; + +const endpoint = '/connection/:connectionId'; + +describe(`GET ${endpoint}`, () => { + beforeAll(async () => { + api = await runServer(); + }); + afterAll(() => { + api.server.close(); + }); + + it('should be protected', async () => { + const res = await api.fetch(endpoint, { + method: 'GET', + params: { connectionId: 'test' }, + query: { provider_config_key: 'github' } + }); + + shouldBeProtected(res); + }); + + it('should 404 on unknown provider', async () => { + const { env } = await seeders.seedAccountEnvAndUser(); + + const res = await api.fetch(endpoint, { + method: 'GET', + token: env.secret_key, + params: { connectionId: 'test' }, + query: { provider_config_key: 'github' } + }); + + isError(res.json); + expect(res.json).toStrictEqual({ + error: { code: 'unknown_provider_config', message: 'Provider does not exists' } + }); + }); + + it('should 404 on unknown connectionId', async () => { + const { env } = await seeders.seedAccountEnvAndUser(); + await seeders.createConfigSeed(env, 'github', 'github'); + + const res = await api.fetch(endpoint, { + method: 'GET', + token: env.secret_key, + params: { connectionId: 'test' }, + query: { provider_config_key: 'github' } + }); + + isError(res.json); + expect(res.json).toStrictEqual({ + error: { code: 'not_found', message: 'Failed to find connection' } + }); + }); + + it('should get a connection', async () => { + const { env, account } = await seeders.seedAccountEnvAndUser(); + await seeders.createConfigSeed(env, 'algolia', 'algolia'); + const endUser = await seeders.createEndUser({ environment: env, account }); + const conn = await seeders.createConnectionSeed(env, 'algolia', endUser, { + rawCredentials: { type: 'API_KEY', apiKey: 'test_api_key' }, + connectionConfig: { APP_ID: 'TEST' } + }); + + const res = await api.fetch(endpoint, { + method: 'GET', + token: env.secret_key, + params: { connectionId: conn.connection_id }, + query: { provider_config_key: 'algolia' } + }); + + isSuccess(res.json); + expect(res.json).toStrictEqual({ + connection_id: conn.connection_id, + created_at: expect.toBeIsoDateTimezone(), + credentials: { + apiKey: 'test_api_key', + type: 'API_KEY' + }, + connection_config: { APP_ID: 'TEST' }, + end_user: { + displayName: null, + email: endUser.email, + id: endUser.endUserId, + organization: { + displayName: null, + id: endUser.organization!.organizationId + } + }, + errors: [], + id: expect.any(Number), + last_fetched_at: expect.toBeIsoDateTimezone(), + metadata: null, + provider: 'algolia', + provider_config_key: 'algolia', + updated_at: expect.toBeIsoDateTimezone() + }); + }); + + it('should get a connection despite another connection with same name on a different provider', async () => { + const { env, account } = await seeders.seedAccountEnvAndUser(); + + await seeders.createConfigSeed(env, 'algolia', 'algolia'); + const endUser = await seeders.createEndUser({ environment: env, account }); + const conn = await seeders.createConnectionSeed(env, 'algolia', endUser, { + rawCredentials: { type: 'API_KEY', apiKey: 'test_api_key' }, + connectionConfig: { APP_ID: 'TEST' } + }); + + await seeders.createConfigSeed(env, 'google', 'google'); + await seeders.createConnectionSeed(env, 'google', endUser, { + connectionId: conn.connection_id, + rawCredentials: { type: 'API_KEY', apiKey: 'test_api_key' }, + connectionConfig: { APP_ID: 'TEST' } + }); + + const res = await api.fetch(endpoint, { + method: 'GET', + token: env.secret_key, + params: { connectionId: conn.connection_id }, + query: { provider_config_key: 'algolia' } + }); + + isSuccess(res.json); + expect(res.json).toStrictEqual({ + connection_id: conn.connection_id, + created_at: expect.toBeIsoDateTimezone(), + credentials: { + apiKey: 'test_api_key', + type: 'API_KEY' + }, + connection_config: { APP_ID: 'TEST' }, + end_user: { + displayName: null, + email: endUser.email, + id: endUser.endUserId, + organization: { + displayName: null, + id: endUser.organization!.organizationId + } + }, + errors: [], + id: expect.any(Number), + last_fetched_at: expect.toBeIsoDateTimezone(), + metadata: null, + provider: 'algolia', + provider_config_key: 'algolia', + updated_at: expect.toBeIsoDateTimezone() + }); + }); +}); diff --git a/packages/server/lib/controllers/connection/connectionId/getConnection.ts b/packages/server/lib/controllers/connection/connectionId/getConnection.ts new file mode 100644 index 00000000000..c9a5d63af75 --- /dev/null +++ b/packages/server/lib/controllers/connection/connectionId/getConnection.ts @@ -0,0 +1,111 @@ +import { z } from 'zod'; +import { asyncWrapper } from '../../../utils/asyncWrapper.js'; +import { metrics, zodErrorToHTTP } from '@nangohq/utils'; +import type { GetPublicConnection } from '@nangohq/types'; +import { connectionService, configService } from '@nangohq/shared'; +import { connectionRefreshFailed as connectionRefreshFailedHook, connectionRefreshSuccess as connectionRefreshSuccessHook } from '../../../hooks/hooks.js'; +import { logContextGetter } from '@nangohq/logs'; +import { connectionIdSchema, providerConfigKeySchema, stringBool } from '../../../helpers/validation.js'; +import { connectionFullToPublicApi } from '../../../formatters/connection.js'; + +const queryStringValidation = z + .object({ + provider_config_key: providerConfigKeySchema, + refresh_token: stringBool.optional(), + force_refresh: stringBool.optional() + }) + .strict(); + +const paramValidation = z + .object({ + connectionId: connectionIdSchema + }) + .strict(); + +export const getPublicConnection = asyncWrapper(async (req, res) => { + const queryParamValues = queryStringValidation.safeParse(req.query); + if (!queryParamValues.success) { + res.status(400).send({ error: { code: 'invalid_query_params', errors: zodErrorToHTTP(queryParamValues.error) } }); + return; + } + + const paramValue = paramValidation.safeParse(req.params); + if (!paramValue.success) { + res.status(400).send({ error: { code: 'invalid_uri_params', errors: zodErrorToHTTP(paramValue.error) } }); + return; + } + + const { environment, account } = res.locals; + + const queryParams: GetPublicConnection['Querystring'] = queryParamValues.data; + const params: GetPublicConnection['Params'] = paramValue.data; + + const { provider_config_key: providerConfigKey, force_refresh: instantRefresh, refresh_token: returnRefreshToken } = queryParams; + const { connectionId } = params; + + const isSync = req.headers['Nango-Is-Sync'] === 'true'; + + if (!isSync) { + metrics.increment(metrics.Types.GET_CONNECTION, 1, { accountId: account.id }); + } + + const integration = await configService.getProviderConfig(providerConfigKey, environment.id); + if (!integration) { + res.status(400).send({ error: { code: 'unknown_provider_config', message: 'Provider does not exists' } }); + return; + } + + const connectionRes = await connectionService.getConnection(connectionId, providerConfigKey, environment.id); + if (connectionRes.error || !connectionRes.response) { + res.status(404).send({ error: { code: 'not_found', message: 'Failed to find connection' } }); + return; + } + + const credentialResponse = await connectionService.refreshOrTestCredentials({ + account, + environment, + connection: connectionRes.response, + integration, + logContextGetter, + instantRefresh: instantRefresh ?? false, + onRefreshSuccess: connectionRefreshSuccessHook, + onRefreshFailed: connectionRefreshFailedHook + }); + if (credentialResponse.isErr()) { + res.status(500).send({ error: { code: 'server_error', message: 'Failed to refresh or test credentials' } }); + return; + } + + const { value: connection } = credentialResponse; + + if (connection && connection.credentials && connection.credentials.type === 'OAUTH2' && !returnRefreshToken) { + if (connection.credentials.refresh_token) { + delete connection.credentials.refresh_token; + } + + if (connection.credentials.raw && connection.credentials.raw['refresh_token']) { + const rawCreds = { ...connection.credentials.raw }; // Properties from 'raw' are not mutable so we need to create a new object. + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + + const { refresh_token, ...rest } = rawCreds; + connection.credentials.raw = rest; + } + } + + // We get connection one last time to get endUser, errors + // This is very unoptimized unfortunately + const finalConnections = await connectionService.listConnections({ environmentId: environment.id, connectionId, integrationIds: [providerConfigKey] }); + if (finalConnections.length !== 1 || !finalConnections[0]) { + res.status(500).send({ error: { code: 'server_error', message: 'Failed to get connection' } }); + return; + } + + res.status(200).send( + connectionFullToPublicApi({ + data: { ...finalConnections[0].connection, credentials: connection.credentials }, + activeLog: finalConnections[0].active_logs, + endUser: finalConnections[0].end_user, + provider: finalConnections[0].provider + }) + ); +}); diff --git a/packages/server/lib/controllers/connection/getConnections.ts b/packages/server/lib/controllers/connection/getConnections.ts index 0b4892b5804..03e41cccad0 100644 --- a/packages/server/lib/controllers/connection/getConnections.ts +++ b/packages/server/lib/controllers/connection/getConnections.ts @@ -2,7 +2,7 @@ import { asyncWrapper } from '../../utils/asyncWrapper.js'; import { zodErrorToHTTP } from '@nangohq/utils'; import type { GetPublicConnections } from '@nangohq/types'; import { AnalyticsTypes, analytics, connectionService } from '@nangohq/shared'; -import { connectionToPublicApi } from '../../formatters/connection.js'; +import { connectionSimpleToPublicApi } from '../../formatters/connection.js'; import { z } from 'zod'; import { bodySchema } from '../connect/postSessions.js'; @@ -40,7 +40,7 @@ export const getPublicConnections = asyncWrapper(async (re res.status(200).send({ connections: connections.map((data) => { // TODO: return end_user - return connectionToPublicApi({ + return connectionSimpleToPublicApi({ data: data.connection, activeLog: data.active_logs, provider: data.provider, diff --git a/packages/server/lib/controllers/v1/connections/connectionId/getConnection.ts b/packages/server/lib/controllers/v1/connections/connectionId/getConnection.ts index 4e7bb7faec2..055fb05bd36 100644 --- a/packages/server/lib/controllers/v1/connections/connectionId/getConnection.ts +++ b/packages/server/lib/controllers/v1/connections/connectionId/getConnection.ts @@ -47,8 +47,8 @@ export const getConnection = asyncWrapper(async (req, res) => { const { environment, account } = res.locals; - const queryParams = queryParamValues.data; - const params = paramValue.data; + const queryParams: GetConnection['Querystring'] = queryParamValues.data; + const params: GetConnection['Params'] = paramValue.data; const { provider_config_key: providerConfigKey } = queryParams; const { connectionId } = params; diff --git a/packages/server/lib/controllers/v1/connections/getConnections.ts b/packages/server/lib/controllers/v1/connections/getConnections.ts index 9bff957c22b..746bba1acbc 100644 --- a/packages/server/lib/controllers/v1/connections/getConnections.ts +++ b/packages/server/lib/controllers/v1/connections/getConnections.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { asyncWrapper } from '../../../utils/asyncWrapper.js'; import { zodErrorToHTTP } from '@nangohq/utils'; import type { GetConnections } from '@nangohq/types'; -import { envSchema, providerConfigKeySchema } from '../../../helpers/validation.js'; +import { envSchema, providerConfigKeySchema, stringBool } from '../../../helpers/validation.js'; import { connectionService } from '@nangohq/shared'; import { connectionSimpleToApi } from '../../../formatters/connection.js'; @@ -15,12 +15,7 @@ const queryStringValidation = z .pipe(z.array(providerConfigKeySchema)) .optional(), search: z.string().max(255).optional(), - withError: z - .enum(['true', 'false', '']) - .optional() - .default('false') - .transform((value) => value === 'true') - .optional(), + withError: stringBool.optional(), env: envSchema, page: z.coerce.number().min(0).max(50).optional() }) diff --git a/packages/server/lib/formatters/connection.ts b/packages/server/lib/formatters/connection.ts index e420953c0bc..d2590c77148 100644 --- a/packages/server/lib/formatters/connection.ts +++ b/packages/server/lib/formatters/connection.ts @@ -1,4 +1,12 @@ -import type { ApiConnectionFull, ApiConnectionSimple, ApiPublicConnection, DBConnection, DBEndUser } from '@nangohq/types'; +import type { + AllAuthCredentials, + ApiConnectionFull, + ApiConnectionSimple, + ApiPublicConnection, + ApiPublicConnectionFull, + DBConnection, + DBEndUser +} from '@nangohq/types'; export function connectionSimpleToApi({ data, @@ -39,7 +47,7 @@ export function connectionFullToApi(connection: DBConnection): ApiConnectionFull }; } -export function connectionToPublicApi({ +export function connectionSimpleToPublicApi({ data, provider, activeLog, @@ -68,3 +76,37 @@ export function connectionToPublicApi({ created: String(data.created_at) }; } + +export function connectionFullToPublicApi({ + data, + provider, + activeLog, + endUser +}: { + data: DBConnection; + provider: string; + activeLog: [{ type: string; log_id: string }]; + endUser: DBEndUser | null; +}): ApiPublicConnectionFull { + return { + id: data.id, + connection_id: data.connection_id, + provider_config_key: data.provider_config_key, + provider, + errors: activeLog, + end_user: endUser + ? { + id: endUser.end_user_id, + displayName: endUser.display_name || null, + email: endUser.email, + organization: endUser.organization_id ? { id: endUser.organization_id, displayName: endUser.organization_display_name || null } : null + } + : null, + metadata: data.metadata || null, + connection_config: data.connection_config || {}, + created_at: String(data.created_at), + updated_at: String(data.updated_at), + last_fetched_at: String(data.last_fetched_at), + credentials: data.credentials as AllAuthCredentials + }; +} diff --git a/packages/server/lib/helpers/validation.ts b/packages/server/lib/helpers/validation.ts index 91e2426fbcd..c17d17c5b62 100644 --- a/packages/server/lib/helpers/validation.ts +++ b/packages/server/lib/helpers/validation.ts @@ -31,3 +31,9 @@ export const connectionCredential = z.union([ z.object({ public_key: z.string().uuid(), hmac: z.string().optional() }), z.object({ connect_session_token: connectSessionTokenSchema }) ]); + +export const stringBool = z + .enum(['true', 'false', '']) + .optional() + .default('false') + .transform((value) => value === 'true'); diff --git a/packages/server/lib/routes.ts b/packages/server/lib/routes.ts index 8bc4e922b1a..333b11c20dc 100644 --- a/packages/server/lib/routes.ts +++ b/packages/server/lib/routes.ts @@ -108,6 +108,7 @@ import { postPublicApiKeyAuthorization } from './controllers/auth/postApiKey.js' import { postPublicBasicAuthorization } from './controllers/auth/postBasic.js'; import { postPublicAppStoreAuthorization } from './controllers/auth/postAppStore.js'; import { postRollout } from './controllers/fleet/postRollout.js'; +import { getPublicConnection } from './controllers/connection/connectionId/getConnection.js'; import { postWebhook } from './controllers/webhook/environmentUuid/postWebhook.js'; export const router = express.Router(); @@ -218,7 +219,7 @@ publicAPI.route('/config/:providerConfigKey').delete(apiAuth, deletePublicIntegr publicAPI.route('/integrations').get(connectSessionOrApiAuth, getPublicListIntegrations); publicAPI.route('/integrations/:uniqueKey').get(apiAuth, getPublicIntegration); -publicAPI.route('/connection/:connectionId').get(apiAuth, connectionController.getConnectionCreds.bind(connectionController)); +publicAPI.route('/connection/:connectionId').get(apiAuth, getPublicConnection); publicAPI.route('/connection').get(apiAuth, getPublicConnections); publicAPI.route('/connection/:connectionId').delete(apiAuth, deletePublicConnection); publicAPI.route('/connection/:connectionId/metadata').post(apiAuth, connectionController.setMetadataLegacy.bind(connectionController)); diff --git a/packages/shared/lib/clients/orchestrator.ts b/packages/shared/lib/clients/orchestrator.ts index 88726470dd6..ad15935bc2c 100644 --- a/packages/shared/lib/clients/orchestrator.ts +++ b/packages/shared/lib/clients/orchestrator.ts @@ -167,7 +167,7 @@ export class Orchestrator { action: actionName, connection: connection.connection_id, integration: connection.provider_config_key, - truncated_response: JSON.stringify(res.value, null, 2)?.slice(0, 100) + truncated_response: JSON.stringify(res.value)?.slice(0, 100) }); await telemetry.log( @@ -175,7 +175,7 @@ export class Orchestrator { content, LogActionEnum.ACTION, { - input: JSON.stringify(input, null, 2), + input: JSON.stringify(input), environmentId: String(connection.environment_id), connectionId: connection.connection_id, providerConfigKey: connection.provider_config_key, @@ -220,7 +220,7 @@ export class Orchestrator { LogActionEnum.ACTION, { error: stringifyError(err), - input: JSON.stringify(input, null, 2), + input: JSON.stringify(input), environmentId: String(connection.environment_id), connectionId: connection.connection_id, providerConfigKey: connection.provider_config_key, diff --git a/packages/shared/lib/models/Proxy.ts b/packages/shared/lib/models/Proxy.ts index d09bb3a33ae..4e5e88544ac 100644 --- a/packages/shared/lib/models/Proxy.ts +++ b/packages/shared/lib/models/Proxy.ts @@ -61,14 +61,14 @@ export interface ApplicationConstructedProxyConfiguration extends BaseProxyConfi | TwoStepCredentials | SignatureCredentials; provider: Provider; - connection: Connection; + connection: Pick; } export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'; export interface InternalProxyConfiguration { providerName: string; - connection: Connection; + connection: Pick; existingActivityLogId?: string | null | undefined; } diff --git a/packages/shared/lib/sdk/sync.ts b/packages/shared/lib/sdk/sync.ts index 79fca94b913..4079f400b86 100644 --- a/packages/shared/lib/sdk/sync.ts +++ b/packages/shared/lib/sdk/sync.ts @@ -21,7 +21,7 @@ import type { SyncConfig } from '../models/Sync.js'; import type { ValidateDataError } from './dataValidation.js'; import { validateData } from './dataValidation.js'; import { NangoError } from '../utils/error.js'; -import type { DBTeam, GetPublicIntegration, MessageRowInsert, RunnerFlags } from '@nangohq/types'; +import type { ApiEndUser, DBTeam, GetPublicIntegration, MessageRowInsert, RunnerFlags } from '@nangohq/types'; import { getProvider } from '../services/providers.js'; import { redactHeaders, redactURL } from '../utils/http.js'; @@ -312,18 +312,18 @@ interface MetadataChangeResponse { } interface Connection { - id?: number; - created_at: Date; - updated_at: Date; + id: number; provider_config_key: string; connection_id: string; connection_config: Record; - environment_id: number; - metadata?: Metadata | null; - credentials_iv?: string | null; - credentials_tag?: string | null; + created_at: string; + updated_at: string; + last_fetched_at: string; + metadata: Record | null; + provider: string; + errors: { type: string; log_id: string }[]; + end_user: ApiEndUser | null; credentials: AuthCredentials; - end_user_id: number | null; } export class ActionError> extends Error { diff --git a/packages/shared/lib/seeders/connection.seeder.ts b/packages/shared/lib/seeders/connection.seeder.ts index 5e8cc45e16e..c91d812d80c 100644 --- a/packages/shared/lib/seeders/connection.seeder.ts +++ b/packages/shared/lib/seeders/connection.seeder.ts @@ -24,14 +24,23 @@ export const createConnectionSeeds = async (env: DBEnvironment): Promise => { - const name = Math.random().toString(36).substring(7); +export const createConnectionSeed = async ( + env: DBEnvironment, + provider: string, + endUser?: EndUser, + rest?: { + connectionId?: string; + rawCredentials?: AuthCredentials; + connectionConfig?: any; + } +): Promise => { + const name = rest?.connectionId ? rest.connectionId : Math.random().toString(36).substring(7); const result = await connectionService.upsertConnection({ connectionId: name, providerConfigKey: provider, provider: provider, - parsedRawCredentials: {} as AuthCredentials, - connectionConfig: {}, + parsedRawCredentials: rest?.rawCredentials || ({} as AuthCredentials), + connectionConfig: rest?.connectionConfig || {}, environmentId: env.id, accountId: 0 }); diff --git a/packages/shared/lib/services/connection.service.ts b/packages/shared/lib/services/connection.service.ts index 8284f7c3425..9dce601fc0b 100644 --- a/packages/shared/lib/services/connection.service.ts +++ b/packages/shared/lib/services/connection.service.ts @@ -1,7 +1,7 @@ import jwt from 'jsonwebtoken'; import { XMLBuilder, XMLParser } from 'fast-xml-parser'; import type { Knex } from '@nangohq/database'; -import db, { schema, dbNamespace } from '@nangohq/database'; +import db, { dbNamespace } from '@nangohq/database'; import analytics, { AnalyticsTypes } from '../utils/analytics.js'; import type { Config as ProviderConfig, AuthCredentials, OAuth1Credentials, Config } from '../models/index.js'; import { LogActionEnum } from '../models/Telemetry.js'; @@ -384,49 +384,15 @@ class ConnectionService { } public async getConnection(connectionId: string, providerConfigKey: string, environment_id: number): Promise> { - 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: StoredConnection[] | null = (await schema() - .select('*') - .from(`_nango_connections`) - .where({ connection_id: connectionId, provider_config_key: providerConfigKey, environment_id, deleted: false })) as unknown as StoredConnection[]; - - const storedConnection = result == null || result.length == 0 ? null : result[0] || null; - - if (!storedConnection) { - const environmentName = await environmentService.getEnvironmentName(environment_id); + const rawConnection = await db.knex + .from(`_nango_connections`) + .select('*') + .where({ connection_id: connectionId, provider_config_key: providerConfigKey, environment_id, deleted: false }) + .limit(1) + .first(); - const error = new NangoError('unknown_connection', { connectionId, providerConfigKey, environmentName }); + if (!rawConnection) { + const error = new NangoError('unknown_connection', { connectionId, providerConfigKey }); await telemetry.log(LogTypes.GET_CONNECTION_FAILURE, error.message, LogActionEnum.AUTH, { environmentId: String(environment_id), @@ -438,67 +404,14 @@ class ConnectionService { return { success: false, error, response: null }; } - const connection = encryptionManager.decryptConnection(storedConnection); + const connection = encryptionManager.decryptConnection(rawConnection)!; // Parse the token expiration date. - if (connection != null) { - const credentials = connection.credentials as - | OAuth1Credentials - | OAuth2Credentials - | AppCredentials - | OAuth2ClientCredentials - | TableauCredentials - | JwtCredentials - | TwoStepCredentials - | BillCredentials - | SignatureCredentials; - if (credentials.type && credentials.type === 'OAUTH2') { - const creds = credentials; - creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined; - connection.credentials = creds; - } - - if (credentials.type && credentials.type === 'APP') { - const creds = credentials; - creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined; - connection.credentials = creds; - } - - if (credentials.type && credentials.type === 'OAUTH2_CC') { - const creds = credentials; - creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined; - connection.credentials = creds; - } - - if (credentials.type && credentials.type === 'TABLEAU') { - const creds = credentials; - creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined; - connection.credentials = creds; - } - - if (credentials.type && credentials.type === 'JWT') { - const creds = credentials; - creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined; - connection.credentials = creds; - } - - if (credentials.type && credentials.type === 'BILL') { - const creds = credentials; - creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined; - connection.credentials = creds; - } - - if (credentials.type && credentials.type === 'SIGNATURE') { - const creds = credentials; - creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined; - connection.credentials = creds; - } - - if (credentials.type && credentials.type === 'TWO_STEP') { - const creds = credentials; - creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined; - connection.credentials = creds; - } + const credentials = connection.credentials; + if (credentials.type && 'expires_at' in credentials) { + const creds = credentials; + creds.expires_at = creds.expires_at != null ? parseTokenExpirationDate(creds.expires_at) : undefined; + connection.credentials = creds; } return { success: true, error: null, response: connection }; diff --git a/packages/shared/lib/services/proxy.service.unit.test.ts b/packages/shared/lib/services/proxy.service.unit.test.ts index 893a7ae39e6..875d8390b3b 100644 --- a/packages/shared/lib/services/proxy.service.unit.test.ts +++ b/packages/shared/lib/services/proxy.service.unit.test.ts @@ -474,9 +474,7 @@ describe('Proxy service Construct URL Tests', () => { } }, token: { apiKey: 'sweet-secret-token' }, - connection: { - environment_id: 1 - } + connection: {} }); const url = proxyService.constructUrl(config); @@ -659,14 +657,9 @@ describe('Proxy service configure', () => { const internalConfig: InternalProxyConfiguration = { providerName: 'provider-1', connection: { - environment_id: 1, - end_user_id: null, connection_id: 'connection-1', - provider_config_key: 'provider-config-key-1', credentials: {} as OAuth2Credentials, - connection_config: {}, - created_at: new Date(), - updated_at: new Date() + connection_config: {} }, existingActivityLogId: '1' }; @@ -694,14 +687,9 @@ describe('Proxy service configure', () => { const internalConfig: InternalProxyConfiguration = { providerName: 'provider-1', connection: { - environment_id: 1, - end_user_id: null, connection_id: 'connection-1', - provider_config_key: 'provider-config-key-1', credentials: {} as OAuth2Credentials, - connection_config: {}, - created_at: new Date(), - updated_at: new Date() + connection_config: {} }, existingActivityLogId: '1' }; @@ -730,14 +718,9 @@ describe('Proxy service configure', () => { const internalConfig: InternalProxyConfiguration = { providerName: 'provider-1', connection: { - environment_id: 1, - end_user_id: null, connection_id: 'connection-1', - provider_config_key: 'provider-config-key-1', credentials: {} as OAuth2Credentials, - connection_config: {}, - created_at: new Date(), - updated_at: new Date() + connection_config: {} }, existingActivityLogId: '1' }; @@ -766,14 +749,9 @@ describe('Proxy service configure', () => { const internalConfig: InternalProxyConfiguration = { providerName: 'unknown', connection: { - environment_id: 1, - end_user_id: null, connection_id: 'connection-1', - provider_config_key: 'provider-config-key-1', credentials: {} as OAuth2Credentials, - connection_config: {}, - created_at: new Date(), - updated_at: new Date() + connection_config: {} }, existingActivityLogId: '1' }; @@ -808,14 +786,9 @@ describe('Proxy service configure', () => { const internalConfig: InternalProxyConfiguration = { providerName: 'github', connection: { - environment_id: 1, - end_user_id: null, connection_id: 'connection-1', - provider_config_key: 'provider-config-key-1', credentials: {} as OAuth2Credentials, - connection_config: {}, - created_at: new Date(), - updated_at: new Date() + connection_config: {} }, existingActivityLogId: '1' }; @@ -844,9 +817,7 @@ describe('Proxy service configure', () => { baseUrlOverride: 'https://api.github.com.override', decompress: false, connection: { - environment_id: 1, connection_id: 'connection-1', - provider_config_key: 'provider-config-key-1', credentials: {}, connection_config: {} }, diff --git a/packages/shared/lib/utils/utils.ts b/packages/shared/lib/utils/utils.ts index 16c2faec2ea..37394facb44 100644 --- a/packages/shared/lib/utils/utils.ts +++ b/packages/shared/lib/utils/utils.ts @@ -224,7 +224,7 @@ export function extractValueByPath(obj: Record, path: string): any return get(obj, path); } -export function connectionCopyWithParsedConnectionConfig(connection: Connection) { +export function connectionCopyWithParsedConnectionConfig(connection: Pick) { const connectionCopy = Object.assign({}, connection); const rawConfig: Record = connectionCopy.connection_config; diff --git a/packages/types/lib/api.endpoints.ts b/packages/types/lib/api.endpoints.ts index f6b22551fde..b1940d96eeb 100644 --- a/packages/types/lib/api.endpoints.ts +++ b/packages/types/lib/api.endpoints.ts @@ -52,6 +52,7 @@ import type { GetConnection, GetConnections, GetConnectionsCount, + GetPublicConnection, GetPublicConnections, PostConnectionRefresh } from './connection/api/get'; @@ -80,6 +81,7 @@ export type PublicApiEndpoints = | PostConnectSessions | PostPublicConnectSessionsReconnect | GetPublicConnections + | GetPublicConnection | GetConnectSession | DeleteConnectSession | PostDeployInternal diff --git a/packages/types/lib/connection/api/get.ts b/packages/types/lib/connection/api/get.ts index 931a7214a10..77e0c10fde0 100644 --- a/packages/types/lib/connection/api/get.ts +++ b/packages/types/lib/connection/api/get.ts @@ -3,6 +3,7 @@ import type { DBConnection } from '../db.js'; import type { ActiveLog } from '../../notification/active-logs/db.js'; import type { Merge } from 'type-fest'; import type { ApiEndUser } from '../../endUser/index.js'; +import type { AllAuthCredentials } from '../../auth/api.js'; export type ApiConnectionSimple = Pick, 'id' | 'connection_id' | 'provider_config_key' | 'created_at' | 'updated_at'> & { provider: string; @@ -77,6 +78,32 @@ export type GetConnection = Endpoint<{ }; }; }>; + +export type ApiPublicConnectionFull = Pick & { + created_at: string; + updated_at: string; + last_fetched_at: string; + metadata: Record | null; + provider: string; + errors: { type: string; log_id: string }[]; + end_user: ApiEndUser | null; + credentials: AllAuthCredentials; +}; +export type GetPublicConnection = Endpoint<{ + Method: 'GET'; + Params: { + connectionId: string; + }; + Querystring: { + provider_config_key: string; + refresh_token?: boolean | undefined; + force_refresh?: boolean | undefined; + }; + Path: '/connection/:connectionId'; + Error: ApiError<'unknown_provider_config'>; + Success: ApiPublicConnectionFull; +}>; + export type PostConnectionRefresh = Endpoint<{ Method: 'POST'; Params: {