import { batchDeleteAppIndexDocumentsSchema, batchUpsertAppIndexDocumentsSchema, colineAppRegistrationPayloadSchema, type ColineKairoIntegration, createNotificationInputSchema, emitAmbientEventsInputSchema, appPermissionSchema, type AppPermission, type BatchDeleteAppIndexDocumentsInput, type BatchUpsertAppIndexDocumentsInput, type ColineAppRegistrationPayload, type CreateNotificationInputInput, type EmitAmbientEventsInputInput, } from "./manifest"; import { ColineApiError } from "./errors"; interface ApiErrorPayload { error?: { code?: string; message?: string; }; } async function parseJsonResponse(response: Response): Promise { const payload = (await response.json().catch(() => null)) as T | null; if (!response.ok) { const errorMessage = payload && typeof payload === "object" && "error_description" in payload && typeof (payload as Record)["error_description"] === "string" ? ((payload as Record)["error_description"] as string) : `Request failed (${response.status}).`; throw new ColineApiError({ message: errorMessage, type: "oauth", status: response.status, code: payload && typeof payload === "object" && "error" in payload && typeof (payload as Record)["error"] === "string" ? ((payload as Record)["error"] as string) : null, }); } if (!payload) { throw new ColineApiError({ message: "Invalid JSON response.", type: "oauth", status: response.status, }); } return payload; } async function parseApiResponse(response: Response): Promise { const payload = (await response.json().catch(() => null)) as | { data?: T; error?: ApiErrorPayload["error"] } | null; if (!response.ok) { throw new ColineApiError({ message: payload?.error?.message ?? `Request failed (${response.status}).`, type: "api", status: response.status, code: payload?.error?.code ?? null, }); } if (!payload || !("data" in payload)) { throw new ColineApiError({ message: "Invalid API response.", type: "api", status: response.status, }); } return payload.data as T; } export interface ColineApiClientOptions { baseUrl: string; apiKey?: string; fetch?: typeof fetch; headers?: HeadersInit; } export interface RegisteredAppRecord { appId: string; appKey: string; name: string; description: string | null; iconUrl: string | null; status: "draft" | "active" | "disabled" | "archived"; ownerUserId: string | null; storeListed: boolean; latestPublishedVersion: string | null; } export interface RegisteredAppDetail extends RegisteredAppRecord { latestPublishedVersionId: string | null; } export interface RegisteredAppVersionSummary { versionId: string; version: string; status: "draft" | "published" | "deprecated"; reviewStatus: "draft" | "submitted" | "approved" | "rejected"; apiBaseUrl: string; permissions: string[]; notificationChannels: Array<{ key: string; name: string; description?: string; defaultDeliveries: string[]; defaultPriority: "low" | "default" | "high"; }>; kairo: ColineKairoIntegration | null; fileTypes: Array<{ typeKey: string; name: string; description: string | null; }>; submittedAt: string | null; reviewedAt: string | null; reviewNotes: string | null; publishedAt: string | null; createdAt: string; updatedAt: string; } export interface WorkspaceInstalledApp { installId: string; appId: string; appKey: string; name: string; description: string | null; iconUrl: string | null; status: "active" | "suspended" | "revoked"; installScope: "workspace" | "user"; scopeSubjectId: string; version: string | null; } export interface WorkspaceAppCatalogItem { appId: string; appKey: string; name: string; description: string | null; iconUrl: string | null; status: "draft" | "active" | "disabled" | "archived"; latestPublishedVersion: string | null; latestPublishedVersionId: string | null; isInstalled: boolean; } export interface WorkspaceAppsResponse { installedApps: WorkspaceInstalledApp[]; catalog: WorkspaceAppCatalogItem[]; permissions: { canManageApps: boolean; }; } export interface InstallWorkspaceAppInput { appId: string; pinnedVersionId?: string; grantedPermissions?: AppPermission[]; } export interface InstallWorkspaceAppResponse { installId: string; appId: string; appKey: string; version: string; grantedPermissions: AppPermission[]; } export interface SubmitAppVersionForReviewInput { storeListed?: boolean; } export interface SubmitAppVersionForReviewResponse { appId: string; appKey: string; versionId: string; version: string; reviewStatus: "submitted"; storeListed: boolean; } export interface WorkspaceAppPermissionsResponse { install: WorkspaceInstalledApp; availablePermissions: string[]; grantedPermissions: string[]; permissions: { canManageApps: boolean; }; } export interface WorkspaceInstalledAppResponse { install: WorkspaceInstalledApp; permissions: { canManageApps: boolean; }; } export interface AppFileDocumentResponse { file: { id: string; name: string; fileType: string; appKey: string; appName: string; typeLabel: string; openHref: string; updatedAt: string; }; document: Record; version: number; capabilities: { canRead: boolean; canWrite: boolean; canManage: boolean; }; } export interface TabModelRecord { id: string; object: "model"; created: number; owned_by: string; providerModelId: string; pricing: { inputPerMillionUsd: number; outputPerMillionUsd: number; }; capabilities: { streaming: boolean; surfaces: string[]; }; } export interface OAuthTokenResponse { access_token: string; token_type: "Bearer"; expires_in: number; refresh_token: string; scope: string; } export interface OAuthUserInfoResponse { sub: string; email: string; email_verified: boolean; name: string; given_name: string | null; family_name: string | null; picture: string | null; preferred_username: string | null; scope: string; } export interface UpdateWorkspaceAppPermissionsInput { grantedPermissions: AppPermission[]; } function joinUrl(baseUrl: string, path: string): string { return `${baseUrl.replace(/\/+$/, "")}${path}`; } export class ColineApiClient { private readonly baseUrl: string; private readonly apiKey: string | undefined; private readonly fetchImpl: typeof fetch; private readonly defaultHeaders: HeadersInit | undefined; constructor(options: ColineApiClientOptions) { this.baseUrl = options.baseUrl; this.apiKey = options.apiKey; this.fetchImpl = options.fetch ?? fetch; this.defaultHeaders = options.headers; } private async request(path: string, init?: RequestInit): Promise { const headers = new Headers(this.defaultHeaders); const requestHeaders = new Headers(init?.headers); requestHeaders.forEach((value, key) => headers.set(key, value)); if (!headers.has("Content-Type") && init?.body) { headers.set("Content-Type", "application/json"); } if (this.apiKey && !headers.has("Authorization")) { headers.set("Authorization", `Bearer ${this.apiKey}`); } const response = await this.fetchImpl(joinUrl(this.baseUrl, path), { ...init, headers, }); return parseApiResponse(response); } withApiKey(apiKey: string): ColineApiClient { const options: ColineApiClientOptions = { baseUrl: this.baseUrl, apiKey, fetch: this.fetchImpl, }; if (this.defaultHeaders !== undefined) { options.headers = this.defaultHeaders; } return new ColineApiClient(options); } withHeaders(headers: HeadersInit): ColineApiClient { const mergedHeaders = new Headers(this.defaultHeaders); new Headers(headers).forEach((value, key) => mergedHeaders.set(key, value)); const options: ColineApiClientOptions = { baseUrl: this.baseUrl, fetch: this.fetchImpl, headers: mergedHeaders, }; if (this.apiKey !== undefined) { options.apiKey = this.apiKey; } return new ColineApiClient(options); } buildLoginWithColineAuthorizeUrl(input: { clientId: string; redirectUri: string; scope?: string; state?: string; codeChallenge?: string; codeChallengeMethod?: "S256" | "plain"; }): string { const url = new URL("/api/v1/oauth/authorize", this.baseUrl); url.searchParams.set("response_type", "code"); url.searchParams.set("client_id", input.clientId); url.searchParams.set("redirect_uri", input.redirectUri); if (input.scope) { url.searchParams.set("scope", input.scope); } if (input.state) { url.searchParams.set("state", input.state); } if (input.codeChallenge) { url.searchParams.set("code_challenge", input.codeChallenge); } if (input.codeChallengeMethod) { url.searchParams.set("code_challenge_method", input.codeChallengeMethod); } return url.toString(); } async listApps(): Promise<{ apps: RegisteredAppRecord[] }> { return this.request<{ apps: RegisteredAppRecord[] }>("/api/v1/apps"); } async getApp(appKey: string): Promise<{ app: RegisteredAppDetail }> { return this.request<{ app: RegisteredAppDetail }>(`/api/v1/apps/${appKey}`); } async listAppVersions( appKey: string, ): Promise<{ versions: RegisteredAppVersionSummary[] }> { return this.request<{ versions: RegisteredAppVersionSummary[] }>( `/api/v1/apps/${appKey}/versions`, ); } async registerApp( payload: ColineAppRegistrationPayload, ): Promise<{ appId: string; appKey: string; versionId: string; version: string }> { const body = colineAppRegistrationPayloadSchema.parse(payload); return this.request<{ appId: string; appKey: string; versionId: string; version: string; }>("/api/apps", { method: "POST", body: JSON.stringify(body), }); } async submitAppVersionForReview( appKey: string, versionId: string, input: SubmitAppVersionForReviewInput = {}, ): Promise { return this.request( `/api/apps/${appKey}/versions/${versionId}/submit`, { method: "POST", body: JSON.stringify({ ...(input.storeListed !== undefined ? { storeListed: input.storeListed } : {}), }), }, ); } async listWorkspaceApps(workspaceSlug: string): Promise { return this.request( `/api/v1/workspaces/${workspaceSlug}/apps`, ); } async installWorkspaceApp( workspaceSlug: string, input: InstallWorkspaceAppInput, ): Promise { const grantedPermissions = input.grantedPermissions ? input.grantedPermissions.map((permission) => appPermissionSchema.parse(permission), ) : undefined; return this.request( `/api/v1/workspaces/${workspaceSlug}/apps`, { method: "POST", body: JSON.stringify({ appId: input.appId, ...(input.pinnedVersionId ? { pinnedVersionId: input.pinnedVersionId } : {}), ...(grantedPermissions ? { grantedPermissions } : {}), }), }, ); } async getWorkspaceAppPermissions( workspaceSlug: string, appKey: string, ): Promise { return this.request( `/api/v1/workspaces/${workspaceSlug}/apps/${appKey}/permissions`, ); } async getWorkspaceInstalledApp( workspaceSlug: string, appKey: string, ): Promise { return this.request( `/api/v1/workspaces/${workspaceSlug}/apps/${appKey}`, ); } async updateWorkspaceAppPermissions( workspaceSlug: string, appKey: string, input: UpdateWorkspaceAppPermissionsInput, ): Promise { return this.request( `/api/v1/workspaces/${workspaceSlug}/apps/${appKey}/permissions`, { method: "PUT", body: JSON.stringify({ grantedPermissions: input.grantedPermissions.map((permission) => appPermissionSchema.parse(permission), ), }), }, ); } async uninstallWorkspaceApp( workspaceSlug: string, appKey: string, ): Promise<{ ok: true }> { return this.request<{ ok: true }>( `/api/v1/workspaces/${workspaceSlug}/apps/${appKey}`, { method: "DELETE", }, ); } async emitAppAmbientEvents( workspaceSlug: string, appKey: string, input: EmitAmbientEventsInputInput, ): Promise<{ events: Array<{ id: string; dedupeKey: string }> }> { const body = emitAmbientEventsInputSchema.parse(input); return this.request<{ events: Array<{ id: string; dedupeKey: string }> }>( `/api/v1/workspaces/${workspaceSlug}/apps/${appKey}/ambient/events`, { method: "POST", body: JSON.stringify(body), }, ); } async createAppNotification( workspaceSlug: string, appKey: string, input: CreateNotificationInputInput, ): Promise<{ ok: true }> { const body = createNotificationInputSchema.parse(input); return this.request<{ ok: true }>( `/api/v1/workspaces/${workspaceSlug}/apps/${appKey}/notifications`, { method: "POST", body: JSON.stringify(body), }, ); } async upsertAppIndexDocuments( workspaceSlug: string, appKey: string, input: BatchUpsertAppIndexDocumentsInput, ): Promise<{ documents: Array<{ id: string; documentKey: string; visibilityMode: string }> }> { const body = batchUpsertAppIndexDocumentsSchema.parse(input); return this.request<{ documents: Array<{ id: string; documentKey: string; visibilityMode: string }>; }>(`/api/v1/workspaces/${workspaceSlug}/apps/${appKey}/index-documents`, { method: "POST", body: JSON.stringify(body), }); } async deleteAppIndexDocuments( workspaceSlug: string, appKey: string, input: BatchDeleteAppIndexDocumentsInput, ): Promise<{ deleted: Array<{ id: string; documentKey: string }> }> { const body = batchDeleteAppIndexDocumentsSchema.parse(input); return this.request<{ deleted: Array<{ id: string; documentKey: string }> }>( `/api/v1/workspaces/${workspaceSlug}/apps/${appKey}/index-documents`, { method: "DELETE", body: JSON.stringify(body), }, ); } async getAppFileDocument( workspaceSlug: string, appKey: string, fileId: string, ): Promise { return this.request( `/api/v1/workspaces/${workspaceSlug}/apps/${appKey}/files/${fileId}`, ); } async updateAppFileDocument( workspaceSlug: string, appKey: string, fileId: string, document: Record, ): Promise<{ document: Record; version: number; updatedAt: string }> { return this.request<{ document: Record; version: number; updatedAt: string; }>(`/api/v1/workspaces/${workspaceSlug}/apps/${appKey}/files/${fileId}`, { method: "PATCH", body: JSON.stringify({ document }), }); } async listTabModels(): Promise<{ object: "list"; data: TabModelRecord[] }> { return this.request<{ object: "list"; data: TabModelRecord[] }>( "/api/v1/tab/models", ); } async createTabChatCompletion(input: { model: string; stream?: boolean; tab_context: Record; messages?: Array<{ role?: string; content?: string }>; }): Promise<{ id: string; object: "chat.completion"; created: number; model: string; choices: Array<{ index: number; message: { role: "assistant"; content: string }; finish_reason: "stop"; }>; usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; }> { return this.request("/api/v1/tab/chat/completions", { method: "POST", body: JSON.stringify(input), }); } async exchangeOAuthCode(input: { clientId: string; clientSecret: string; code: string; redirectUri: string; codeVerifier?: string; }): Promise { const response = await this.fetchImpl(joinUrl(this.baseUrl, "/api/v1/oauth/token"), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ grant_type: "authorization_code", client_id: input.clientId, client_secret: input.clientSecret, code: input.code, redirect_uri: input.redirectUri, ...(input.codeVerifier ? { code_verifier: input.codeVerifier } : {}), }), }); return parseJsonResponse(response); } async getOAuthUserInfo(accessToken: string): Promise { const response = await this.fetchImpl(joinUrl(this.baseUrl, "/api/v1/oauth/userinfo"), { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, }, }); return parseJsonResponse(response); } }