framework
Version:
The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.
118 lines • 7.96 kB
TypeScript
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { ProjectsProvider } from './projects.js';
import type { FrameworkEvent } from '../events.js';
import type { PreferencesStore } from '../registry.js';
import type { DiscordCredentialsStore } from '../discord-credentials.js';
import type { QuotaSource } from './quota.js';
import type { AutoPmReporter } from '../auto-pm.js';
import type { AddProjectResult, PreviewResult, PreviewStatus, StartRunKind, StartRunOptions, StartRunResult } from './types.js';
import type { ServeTarget } from '../preview.js';
import type { RunMeta } from '../store/index.js';
/** Wired by the daemon so `sendStart` can reach the daemon's own `startRun` closure. */
export type StartRunHandler = (prompt: string, kind: StartRunKind, options: StartRunOptions, projectId?: string) => StartRunResult | Promise<StartRunResult>;
/** Wired by the daemon so `sendAddProject` can install + register a repo (#433). */
export type AddProjectHandler = (path: string, directory: boolean) => AddProjectResult | Promise<AddProjectResult>;
/** Wired by the daemon so the Preview RPCs can serve/stop/report a project's app (#475). */
export interface PreviewHandlers {
/** `runId` serves that session's own worktree instead of the project's checkout (#797). */
start: (projectId?: string, targetId?: string, runId?: string) => PreviewResult | Promise<PreviewResult>;
/** List the servable apps (#651) for the Serve picker in a multi-package repo. */
targets: (projectId?: string, runId?: string) => ServeTarget[] | Promise<ServeTarget[]>;
stop: (projectId?: string, runId?: string) => void | Promise<void>;
status: (projectId?: string, runId?: string) => PreviewStatus | Promise<PreviewStatus>;
}
/** Resolve a run to its live event stream: the relay feeds `onEvents` from its own in-memory stream
* rather than a file on disk (#426), and the daemon feeds a run it is relaying from a device (#1067).
* Returns undefined when there is no in-memory stream, so `onEvents` falls back to tailing the log. */
export type EventsSource = (projectId: string, runId?: string) => AsyncIterable<FrameworkEvent> | undefined;
/** Look up the device a relayed run (#1067) executes on, or undefined for an ordinary local run. The
* daemon wires this from its live relayed-run map; a run-scoped RPC uses it to forward a remote run's
* read/steer/handoff to that device instead of resolving a (nonexistent) local checkout. */
export interface RemoteRuns {
target(runId: string | undefined): {
url: string;
token: string;
} | undefined;
/** A project's relayed run stubs (#1077), so `onRuns` can show a remote run in the list and re-open it after a reload. */
list(projectId: string): RunMeta[];
}
/**
* The Telefunc request context the mount provides. `sendStart` reads `startRun` from it;
* every project-keyed RPC reads `projects` (#427) — the daemon leaves it unset to use the
* global registry, the per-run foreground dashboard passes a single-project provider. The
* relay passes `eventsSource` (#426) so `onEvents` streams its in-memory run instead of a
* file, plus an empty `projects` so the file/registry RPCs return nothing on a public host.
*/
export interface DashboardContext {
startRun?: StartRunHandler;
addProject?: AddProjectHandler;
/** The Preview handler set (#475); the daemon wires it, other hosts leave it unset. */
preview?: PreviewHandlers;
projects?: ProjectsProvider;
eventsSource?: EventsSource;
/** The relayed-run lookup (#1067 slice 2): only the daemon wires it, so a run-scoped RPC can tell a
* local run from one running on a connected device and forward the call there. */
remote?: RemoteRuns;
/** The user-preferences store (#410). The daemon/foreground wire the real registry file;
* a public host (the relay) leaves it unset so `onPreferences`/`savePreferences` are inert. */
preferences?: PreferencesStore;
/** The quota source behind the usage panel (#533). The daemon wires a live poller;
* a public host (the relay) leaves it unset, so `onQuota` reports it has no reading. */
quota?: QuotaSource;
/** The Discord credentials store (#1095). The daemon wires one that also reloads its Discord
* services on a save; a public host leaves it unset, so nothing there is configurable. */
discord?: DiscordCredentialsStore;
/** What auto PM last decided (#1161). Only the daemon runs the sweep, so only it wires this. */
autoPm?: AutoPmReporter;
/**
* Run an auto PM sweep now rather than at the next interval (#1210). Same reason `autoPm` is
* daemon-only: the loop lives in that process, so nowhere else has one to fire. Resolves when
* the sweep does (#1433), so the trigger RPC can await it and return what it decided.
*/
autoPmSweep?: (opts?: {
drainOnly?: boolean;
}) => void | Promise<void>;
}
/**
* CSRF guard for the state-changing Telefunc calls. A browser attaches an `Origin`
* header to every cross-site request, so we reject any POST whose Origin is not this
* same server (or a loopback host) — otherwise a page on `evil.com` could `fetch()` the
* localhost dashboard and spawn/steer a run. An absent Origin means a non-browser caller
* (curl, the test suite) with no ambient session to abuse, so it passes. Lives here beside
* the mount, its only caller.
*/
export declare function isSameOriginRequest(req: IncomingMessage): boolean;
/**
* DNS-rebinding guard, the other half of the CSRF check above. A page on `evil.com` whose DNS
* re-answers as `127.0.0.1` is *same-origin* with this server as far as the browser is concerned,
* so its `fetch()` takes the passing branch of {@link isSameOriginRequest} — and every RPC behind
* the mount, `sendStart` included, is reachable from a page the user merely visited.
*
* The `Host` header is what still gives the attacker away: it carries the name the browser was
* asked for (`evil.com`), not the address it resolved to. So when we are bound to loopback, the
* only `Host` a real user's browser can send is a loopback one (or the bound address itself) —
* anything else is a rebound name and is rejected. An absent `Host` is rejected too when we are
* enforcing: HTTP/1.1 requires it, and every browser sends it.
*
* A non-loopback bind (`--host`, #1051) is reached by a hostname we cannot predict, so there is
* no allowlist to check against; that case gates behind the shared daemon token instead. Hosts
* that never pass a bind host at all (the relay, which serves a public domain) are unaffected.
*/
export declare function isExpectedHost(req: IncomingMessage, boundHost: string | undefined): boolean;
/**
* Mount the dashboard's Telefunc surface (#405) on the daemon's `node:http` server: one
* `serve()` handles both the RPCs and the Channel SSE stream at `/_telefunc`. Telefunc
* runs in the daemon process, so a `sendStart` telefunction can call the daemon's own
* `startRun` via the request context. The `context` is exactly what each telefunction
* reaches through {@link getContext} (see {@link DashboardContext}): the daemon wires the
* full set, the relay passes only an events source plus an empty projects provider. Cross-
* origin POSTs are rejected (CSRF: a page on evil.com must not steer or start a run), as are
* requests carrying someone else's `Host` when we are bound to loopback (DNS rebinding: the
* same page must not reach us by pointing its own name at `127.0.0.1`). Pass `opts.host` — the
* address the server is bound to — to enable that second check; a host serving a public domain
* (the relay) leaves it unset. Returns whether the request was Telefunc's.
*/
export declare function makeTelefuncMount(context?: DashboardContext, opts?: {
host?: string;
}): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
//# sourceMappingURL=telefunc-serve.d.ts.map