diff --git a/src/core/resources/connector/api.ts b/src/core/resources/connector/api.ts new file mode 100644 index 0000000..9de86e2 --- /dev/null +++ b/src/core/resources/connector/api.ts @@ -0,0 +1,128 @@ +import type { KyResponse } from "ky"; +import { getAppClient } from "@/core/clients/index.js"; +import { ApiError, SchemaValidationError } from "@/core/errors.js"; +import type { + IntegrationType, + ListConnectorsResponse, + OAuthStatusResponse, + RemoveConnectorResponse, + SyncConnectorResponse, +} from "./schema.js"; +import { + ListConnectorsResponseSchema, + OAuthStatusResponseSchema, + RemoveConnectorResponseSchema, + SyncConnectorResponseSchema, +} from "./schema.js"; + +/** + * List all connectors for the current app. + * GET /api/apps/{app_id}/external-auth/list + */ +export async function listConnectors(): Promise { + const appClient = getAppClient(); + + let response: KyResponse; + try { + response = await appClient.get("external-auth/list"); + } catch (error) { + throw await ApiError.fromHttpError(error, "listing connectors"); + } + + const result = ListConnectorsResponseSchema.safeParse(await response.json()); + + if (!result.success) { + throw new SchemaValidationError( + "Invalid response from server", + result.error + ); + } + + return result.data; +} + +export async function syncConnector( + integrationType: IntegrationType, + scopes: string[] +): Promise { + const appClient = getAppClient(); + + let response: KyResponse; + try { + response = await appClient.post("external-auth/sync", { + json: { + integration_type: integrationType, + scopes, + }, + }); + } catch (error) { + throw await ApiError.fromHttpError(error, "syncing connector"); + } + + const result = SyncConnectorResponseSchema.safeParse(await response.json()); + + if (!result.success) { + throw new SchemaValidationError( + "Invalid response from server", + result.error + ); + } + + return result.data; +} + +export async function getOAuthStatus( + integrationType: IntegrationType, + connectionId: string +): Promise { + const appClient = getAppClient(); + + let response: KyResponse; + try { + response = await appClient.get("external-auth/status", { + searchParams: { + integration_type: integrationType, + connection_id: connectionId, + }, + }); + } catch (error) { + throw await ApiError.fromHttpError(error, "checking OAuth status"); + } + + const result = OAuthStatusResponseSchema.safeParse(await response.json()); + + if (!result.success) { + throw new SchemaValidationError( + "Invalid response from server", + result.error + ); + } + + return result.data; +} + +export async function removeConnector( + integrationType: IntegrationType +): Promise { + const appClient = getAppClient(); + + let response: KyResponse; + try { + response = await appClient.delete( + `external-auth/integrations/${integrationType}/remove` + ); + } catch (error) { + throw await ApiError.fromHttpError(error, "removing connector"); + } + + const result = RemoveConnectorResponseSchema.safeParse(await response.json()); + + if (!result.success) { + throw new SchemaValidationError( + "Invalid response from server", + result.error + ); + } + + return result.data; +} diff --git a/src/core/resources/connector/index.ts b/src/core/resources/connector/index.ts index 6841262..8182de9 100644 --- a/src/core/resources/connector/index.ts +++ b/src/core/resources/connector/index.ts @@ -1,3 +1,4 @@ +export * from "./api.js"; export * from "./config.js"; export * from "./resource.js"; export * from "./schema.js"; diff --git a/src/core/resources/connector/resource.ts b/src/core/resources/connector/resource.ts index 2a12c6f..3fb5bce 100644 --- a/src/core/resources/connector/resource.ts +++ b/src/core/resources/connector/resource.ts @@ -2,15 +2,9 @@ import type { Resource } from "../types.js"; import { readAllConnectors } from "./config.js"; import type { ConnectorResource } from "./schema.js"; -/** - * Connector resource implementation. - * Note: Connectors are push-only (no pull support). - * The push function will be implemented when the OAuth flow is ready. - */ export const connectorResource: Resource = { readAll: readAllConnectors, push: async () => { - // Push will be implemented in the OAuth flow task throw new Error("Connector push not yet implemented"); }, }; diff --git a/src/core/resources/connector/schema.ts b/src/core/resources/connector/schema.ts index 6daf9b7..c47f16a 100644 --- a/src/core/resources/connector/schema.ts +++ b/src/core/resources/connector/schema.ts @@ -1,9 +1,5 @@ import { z } from "zod"; -// ─── CONNECTOR SCHEMAS PER INTEGRATION ──────────────────────────────────────── -// Each integration has a literal type discriminator. -// Scopes are provider-specific - see official docs for available scopes. - /** Google Calendar - Scopes: https://developers.google.com/identity/protocols/oauth2/scopes#calendar */ export const GoogleCalendarConnectorSchema = z.object({ type: z.literal("googlecalendar"), @@ -76,12 +72,6 @@ export const TikTokConnectorSchema = z.object({ scopes: z.array(z.string()).default([]), }); -// ─── DISCRIMINATED UNION ────────────────────────────────────────────────────── - -/** - * Local connector resource schema using discriminated union. - * Each integration type has its own schema with a literal type discriminator. - */ export const ConnectorResourceSchema = z.discriminatedUnion("type", [ GoogleCalendarConnectorSchema, GoogleDriveConnectorSchema, @@ -99,9 +89,6 @@ export const ConnectorResourceSchema = z.discriminatedUnion("type", [ export type ConnectorResource = z.infer; -/** - * Supported OAuth integration types. - */ export const IntegrationTypeSchema = z.enum([ "googlecalendar", "googledrive", @@ -119,11 +106,6 @@ export const IntegrationTypeSchema = z.enum([ export type IntegrationType = z.infer; -// ─── API RESPONSE SCHEMAS ───────────────────────────────────────────────────── - -/** - * Connector status from upstream API. - */ export const ConnectorStatusSchema = z.enum([ "ACTIVE", "DISCONNECTED", @@ -132,9 +114,6 @@ export const ConnectorStatusSchema = z.enum([ export type ConnectorStatus = z.infer; -/** - * Upstream connector from the list API. - */ export const UpstreamConnectorSchema = z.object({ integration_type: IntegrationTypeSchema, status: ConnectorStatusSchema, @@ -144,9 +123,6 @@ export const UpstreamConnectorSchema = z.object({ export type UpstreamConnector = z.infer; -/** - * Response from GET /api/apps/{app_id}/external-auth/list - */ export const ListConnectorsResponseSchema = z.object({ integrations: z.array(UpstreamConnectorSchema), }); @@ -155,14 +131,32 @@ export type ListConnectorsResponse = z.infer< typeof ListConnectorsResponseSchema >; -/** - * Response from GET /api/external-auth/auto-added-scopes - */ -export const AutoAddedScopesResponseSchema = z.record( - IntegrationTypeSchema, - z.array(z.string()) -); +export const SyncConnectorResponseSchema = z.object({ + redirect_url: z.string().nullable(), + connection_id: z.string().nullable(), + already_authorized: z.boolean(), + error: z.literal("different_user").optional(), + error_message: z.string().optional(), + other_user_email: z.string().optional(), +}); + +export type SyncConnectorResponse = z.infer; + +export const OAuthPollingStatusSchema = z.enum(["ACTIVE", "FAILED", "PENDING"]); + +export type OAuthPollingStatus = z.infer; + +export const OAuthStatusResponseSchema = z.object({ + status: OAuthPollingStatusSchema, +}); + +export type OAuthStatusResponse = z.infer; + +export const RemoveConnectorResponseSchema = z.object({ + status: z.literal("removed"), + integration_type: IntegrationTypeSchema, +}); -export type AutoAddedScopesResponse = z.infer< - typeof AutoAddedScopesResponseSchema +export type RemoveConnectorResponse = z.infer< + typeof RemoveConnectorResponseSchema >;