/** * Daemon-internal API — typed Route B server for `/__daemon/*`. * * Dispatch pipeline: * 1) `verifyDaemonRequest` checks HMAC + loopback + ts ±60s + nonce replay. * Reads the body stream EXACTLY ONCE and returns `bodyRaw`. * 2) `bodyRaw` is JSON-parsed (empty body → `undefined`); a parse failure * after a valid HMAC returns 400 `bad_json` without re-reading `req`. * 3) Dispatch matches `(method, path)` against a typed allowlist * endpoints — there is intentionally NO generic forward, so a daemon * can never use Route B as a path-shifting proxy. * * Settings-write also enforces the union_id owner gate: the body must * carry an `ownerUnionId` (`on_`-prefixed) that resolves to a candidate in * the global owner set, or the request returns 403 `owner_only`. * * The factory exposes both `handle(req,res,url)` (production wiring) and * `dispatchForTest(method, url, bodyRaw)` (skips HMAC for unit tests that * focus on route shape; full HMAC flow is covered by daemon-internal-auth). */ import type { IncomingMessage, ServerResponse } from 'node:http'; import { type ClockLike, type NonceStore } from './daemon-internal-auth.js'; import { type GroupsActionDeps, type HandlerResult } from './groups-action-helpers.js'; import { type ResolvedDashboardSettingsView, type SettingsWriteApplierDeps } from './settings-write-applier.js'; import { type SettingsOwnerResolverDeps } from './settings-owner-resolver.js'; export type SimpleHttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; /** Deps the dispatcher needs — all IO is injected. */ export interface DaemonInternalApiDeps { /** `.dashboard-secret` body (string used directly as HMAC key — same convention as `/__cli/rotate`). */ secret: string; /** Override for tests to inject a fake clock-aware nonce store. Production uses `createNonceStore()`. */ nonceStore?: NonceStore; /** Override for tests to advance time deterministically inside verifyDaemonRequest. */ clock?: ClockLike; getSessions: () => unknown[]; getSchedules: () => unknown[]; resolveDashboardSettings: () => ResolvedDashboardSettingsView; /** Returns `{ chats, bots }`; groups model requires both for missingOnly accuracy. */ buildGroupsMatrix: () => Promise<{ chats: unknown[]; bots: unknown[]; }>; settingsApplierDeps: SettingsWriteApplierDeps; groupsActionDeps: GroupsActionDeps; proxyToDaemon: (larkAppId: string, daemonPath: string, init: RequestInit) => Promise; ownerOf: (sessionId: string) => string | undefined; /** Companion of `ownerOf` — tells "row missing" apart from "legacy row". * Same rationale as `scheduleExists`. */ sessionExists: (sessionId: string) => boolean; scheduleOwnerOf: (id: string) => string | undefined; /** True iff a schedule row with this id exists at all in the aggregator, * regardless of its `larkAppId` presence. Used by the Route B write gate * to tell apart "legacy schedule (no owner field)" from "unknown id". */ scheduleExists: (id: string) => boolean; /** Override for unit tests; production omits and uses the real federation helper. */ settingsOwnerDeps?: SettingsOwnerResolverDeps; } export interface DispatchContext { bodyRaw: string; body: unknown; url: URL; /** * Authenticated caller's bot `larkAppId` — populated by `handle()` from * `verify.appId` (`daemon-internal-auth.ts:232`). Aggregated read routes * use this id for their default per-bot view; `?scope=global` explicitly * widens `/dashboard` list reads to the Bot Owner's deployment-wide view. * undefined only on the test seam (`dispatchForTest`) where the caller is * trusted to assert their own scope. */ callerAppId?: string; } /** * Pure dispatcher: matches `(method, path)` against the typed allowlist. * Returns `unknown_endpoint` (404) when no path matches, `method_not_allowed` * (405) when a path matches but the method does not, or hands off to the * matched handler. */ export declare function dispatchDaemonInternalRequest(method: string, url: URL, bodyRaw: string, deps: DaemonInternalApiDeps, callerAppId?: string): Promise; export interface DaemonInternalApi { /** Production entry point: verify HMAC, JSON-parse, dispatch, write response. */ handle(req: IncomingMessage, res: ServerResponse, url: URL): Promise; /** Test seam: bypass HMAC, exercise dispatch shape directly. `callerAppId` * emulates the authenticated bot id so read-scoping tests can drive the * per-bot filter without going through HMAC. */ dispatchForTest(method: string, url: URL, bodyRaw?: string, callerAppId?: string): Promise; } export declare function createDaemonInternalApi(deps: DaemonInternalApiDeps): DaemonInternalApi; //# sourceMappingURL=daemon-internal-api.d.ts.map