import { batchDeleteAppIndexDocumentsSchema, batchUpsertAppIndexDocumentsSchema, colineAppRegistrationPayloadSchema, createNotificationInputSchema, emitAmbientEventsInputSchema, appPermissionSchema, type AppPermission, type BatchDeleteAppIndexDocumentsInput, type BatchUpsertAppIndexDocumentsInput, type ColineAppRegistrationPayload, type CreateNotificationInputInput, type EmitAmbientEventsInputInput, } from "./manifest"; import { ColineApiError } from "./errors"; import type { components as AppPlatformComponents } from "./generated/app-platform-v1"; import { createAppPlatformClient, type AppPlatformClient, } from "./app-platform-client"; 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; } export interface ColineApiClientOptions { baseUrl: string; apiKey?: string; fetch?: typeof fetch; headers?: HeadersInit; } type Schemas = AppPlatformComponents["schemas"]; type UnwrapData = Schemas[TKey] extends { data: infer T } ? T : never; export type RegisteredAppDetail = Schemas["RegisteredAppDetail"]; export type RegisteredAppRecord = Omit< RegisteredAppDetail, "latestPublishedVersionId" >; export type RegisteredAppVersionSummary = Schemas["RegisteredAppVersionSummary"]; export type WorkspaceInstalledApp = Schemas["InstalledAppSummary"]; export type WorkspaceAppCatalogItem = Schemas["AppCatalogItem"]; export type WorkspaceAppsResponse = UnwrapData<"WorkspaceAppsResponse">; export interface InstallWorkspaceAppInput { appId: string; pinnedVersionId?: string; grantedPermissions?: AppPermission[]; } export type InstallWorkspaceAppResponse = UnwrapData<"InstallWorkspaceAppResponse">; export interface SubmitAppVersionForReviewInput { storeListed?: boolean; } export type SubmitAppVersionForReviewResponse = UnwrapData< "SubmitAppVersionForReviewResponse" >; export type WorkspaceAppPermissionsResponse = UnwrapData< "WorkspaceAppPermissionsResponse" >; export type WorkspaceInstalledAppResponse = UnwrapData< "WorkspaceInstalledAppResponse" >; export type WorkspaceAppSecretsResponse = UnwrapData< "WorkspaceAppSecretsResponse" >; export type WorkspaceAppSecretSummary = WorkspaceAppSecretsResponse["secrets"][number]; export type CreateWorkspaceAppSecretResponse = UnwrapData< "CreateWorkspaceAppSecretResponse" >; export type WorkspaceAppOauthConnectionsResponse = UnwrapData< "WorkspaceAppOauthConnectionsResponse" >; export type WorkspaceAppOauthConnectionSummary = WorkspaceAppOauthConnectionsResponse["connections"][number]; export type CreateWorkspaceAppOauthConnectionResponse = UnwrapData< "CreateWorkspaceAppOauthConnectionResponse" >; export type WorkspaceAppDeliveriesResponse = UnwrapData< "WorkspaceAppDeliveriesResponse" >; export type WorkspaceAppDeliverySummary = WorkspaceAppDeliveriesResponse["deliveries"][number]; export type SendWorkspaceAppTestDeliveryResponse = UnwrapData< "SendWorkspaceAppTestDeliveryResponse" >; export type AppFilesListResponse = UnwrapData<"AppFilesListResponse">; export type AppFileDocumentResponse = UnwrapData<"AppFileDocumentResponse">; export type AppFileDocumentUpdateResponse = UnwrapData< "AppFileDocumentUpdateResponse" >; export type CreateAppFileResponse = UnwrapData<"CreateAppFileResponse">; export type ExecuteAppActionResponse = UnwrapData<"ExecuteAppActionResponse">; export type OkResponse = UnwrapData<"OkResponse">; export type TabModelRecord = Schemas["TabModelRecord"]; export type TabModelsResponse = UnwrapData<"TabModelsResponse">; export type TabChatCompletionResponse = UnwrapData< "TabChatCompletionResponseEnvelope" >; export type UpsertAppIndexDocumentsResponse = UnwrapData< "UpsertAppIndexDocumentsResponse" >; export type DeleteAppIndexDocumentsResponse = UnwrapData< "DeleteAppIndexDocumentsResponse" >; export type EmitAppAmbientEventsResponse = UnwrapData< "EmitAppAmbientEventsResponse" >; 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[]; } export interface CreateAppFileInput { name: string; typeKey?: string; driveId?: string; parentId?: string | null; document?: Record; } export interface ExecuteAppActionInput { action: { type: string; payload?: Record; }; fileId?: string | null; } function joinUrl(baseUrl: string, path: string): string { return `${baseUrl.replace(/\/+$/, "")}${path}`; } function toRegisterAppInput(payload: ColineAppRegistrationPayload) { const notificationChannels = payload.manifest.notificationChannels.map((channel) => ({ key: channel.key, name: channel.name, defaultDeliveries: [...channel.defaultDeliveries], defaultPriority: channel.defaultPriority, ...(channel.description ? { description: channel.description } : {}), })); const manifest = { key: payload.manifest.key, name: payload.manifest.name, permissions: payload.manifest.permissions, notificationChannels, hosting: { mode: payload.manifest.hosting.mode, baseUrl: payload.manifest.hosting.baseUrl, }, ...(payload.manifest.description ? { description: payload.manifest.description } : {}), ...(payload.manifest.iconUrl ? { iconUrl: payload.manifest.iconUrl } : {}), ...(payload.manifest.kairo ? { kairo: JSON.parse(JSON.stringify(payload.manifest.kairo)) as Record< string, unknown >, } : {}), }; const fileTypes = payload.fileTypes.map((fileType) => ({ typeKey: fileType.typeKey, name: fileType.name, storage: fileType.storage, indexable: fileType.indexable, homeRenderMode: fileType.homeRenderMode, ...(fileType.description ? { description: fileType.description } : {}), })); return { version: payload.version, manifest, fileTypes, }; } export class ColineApiClient { private readonly baseUrl: string; private readonly apiKey: string | undefined; private readonly fetchImpl: typeof fetch; private readonly defaultHeaders: HeadersInit | undefined; private readonly appPlatformClient: AppPlatformClient; constructor(options: ColineApiClientOptions) { this.baseUrl = options.baseUrl; this.apiKey = options.apiKey; this.fetchImpl = options.fetch ?? fetch; this.defaultHeaders = options.headers; this.appPlatformClient = createAppPlatformClient(options); } private async unwrapGeneratedResponse( promise: Promise<{ data?: { data: T }; error?: ApiErrorPayload; response: Response; }>, ): Promise { const { data, error, response } = await promise; if (error) { throw new ColineApiError({ message: error.error?.message ?? `Request failed (${response.status}).`, type: "api", status: response.status, code: error.error?.code ?? null, }); } if (data === undefined) { throw new ColineApiError({ message: "Invalid API response.", type: "api", status: response.status, }); } return data.data; } 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.unwrapGeneratedResponse( this.appPlatformClient.GET("/api/v1/apps"), ); } async getApp(appKey: string): Promise<{ app: RegisteredAppDetail }> { return this.unwrapGeneratedResponse( this.appPlatformClient.GET("/api/v1/apps/{appKey}", { params: { path: { appKey } }, }), ); } async listAppVersions( appKey: string, ): Promise<{ versions: RegisteredAppVersionSummary[] }> { return this.unwrapGeneratedResponse( this.appPlatformClient.GET("/api/v1/apps/{appKey}/versions", { params: { path: { appKey } }, }), ); } async registerApp( payload: ColineAppRegistrationPayload, ): Promise<{ appId: string; appKey: string; versionId: string; version: string }> { const body = toRegisterAppInput(colineAppRegistrationPayloadSchema.parse(payload)); return this.unwrapGeneratedResponse( this.appPlatformClient.POST("/api/v1/apps", { body, }), ); } async createAppVersion( appKey: string, payload: ColineAppRegistrationPayload, ): Promise<{ appId: string; appKey: string; versionId: string; version: string }> { const body = toRegisterAppInput(colineAppRegistrationPayloadSchema.parse(payload)); return this.unwrapGeneratedResponse( this.appPlatformClient.POST("/api/v1/apps/{appKey}/versions", { params: { path: { appKey } }, body, }), ); } async submitAppVersionForReview( appKey: string, versionId: string, input: SubmitAppVersionForReviewInput = {}, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.POST( "/api/v1/apps/{appKey}/versions/{versionId}/submit", { params: { path: { appKey, versionId } }, body: { ...(input.storeListed !== undefined ? { storeListed: input.storeListed } : {}), }, }, ), ); } async listWorkspaceApps(workspaceSlug: string): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.GET("/api/v1/workspaces/{workspaceSlug}/apps", { params: { path: { workspaceSlug } }, }), ); } async installWorkspaceApp( workspaceSlug: string, input: InstallWorkspaceAppInput, ): Promise { const grantedPermissions = input.grantedPermissions ? input.grantedPermissions.map((permission) => appPermissionSchema.parse(permission), ) : undefined; return this.unwrapGeneratedResponse( this.appPlatformClient.POST("/api/v1/workspaces/{workspaceSlug}/apps", { params: { path: { workspaceSlug } }, body: { appId: input.appId, ...(input.pinnedVersionId ? { pinnedVersionId: input.pinnedVersionId } : {}), ...(grantedPermissions ? { grantedPermissions } : {}), }, }), ); } async getWorkspaceAppPermissions( workspaceSlug: string, appKey: string, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.GET( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/permissions", { params: { path: { workspaceSlug, appKey } }, }, ), ); } async getWorkspaceInstalledApp( workspaceSlug: string, appKey: string, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.GET("/api/v1/workspaces/{workspaceSlug}/apps/{appKey}", { params: { path: { workspaceSlug, appKey } }, }), ); } async updateWorkspaceAppPermissions( workspaceSlug: string, appKey: string, input: UpdateWorkspaceAppPermissionsInput, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.PUT( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/permissions", { params: { path: { workspaceSlug, appKey } }, body: { grantedPermissions: input.grantedPermissions.map((permission) => appPermissionSchema.parse(permission), ), }, }, ), ); } async uninstallWorkspaceApp( workspaceSlug: string, appKey: string, ): Promise<{ ok: true }> { return this.unwrapGeneratedResponse( this.appPlatformClient.DELETE( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}", { params: { path: { workspaceSlug, appKey } }, }, ), ); } async listWorkspaceAppSecrets( workspaceSlug: string, appKey: string, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.GET( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/secrets", { params: { path: { workspaceSlug, appKey } }, }, ), ); } async createWorkspaceAppSecret( workspaceSlug: string, appKey: string, input: { key: string; value: string }, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.POST( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/secrets", { params: { path: { workspaceSlug, appKey } }, body: input, }, ), ); } async deleteWorkspaceAppSecret( workspaceSlug: string, appKey: string, secretId: string, ): Promise<{ ok: true }> { return this.unwrapGeneratedResponse( this.appPlatformClient.DELETE( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/secrets/{secretId}", { params: { path: { workspaceSlug, appKey, secretId } }, }, ), ); } async listWorkspaceAppOauthConnections( workspaceSlug: string, appKey: string, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.GET( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/oauth-connections", { params: { path: { workspaceSlug, appKey } }, }, ), ); } async createWorkspaceAppOauthConnection( workspaceSlug: string, appKey: string, input: { providerKey: string; externalAccountId?: string | null; accessToken: string; refreshToken?: string | null; scopes: string[]; tokenExpiresAt?: string | null; metadata?: Record; }, ): Promise { const body: { providerKey: string; accessToken: string; scopes: string[]; externalAccountId?: string; refreshToken?: string; tokenExpiresAt?: string; metadata?: Record; } = { providerKey: input.providerKey, accessToken: input.accessToken, scopes: input.scopes, }; if (input.externalAccountId) { body.externalAccountId = input.externalAccountId; } if (input.refreshToken) { body.refreshToken = input.refreshToken; } if (input.tokenExpiresAt) { body.tokenExpiresAt = input.tokenExpiresAt; } if (input.metadata) { body.metadata = input.metadata; } return this.unwrapGeneratedResponse( this.appPlatformClient.POST( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/oauth-connections", { params: { path: { workspaceSlug, appKey } }, body, }, ), ); } async deleteWorkspaceAppOauthConnection( workspaceSlug: string, appKey: string, connectionId: string, ): Promise<{ ok: true }> { return this.unwrapGeneratedResponse( this.appPlatformClient.DELETE( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/oauth-connections/{connectionId}", { params: { path: { workspaceSlug, appKey, connectionId } }, }, ), ); } async listWorkspaceAppDeliveries( workspaceSlug: string, appKey: string, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.GET( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/deliveries", { params: { path: { workspaceSlug, appKey } }, }, ), ); } async sendWorkspaceAppTestDelivery( workspaceSlug: string, appKey: string, input: { eventType: string; targetUrl?: string; payload: Record; }, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.POST( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/deliveries", { params: { path: { workspaceSlug, appKey } }, body: input, }, ), ); } async replayWorkspaceAppDelivery( workspaceSlug: string, appKey: string, deliveryId: string, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.POST( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/deliveries/{deliveryId}/replay", { params: { path: { workspaceSlug, appKey, deliveryId } }, }, ), ); } async emitAppAmbientEvents( workspaceSlug: string, appKey: string, input: EmitAmbientEventsInputInput, ): Promise { const parsed = emitAmbientEventsInputSchema.parse(input); const body = { events: parsed.events.map((event) => ({ sourceKind: event.sourceKind, action: event.action, entityType: event.entityType, entityId: event.entityId, dedupeKey: event.dedupeKey, payload: event.payload, metadata: event.metadata, schemaVersion: event.schemaVersion, ...(event.actorUserId ? { actorUserId: event.actorUserId } : {}), ...(event.containerType ? { containerType: event.containerType } : {}), ...(event.containerId ? { containerId: event.containerId } : {}), ...(event.occurredAt ? { occurredAt: event.occurredAt.toISOString() } : {}), })), }; return this.unwrapGeneratedResponse( this.appPlatformClient.POST( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/ambient/events", { params: { path: { workspaceSlug, appKey } }, body, }, ), ); } async createAppNotification( workspaceSlug: string, appKey: string, input: CreateNotificationInputInput, ): Promise { const parsed = createNotificationInputSchema.parse(input); const body = { channelKey: parsed.channelKey, typeKey: parsed.typeKey, recipients: parsed.recipients.map((recipient) => ({ userId: recipient.userId, })), title: parsed.title, priority: parsed.priority, metadata: parsed.metadata, ...(parsed.body ? { body: parsed.body } : {}), ...(parsed.targetUrl ? { targetUrl: parsed.targetUrl } : {}), ...(parsed.icon ? { icon: parsed.icon } : {}), ...(parsed.dedupeKey ? { dedupeKey: parsed.dedupeKey } : {}), }; return this.unwrapGeneratedResponse( this.appPlatformClient.POST( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/notifications", { params: { path: { workspaceSlug, appKey } }, body, }, ), ); } async upsertAppIndexDocuments( workspaceSlug: string, appKey: string, input: BatchUpsertAppIndexDocumentsInput, ): Promise { const parsed = batchUpsertAppIndexDocumentsSchema.parse(input); const body = { documents: parsed.documents.map((document) => ({ documentKey: document.documentKey, documentType: document.documentType, title: document.title, body: document.body, metadata: document.metadata, visibilityMode: document.visibilityMode, visibility: document.visibility.map((grant) => ({ subjectType: grant.subjectType, subjectId: grant.subjectId, })), ...(document.fileId ? { fileId: document.fileId } : {}), ...(document.url ? { url: document.url } : {}), })), }; return this.unwrapGeneratedResponse( this.appPlatformClient.POST( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/index-documents", { params: { path: { workspaceSlug, appKey } }, body, }, ), ); } async deleteAppIndexDocuments( workspaceSlug: string, appKey: string, input: BatchDeleteAppIndexDocumentsInput, ): Promise { const body = batchDeleteAppIndexDocumentsSchema.parse(input); return this.unwrapGeneratedResponse( this.appPlatformClient.DELETE( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/index-documents", { params: { path: { workspaceSlug, appKey } }, body, }, ), ); } async listAppFiles( workspaceSlug: string, appKey: string, query?: { driveId?: string; typeKey?: string; limit?: number; }, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.GET( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/files", { params: { path: { workspaceSlug, appKey }, ...(query ? { query: { ...(query.driveId ? { driveId: query.driveId } : {}), ...(query.typeKey ? { typeKey: query.typeKey } : {}), ...(query.limit !== undefined ? { limit: query.limit } : {}), }, } : {}), }, }, ), ); } async createAppFile( workspaceSlug: string, appKey: string, input: CreateAppFileInput, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.POST( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/files", { params: { path: { workspaceSlug, appKey } }, body: { name: input.name, ...(input.typeKey ? { typeKey: input.typeKey } : {}), ...(input.driveId ? { driveId: input.driveId } : {}), ...(input.parentId !== undefined ? { parentId: input.parentId } : {}), ...(input.document ? { document: input.document } : {}), }, }, ), ); } async executeAppAction( workspaceSlug: string, appKey: string, input: ExecuteAppActionInput, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.POST( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/actions", { params: { path: { workspaceSlug, appKey } }, body: { action: { type: input.action.type, ...(input.action.payload ? { payload: input.action.payload } : {}), }, ...(input.fileId !== undefined ? { fileId: input.fileId } : {}), }, }, ), ); } async getAppFileDocument( workspaceSlug: string, appKey: string, fileId: string, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.GET( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/files/{fileId}", { params: { path: { workspaceSlug, appKey, fileId } }, }, ), ); } async updateAppFileDocument( workspaceSlug: string, appKey: string, fileId: string, document: Record, ): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.PATCH( "/api/v1/workspaces/{workspaceSlug}/apps/{appKey}/files/{fileId}", { params: { path: { workspaceSlug, appKey, fileId } }, body: { document }, }, ), ); } async listTabModels(): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.GET("/api/v1/tab/models"), ); } async createTabChatCompletion(input: { model: string; stream?: boolean; tab_context: Record; messages?: Array<{ role?: string; content?: string }>; }): Promise { return this.unwrapGeneratedResponse( this.appPlatformClient.POST("/api/v1/tab/chat/completions", { body: 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); } }