/** * Federation SPOKE endpoints, mounted INSIDE the dashboard's token gate (these * are owner actions — the dashboard token already proves the owner). The spoke * makes OUTBOUND calls to a hub; it never needs to expose anything inbound. * - POST /api/team/join-remote { hubUrl, inviteCode } * - GET /api/team/remote-roster * - POST /api/team/sync-remote * - POST /api/team/leave-remote { hubUrl, teamId } * * The long-lived syncToken is sent in the `Authorization: Bearer` header (never * in a URL, so it stays out of access/proxy logs). All hub calls have a timeout. * See docs/federation-design.md. */ import type { IncomingMessage, ServerResponse } from 'node:http'; import { type LiveBot } from '../services/team-roster.js'; import { type BotConfig } from '../bot-registry.js'; import { type Fetcher, type TeamGroupCreateResult, type TeamGroupOwnerTransferResult } from './federated-group-core.js'; export interface OwnerCandidate { unionId: string; name: string; } /** Injectable seams for the owner resolver (defaults hit Feishu via the bot's * own credentials). Lets the resolver's registry/iteration logic be unit-tested * without network. */ interface OwnerResolveDeps { configs?: () => BotConfig[]; /** Auth-only callers only need union_id membership and should not pay a * best-effort contact lookup just to decorate direct `on_` candidates. */ skipNames?: boolean; /** Ensure a usable Lark client exists for cfg. The DASHBOARD process has no bot * registry (it proxies to daemons), so getBotClient() would throw — register * the cfg on demand (it carries the app secret from bots.json). */ ensureClient?: (cfg: BotConfig) => void; resolveAllowed?: (larkAppId: string, allowed: string[]) => Promise; resolveUnion?: (larkAppId: string, openId: string) => Promise<{ unionId?: string; name?: string; }>; } /** Resolve this deployment's owner identity from bots.json `allowedUsers` using * each bot's OWN app credentials (no /pair, no shared pairings.json — immune to * the dataDir-split that broke /pair). Iterates ALL bots with non-empty * allowedUsers, resolves each independently, then aggregates and deduplicates by * union_id across all bots. The UI auto-binds when there's exactly one candidate, * else lets the owner pick. A bot failing (no scope / API error) is skipped so * one mis-config doesn't hide the rest. * * allowedUsers 支持三种格式: * - `on_xxx` (union_id) — 跨应用稳定 ID,直接使用,无需 API 解析(推荐) * - 邮箱 — 通过 bot 凭证查询 open_id 再转 union_id * - `ou_xxx` (open_id) — 仅对签发该 open_id 的同一应用有效;跨应用会报 99992361 */ export declare function resolveOwnerCandidatesFromAllowedUsers(d?: OwnerResolveDeps): Promise; /** 上报给团队看板的会话裁剪行所需的最小字段(来源 SessionRow)。 */ export interface TeamSessionRowLike { sessionId: string; botName?: string; cliId?: string; status?: string; title?: string; chatId: string; scope?: string; adopt?: boolean; lastMessageAt?: number; } /** Push this deployment's current bots to every joined hub. Best-effort. * 传 sessionsProvider 时顺带上报团队看板的会话裁剪行:hub 在 sync 响应里下发 * 该团队的协作群清单,按 chatId 过滤本地会话 POST 回 hub(活跃优先,截 200)。 */ export declare function syncAllMemberships(dataDir: string, fetcher?: Fetcher, live?: LiveBot[], sessionsProvider?: () => TeamSessionRowLike[]): Promise<{ synced: number; failed: number; }>; /** Persist a resolved owner as THIS deployment's identity, claim its unassigned * bots (no-steal: only bots without a manual owner), then push the new identity * to every joined hub. Shared verbatim by the interactive bind paths (/pair * consume, dashboard auto-bind) and the headless startup auto-bind, so the three * never drift apart. */ export declare function bindDeploymentOwnerAndClaim(dataDir: string, owner: { unionId?: string; name?: string; }, opts?: { fetcher?: Fetcher; live?: LiveBot[]; }): Promise<{ synced: number; failed: number; }>; /** Headless single-candidate auto-bind, for a standalone (non-team) deployment so * the owner identity gets set WITHOUT a manual dashboard click — the left-rail * avatar then shows, 拉群 can pull the operator in, and bots are claimed. Resolves * the owner from the bots' OWN allowedUsers (no /pair, immune to the dataDir-split * that /pair has). Only acts when there is exactly ONE candidate (unambiguous); a * multi-person deployment still needs the dashboard picker. Idempotent: a no-op * once bound (cheap local read, no network). */ export declare function autoBindOwnerIfUnambiguous(dataDir: string, opts?: { fetcher?: Fetcher; live?: LiveBot[]; ownerCandidates?: () => Promise; }): Promise<{ status: 'already_bound' | 'bound' | 'need_choice' | 'no_candidates'; owner?: OwnerCandidate; candidates?: OwnerCandidate[]; }>; export interface FederationSpokeDeps { dataDir?: string; fetcher?: Fetcher; /** Live daemon-registry bots (injected by dashboard.ts) — authoritative source * for THIS deployment's bots, so an empty/stale bots-info.json never hides * running bots from the team roster / federation sync. */ liveBots?: () => LiveBot[]; /** Injected by dashboard.ts — picks a local online creator + proxies to its * daemon's /api/groups/create (federated bots are added by larkAppId). */ createTeamGroup?: (args: { name: string; larkAppIds: string[]; ownerUnionIds?: string[]; transferOwnerUnionId?: string; }) => Promise; transferTeamGroupOwner?: (args: { creatorLarkAppId: string; chatId: string; transferOwnerUnionId: string; }) => Promise; /** Test seam: resolve owner candidates from allowedUsers (defaults to the real * Feishu-backed resolver). */ ownerCandidates?: () => Promise; } export declare function handleFederationSpokeApi(req: IncomingMessage, res: ServerResponse, url: URL, deps?: FederationSpokeDeps): Promise; export {}; //# sourceMappingURL=federation-spoke-api.d.ts.map