Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions src/core/resources/connector/api.ts
Original file line number Diff line number Diff line change
@@ -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<ListConnectorsResponse> {
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<SyncConnectorResponse> {
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<OAuthStatusResponse> {
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<RemoveConnectorResponse> {
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;
}
1 change: 1 addition & 0 deletions src/core/resources/connector/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./api.js";
export * from "./config.js";
export * from "./resource.js";
export * from "./schema.js";
6 changes: 0 additions & 6 deletions src/core/resources/connector/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConnectorResource> = {
readAll: readAllConnectors,
push: async () => {
// Push will be implemented in the OAuth flow task
throw new Error("Connector push not yet implemented");
},
};
60 changes: 27 additions & 33 deletions src/core/resources/connector/schema.ts
Original file line number Diff line number Diff line change
@@ -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"),
Expand Down Expand Up @@ -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,
Expand All @@ -99,9 +89,6 @@ export const ConnectorResourceSchema = z.discriminatedUnion("type", [

export type ConnectorResource = z.infer<typeof ConnectorResourceSchema>;

/**
* Supported OAuth integration types.
*/
export const IntegrationTypeSchema = z.enum([
"googlecalendar",
"googledrive",
Expand All @@ -119,11 +106,6 @@ export const IntegrationTypeSchema = z.enum([

export type IntegrationType = z.infer<typeof IntegrationTypeSchema>;

// ─── API RESPONSE SCHEMAS ─────────────────────────────────────────────────────

/**
* Connector status from upstream API.
*/
export const ConnectorStatusSchema = z.enum([
"ACTIVE",
"DISCONNECTED",
Expand All @@ -132,9 +114,6 @@ export const ConnectorStatusSchema = z.enum([

export type ConnectorStatus = z.infer<typeof ConnectorStatusSchema>;

/**
* Upstream connector from the list API.
*/
export const UpstreamConnectorSchema = z.object({
integration_type: IntegrationTypeSchema,
status: ConnectorStatusSchema,
Expand All @@ -144,9 +123,6 @@ export const UpstreamConnectorSchema = z.object({

export type UpstreamConnector = z.infer<typeof UpstreamConnectorSchema>;

/**
* Response from GET /api/apps/{app_id}/external-auth/list
*/
export const ListConnectorsResponseSchema = z.object({
integrations: z.array(UpstreamConnectorSchema),
});
Expand All @@ -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<typeof SyncConnectorResponseSchema>;

export const OAuthPollingStatusSchema = z.enum(["ACTIVE", "FAILED", "PENDING"]);

export type OAuthPollingStatus = z.infer<typeof OAuthPollingStatusSchema>;

export const OAuthStatusResponseSchema = z.object({
status: OAuthPollingStatusSchema,
});

export type OAuthStatusResponse = z.infer<typeof OAuthStatusResponseSchema>;

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
>;