/** * Slack Agent Identity Service — one real Slack bot user per agent. * * Crewly Cloud (`/api/cloud/slack/*`) creates one Slack app per agent with * the owner's app configuration token, hands back an install link, and after * the owner's one consent click holds the agent's bot token. This service is * the OSS half: * * - talks to Cloud with the OSS install's Cloud access token; * - caches identities (bot user id, bot token) in * `~/.crewly/slack-agent-identities.json` (mode 0600) so posting works * without a Cloud round-trip; * - polls Cloud while any install is pending so a click on the link shows * up here within ~30 s; * - remembers where each install link was announced and which channels the * bot has been invited into, so the team-channel service does each once. * * Team channels use it to post replies as the agent (its own token) and to * resolve native `<@U…>` mentions. Everything degrades gracefully: with no * Cloud login or no identity, replies fall back to the cosmetic * username/icon override. * * @module services/slack/slack-agent-identity.service */ import type { SlackAgentIdentityRecord, SlackAgentIdentitiesFile, SlackCloudAgentConfig } from '../../types/slack.types.js'; /** The slice of CloudClientService this service needs. */ export interface IdentityCloudClient { isConnected(): boolean; getToken(): string | null; getCloudUrl(): string | null; } /** Constructor dependencies. */ export interface SlackAgentIdentityServiceDeps { cloud: IdentityCloudClient; /** The Slack workspace this instance serves; config tokens are per workspace. */ getWorkspaceId?: () => string | null; /** * This machine's Cloud instance id, so the orchestrator lookup picks * *our* `crewly-orc@` and not another machine's. */ getInstanceId?: () => string | null; storePath?: string; fetchImpl?: typeof fetch; now?: () => number; /** Timer override for tests. */ setInterval?: typeof setInterval; clearInterval?: typeof clearInterval; } /** Cloud's config-token status. */ export interface CloudConfigTokenStatus { configured: boolean; status?: 'ok' | 'invalid'; expiresAt?: string; /** Workspace the stored token belongs to. */ slackTeamId?: string; lastError?: string; } /** Cloud's `/slack/status` payload. */ export interface CloudSlackStatus { enabled: boolean; configToken: CloudConfigTokenStatus; agents: { total: number; installed: number; pending: number; }; } /** Error from Cloud with the stable code it returned. */ export declare class SlackIdentityCloudError extends Error { readonly status: number; readonly code: string; constructor(status: number, code: string, message: string); } /** * Service — see module docs. */ export declare class SlackAgentIdentityService { private readonly logger; private readonly deps; private readonly storePath; private readonly fetchImpl; private store; private loading; private pollTimer; private pollingSince; private readonly listeners; constructor(deps: SlackAgentIdentityServiceDeps); /** * Whether Cloud can be reached (logged in with a token). * * @returns True when the OSS install is connected to Crewly Cloud */ isAvailable(): boolean; /** * Register a callback for the moment an identity becomes installed * (bot user id + token known). The team-channel service uses it to invite * the new bot into its channels. * * @param listener - Called with the installed record * @returns Unsubscribe */ onInstalled(listener: (record: SlackAgentIdentityRecord) => void): () => void; /** * Cloud-side status: whether the feature is enabled there, whether this * account has a config token, and agent counts. * * @returns The status * @throws {SlackIdentityCloudError} on a Cloud failure */ getCloudStatus(): Promise; /** * Store the owner's Slack app configuration token on Cloud (validated * there by rotating it once). * * @param token - Config access token (may be empty/expired) * @param refreshToken - Config refresh token (required) * @returns The stored token's status */ setConfigToken(token: string, refreshToken: string): Promise; /** * Remove the config token on Cloud. * * @returns True when one existed */ deleteConfigToken(): Promise; /** `?slackTeamId=…` for the workspace this instance serves, or ''. */ private workspaceQuery; /** * Ask Cloud for an identity for an agent (idempotent). Returns the cached * record updated with Cloud's answer, including `installUrl` while the * install is pending. * * @param agentSession - Stable agent id * @param displayName - Name the bot will carry in Slack * @param description - Optional app description * @returns The record */ provision(agentSession: string, displayName: string, description?: string): Promise; /** * Uninstall an agent's bot from the workspace, keeping its app (Cloud * frees the Slack app slot and hands back a fresh install link). * * @param agentSession - The agent * @returns The record afterwards */ uninstall(agentSession: string): Promise; /** * Pull every identity (with tokens) from Cloud into the local cache. * Newly installed ones fire {@link onInstalled}. * * @returns All records after the merge */ refreshFromCloud(): Promise; /** * Merge the installed identities handed down with the Cloud-owned Slack * config (`GET /api/cloud/slack/config` → `agents`). Each entry is a fully * installed bot user, so it lands as `installed` and fires * {@link onInstalled} the first time it is seen — the team-channel * service then invites the bot into its channels. Idempotent. * * @param agents - Agents from the Cloud config * @returns Number of records that became installed by this call */ applyCloudConfig(agents: SlackCloudAgentConfig[]): Promise; /** * Delete an agent's Slack app on Cloud and forget it locally. * * @param agentSession - The agent * @returns True when a record existed on Cloud */ remove(agentSession: string): Promise; /** * All cached identities (tokens included — callers must not log them). * * @returns Copies */ list(): Promise; /** * Cached identity for an agent, if any. Synchronous read of the loaded * cache (call {@link list} or any Cloud method first to load it). * * @param agentSession - The agent * @returns The record or null */ get(agentSession: string): SlackAgentIdentityRecord | null; /** * Installed identity (bot user + token) for an agent, or null. * * @param agentSession - The agent * @returns `{ botUserId, botToken }` when installed */ getInstalled(agentSession: string): { botUserId: string; botToken: string; } | null; /** * The orchestrator's record, looked up by its local name. * * Cloud registers the orchestrator under a per-instance session * (`crewly-orc@`) so two machines on one account do not share an * app, while everything on this side calls it `crewly-orc`. Only this * lookup needs to know both spellings. * * @param agentSession - The session asked for * @returns The stored record, or null when this is not the orchestrator */ private getOrchestratorRecord; /** * Agent session for a Slack bot user id (native mention resolution). * * @param botUserId - Slack user id * @returns The agent session or null */ findByBotUserId(botUserId: string): string | null; /** * Mark that an install link was announced in a channel / the bot was * invited into a channel, so it is not repeated. * * @param agentSession - The agent * @param patch - Channel ids to add to either list */ markChannel(agentSession: string, patch: { announcedIn?: string; invitedTo?: string; }): Promise; /** Load the cache once. */ load(): Promise; /** Stop timers (shutdown / tests). */ stop(): void; private mergeView; private save; private ensurePolling; private stopPolling; /** One poll: refresh from Cloud; give up after the max age. */ pollTick(): Promise; private now; private cloudRequest; } /** * Install the process-wide instance (composition root). * * @param service - The service or null */ export declare function setSlackAgentIdentityService(service: SlackAgentIdentityService | null): void; /** * The process-wide instance, or null before wiring. * * @returns The service or null */ export declare function getSlackAgentIdentityService(): SlackAgentIdentityService | null; //# sourceMappingURL=slack-agent-identity.service.d.ts.map