import { type Settings } from "@opengeni/config"; import { OPENGENI_SLACK_BOT_CREDENTIAL_LABEL, OPENGENI_SLACK_BOT_CREDENTIAL_ROLE, type AccessGrant, type ConnectionMetadata, type OpenGeniSlackBotDisplayName, type OpenGeniSlackBotConnectionMetadata } from "@opengeni/contracts"; import { type Database } from "@opengeni/db"; import { type FetchLike } from "@opengeni/network"; import { HTTPException } from "hono/http-exception"; export declare const SLACK_REACTION_IMAGE_MAX_BYTES: number; declare const SLACK_REACTION_CONTEXT_CHECKPOINT_VERSION = 1; type SlackPayload = Record & { ok?: unknown; error?: unknown; }; export type SlackFilesListPage = { count: number; page: number; }; export type SlackFilesCursorContext = { connectionId: string; key: Uint8Array; }; export type VerifiedOpenGeniSlackBot = { grantedScopes: string[]; metadata: OpenGeniSlackBotConnectionMetadata; }; export type PreparedSlackReactionImage = Readonly<{ fileId: string; filename: string; declaredMimeType: string; declaredSizeBytes: number | null; downloadUrl: URL | null; }>; export type DownloadedSlackReactionImage = Readonly<{ fileId: string; filename: string; contentType: "image/png" | "image/jpeg" | "image/webp"; bytes: Uint8Array; }>; export type ExchangedOpenGeniSlackAuthorization = Readonly<{ accessToken: string; appId: string; }>; export declare function exchangeOpenGeniSlackAuthorizationCode(input: { code: string; clientId: string; clientSecret: string; redirectUri: string; }, fetchImpl?: FetchLike): Promise; export type SlackBotReceipt = { credentialRole: typeof OPENGENI_SLACK_BOT_CREDENTIAL_ROLE; credentialLabel: typeof OPENGENI_SLACK_BOT_CREDENTIAL_LABEL; connectionId: string; slackTeamId: string; operation: SlackBotOperation; operationId?: string; clientMessageId?: string; }; export type SlackMessageBlock = { type: "section"; block_id?: string; text: { type: "mrkdwn" | "plain_text"; text: string; emoji?: boolean; }; } | { type: "actions"; block_id: string; elements: Array<{ type: "button"; action_id: string; value: string; text: { type: "plain_text"; text: string; emoji?: boolean; }; style?: "primary" | "danger"; }>; } | { type: "context"; block_id?: string; elements: Array<{ type: "mrkdwn" | "plain_text"; text: string; emoji?: boolean; }>; } | { type: "divider"; }; export type SlackHomeBlock = { type: "header"; block_id?: string; text: { type: "plain_text"; text: string; emoji?: boolean; }; } | { type: "section"; block_id?: string; text: { type: "mrkdwn" | "plain_text"; text: string; emoji?: boolean; }; accessory?: { type: "button"; action_id: string; text: { type: "plain_text"; text: string; emoji?: boolean; }; url: string; }; } | { type: "context"; block_id?: string; elements: Array<{ type: "mrkdwn" | "plain_text"; text: string; emoji?: boolean; }>; } | { type: "actions"; block_id: string; elements: Array<{ type: "button"; action_id: string; text: { type: "plain_text"; text: string; emoji?: boolean; }; url: string; style?: "primary" | "danger"; }>; } | { type: "divider"; }; type SlackBotOperation = "channels.list" | "search.context" | "channel_history.read" | "thread_replies.read" | "users.list" | "files.list" | "file.info" | "file.content.read" | "home.publish" | "message.post" | "message.update" | "message.delete"; type SlackBotContext = { accountId: string; workspaceId: string; subjectId: string | null; sessionId?: string | null; scheduledTaskId?: string | null; }; type SlackProviderAuthorization = () => Promise; export type SlackReactionContextCheckpointBinding = { inboxId: string; accountId: string; workspaceId: string; connectionId: string; providerEventId: string; providerMessageId: string; slackTeamId: string; slackChannelId: string; slackMessageTs: string; }; type SlackReactionCheckpointMessage = { timestamp: string; userId: string; botId: string; threadTimestamp: string; text: string; files: Array<{ id: string; label: string; }>; }; type SlackReactionContextCheckpointUnsigned = { version: typeof SLACK_REACTION_CONTEXT_CHECKPOINT_VERSION; binding: SlackReactionContextCheckpointBinding; state: { createdAtMs: number; pageCount: number; nextCursor: string; seenCursors: string[]; seenMessageTimestamps: string[]; threadTimestamp: string | null; messages: SlackReactionCheckpointMessage[]; }; }; export type SlackReactionContextCheckpoint = SlackReactionContextCheckpointUnsigned & { signature: string; }; export declare class SlackBotProviderError extends Error { readonly code: string; readonly retryAfterMs: number | null; constructor(code: string, retryAfterMs?: number | null); } export declare function authorizeSlackSharedImageRead(channel: { isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; isMpim: boolean; }, authorizeSharedRead: (() => Promise) | undefined): Promise; export type SlackBotCredentialVerificationFailureReason = "scope_mismatch" | "identity_mismatch"; export declare class SlackBotCredentialVerificationError extends HTTPException { readonly failureReason: SlackBotCredentialVerificationFailureReason; constructor(failureReason: SlackBotCredentialVerificationFailureReason, message: string); } /** * Validates a write-only xoxb credential before it can enter encrypted storage. * The OAuth exchange's app_id is the immutable app authority. Slack bot profile * names are mutable presentation data and may lag manifest/App Home changes, so * they must not decide whether a credential belongs to this installation. */ export declare function verifyOpenGeniSlackBotCredential(token: string, expected: Readonly<{ appId: string; displayName: OpenGeniSlackBotDisplayName; }>, fetchImpl?: FetchLike, now?: Date): Promise; export declare function resolveSlackBotConnectionForTool(input: { db: Database; grant: AccessGrant; sessionId: string | null; requestedConnectionId?: string; }): Promise<{ connection: ConnectionMetadata; metadata: OpenGeniSlackBotConnectionMetadata; context: SlackBotContext; }>; export declare class OpenGeniSlackBotClient { private readonly db; private readonly settings; private readonly connection; private readonly metadata; private readonly context; private readonly fetchImpl; private readonly authorizeProviderRequest?; private readonly resolveCredential; constructor(db: Database, settings: Settings, connection: ConnectionMetadata, metadata: OpenGeniSlackBotConnectionMetadata, context: SlackBotContext, fetchImpl?: FetchLike, authorizeProviderRequest?: SlackProviderAuthorization | undefined); listChannels(input?: { limit?: number; cursor?: string; }): Promise<{ channels: { id: string; name: string; isPrivate: boolean; isMember: boolean; isDirectMessage: boolean; isMpim: boolean; isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; contextTeamId: string | null; connectedTeamIds: string[] | null; sharedTeamIds: string[] | null; topic: string; purpose: string; numMembers: number | null; }[]; nextCursor: string | null; } & { receipt: SlackBotReceipt; }>; verifyChannelAccess(channelId: string): Promise<{ id: string; name: string; isPrivate: boolean; isMember: boolean; isDirectMessage: boolean; isMpim: boolean; isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; contextTeamId: string | null; connectedTeamIds: string[] | null; sharedTeamIds: string[] | null; topic: string; purpose: string; numMembers: number | null; }>; /** * Replace one Slack user's private App Home view. `views.publish` is a * naturally convergent replace operation: Slack event retries may safely * repeat the exact bounded view without creating duplicate provider state. */ publishHomeView(input: { userId: string; blocks: SlackHomeBlock[]; hash?: string | null; }): Promise<{ viewId: string; } & { receipt: SlackBotReceipt; }>; slackTaskPolicyFacts(channelId: string, userId: string): Promise<({ conversation: { id: string; name: string; isPrivate: boolean; isMember: boolean; isDirectMessage: boolean; isMpim: boolean; isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; contextTeamId: string | null; connectedTeamIds: string[] | null; sharedTeamIds: string[] | null; topic: string; purpose: string; numMembers: number | null; }; initiator: null; } | { conversation: { id: string; name: string; isPrivate: boolean; isMember: boolean; isDirectMessage: boolean; isMpim: boolean; isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; contextTeamId: string | null; connectedTeamIds: string[] | null; sharedTeamIds: string[] | null; topic: string; purpose: string; numMembers: number | null; }; initiator: { id: string; teamId: string | null; isGuest: boolean | null; isExternal: boolean | null; }; }) & { receipt: SlackBotReceipt; }>; channelHistory(input: { channelId: string; limit?: number; cursor?: string; latest?: string; inclusive?: boolean; authorizeRead?: () => Promise; }): Promise<{ channel: { id: string; name: string; isPrivate: boolean; isMember: boolean; isDirectMessage: boolean; isMpim: boolean; isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; contextTeamId: string | null; connectedTeamIds: string[] | null; sharedTeamIds: string[] | null; topic: string; purpose: string; numMembers: number | null; }; messages: { timestamp: string; userId: string; botId: string; threadTimestamp: string; text: string; files: { id: string; name: string; title: string; mimetype: string; filetype: string; mode: string; size: number | null; originatingHuddleId: string; huddleTranscriptFileId: string; }[]; }[]; nextCursor: string | null; } & { receipt: SlackBotReceipt; }>; threadReplies(input: { channelId: string; threadTimestamp: string; limit?: number; cursor?: string; oldest?: string; latest?: string; inclusive?: boolean; authorizeRead?: () => Promise; }): Promise<{ channel: { id: string; name: string; isPrivate: boolean; isMember: boolean; isDirectMessage: boolean; isMpim: boolean; isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; contextTeamId: string | null; connectedTeamIds: string[] | null; sharedTeamIds: string[] | null; topic: string; purpose: string; numMembers: number | null; }; threadTimestamp: string; messages: { timestamp: string; userId: string; botId: string; threadTimestamp: string; text: string; files: { id: string; name: string; title: string; mimetype: string; filetype: string; mode: string; size: number | null; originatingHuddleId: string; huddleTranscriptFileId: string; }[]; }[]; nextCursor: string | null; } & { receipt: SlackBotReceipt; }>; reactionMessageContext(input: { channelId: string; messageTimestamp: string; checkpoint: unknown | null; checkpointBinding: SlackReactionContextCheckpointBinding; saveCheckpoint: (checkpoint: SlackReactionContextCheckpoint) => Promise; }): Promise<{ channel: { id: string; name: string; isPrivate: boolean; isMember: boolean; isDirectMessage: boolean; isMpim: boolean; isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; contextTeamId: string | null; connectedTeamIds: string[] | null; sharedTeamIds: string[] | null; topic: string; purpose: string; numMembers: number | null; }; threadTimestamp: string; reactedMessage: { timestamp: string; userId: string; botId: string; threadTimestamp: string; text: string; files: { id: string; name: string; title: string; mimetype: string; filetype: string; mode: string; size: number | null; originatingHuddleId: string; huddleTranscriptFileId: string; }[]; }; messages: { timestamp: string; userId: string; botId: string; threadTimestamp: string; text: string; files: { id: string; name: string; title: string; mimetype: string; filetype: string; mode: string; size: number | null; originatingHuddleId: string; huddleTranscriptFileId: string; }[]; }[]; truncated: boolean; } & { receipt: SlackBotReceipt; }>; /** * Re-fetch and authorize the exact reacted-message files before any byte or * workspace-storage mutation. The returned private URLs are process-local * capabilities only and must never be persisted, logged, or projected into * session input. */ prepareReactionImageDownloads(input: { channelId: string; files: readonly { id: string; name: string; title: string; }[]; authorizeSharedRead?: () => Promise; }): Promise; /** Download and fully validate one previously authorized Slack image. */ downloadReactionImage(input: PreparedSlackReactionImage, authorizeSharedRead?: () => Promise): Promise; listUsers(input?: { limit?: number; cursor?: string; }): Promise<{ users: { id: string; name: string; displayName: string; realName: string; isBot: boolean; deleted: boolean; }[]; nextCursor: string | null; } & { receipt: SlackBotReceipt; }>; /** * Workspace-wide public search through Slack's Real-time Search API * (`assistant.search.context`) under the bot identity. * * The bot never carries private-search authority: `channel_types` is pinned * to `public_channel` server-side regardless of caller input, so private * channels, DMs, and MPIMs remain reachable only through a member's personal * hosted-MCP grant. An install predating the search scopes fails closed with * a reinstall hint instead of leaking Slack's `missing_scope` error. */ searchContext(input: { query: string; contentTypes?: readonly ("messages" | "files" | "channels")[]; includeBots?: boolean; /** UNIX seconds bounds on message timestamps. */ before?: number; after?: number; sort?: "score" | "timestamp"; sortDir?: "asc" | "desc"; cursor?: string; limit?: number; }): Promise<{ messages: { channelId: string; channelName: string; ts: string; authorUserId: string; authorName: string; isAuthorBot: boolean; content: string; permalink: string | null; }[]; files: { id: string; title: string; filetype: string; authorUserId: string; authorName: string; dateCreated: number | null; dateUpdated: number | null; content: string; permalink: string | null; }[]; channels: { name: string; topic: string; purpose: string; creatorUserId: string; dateCreated: number | null; permalink: string | null; }[]; nextCursor: string | null; } & { receipt: SlackBotReceipt; }>; listFiles(input: { channelId: string; limit?: number; cursor?: string; }): Promise<{ channel: { id: string; name: string; isPrivate: boolean; isMember: boolean; isDirectMessage: boolean; isMpim: boolean; isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; contextTeamId: string | null; connectedTeamIds: string[] | null; sharedTeamIds: string[] | null; topic: string; purpose: string; numMembers: number | null; }; files: { id: string; name: string; title: string; mimetype: string; filetype: string; mode: string; size: number | null; originatingHuddleId: string; huddleTranscriptFileId: string; }[]; nextCursor: string | null; } & { receipt: SlackBotReceipt; }>; fileInfo(input: { channelId: string; fileId: string; parentFileId?: string; }): Promise<{ channel: { id: string; name: string; isPrivate: boolean; isMember: boolean; isDirectMessage: boolean; isMpim: boolean; isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; contextTeamId: string | null; connectedTeamIds: string[] | null; sharedTeamIds: string[] | null; topic: string; purpose: string; numMembers: number | null; }; file: { id: string; name: string; title: string; mimetype: string; filetype: string; mode: string; size: number | null; originatingHuddleId: string; huddleTranscriptFileId: string; }; } & { receipt: SlackBotReceipt; }>; fileContent(input: { channelId: string; fileId: string; parentFileId?: string; offset?: number; }): Promise<{ channel: { id: string; name: string; isPrivate: boolean; isMember: boolean; isDirectMessage: boolean; isMpim: boolean; isArchived: boolean; isShared: boolean; isExternallyShared: boolean; isOrgShared: boolean; isPendingExternallyShared: boolean; contextTeamId: string | null; connectedTeamIds: string[] | null; sharedTeamIds: string[] | null; topic: string; purpose: string; numMembers: number | null; }; file: { id: string; name: string; title: string; mimetype: string; filetype: string; mode: string; size: number | null; originatingHuddleId: string; huddleTranscriptFileId: string; }; contentType: string; offset: number; content: string; nextOffset: number | null; truncated: boolean; } & { receipt: SlackBotReceipt; }>; /** * Internal server-owned delivery only. Generic model-facing MCP callers do * not have a trustworthy durable logical-delivery identity and must never * reach this method with a caller-generated operation ID. */ postMessage(input: { operationId: string; channelId?: string; userId?: string; threadTimestamp?: string; text: string; blocks?: SlackMessageBlock[]; requireActiveNonSharedChannel?: boolean; }): Promise<{ channelId: string; timestamp: string; threadTimestamp: string | null; receipt: SlackBotReceipt; }>; updateMessage(input: { operationId: string; channelId: string; timestamp: string; text: string; blocks?: SlackMessageBlock[]; }): Promise<{ channelId: string; timestamp: string; receipt: SlackBotReceipt; }>; deleteMessage(input: { operationId: string; channelId: string; timestamp: string; }): Promise<{ channelId: string; timestamp: string; deleted: boolean; receipt: SlackBotReceipt; }>; private slackMessageExists; private reconcilePostMessage; private requireMemberChannel; private requireActiveNonSharedMemberChannel; private requireFileForChannel; private readPrivateFileText; private call; private withAudit; private headersFor; private headersForDestination; private fetchPrivateFile; private fetchPrivateFileOnce; private receipt; private completedPostResult; private completedDeleteResult; private postRequestDigest; private updateRequestDigest; private deleteRequestDigest; private deletePrincipal; private fileListPage; private fileListCursor; private recordAudit; private auditMetadata; } export declare function createOpenGeniSlackBotClient(deps: { db: Database; settings: Settings; slackFetch?: typeof fetch; authorizeProviderRequest?: SlackProviderAuthorization; }, resolved: Awaited>): OpenGeniSlackBotClient; export declare function createOpenGeniSlackBotInteractionClient(deps: { db: Database; settings: Settings; slackFetch?: typeof fetch; authorizeProviderRequest?: SlackProviderAuthorization; }, input: { accountId: string; workspaceId: string; connectionId: string; subjectId: string; sessionId?: string | null; }): Promise; export declare function sniffSlackReactionImageMime(bytes: Uint8Array): "image/png" | "image/jpeg" | "image/webp" | null; export declare function resolveSlackFilesListPage(input: { channelId: string; limit?: number; cursor?: string; }, context: SlackFilesCursorContext): SlackFilesListPage; export declare function createSlackFilesListCursor(input: { channelId: string; count: number; page: number; }, context: SlackFilesCursorContext): string; export declare function nextSlackFilesListPage(payload: SlackPayload, requested: SlackFilesListPage, returnedFileCount: number): number | null; export {};