@intentface/latch-server 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Intentface
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @intentface/latch-server
2
+
3
+ The mountable HTTP surface for a Latch runtime — **one framework-agnostic Web handler** you mount at a single route in any Web-standard host (TanStack Start, Next, Bun, Deno, serverless).
4
+
5
+ ## What it does
6
+
7
+ `createLatchHandler({ runtime, connections, resolveContext })` returns a `(Request) => Response` handler that exposes the runtime over HTTP:
8
+
9
+ - `POST /agents/:agent/:id` — run a chat turn (streams UIMessage SSE).
10
+ - `POST /agents/:agent/:id/approvals` — submit HITL approval decisions (resumes the turn).
11
+ - `POST /agents/:agent/:id/tool-result` — return a client-handled tool result (e.g. `ask_question`).
12
+ - `GET /agents/:agent/:id/history`, `GET /chats`, `GET /runs` — projections for the UI.
13
+ - `GET/POST/DELETE /schedules`, `GET /schedules/:id`, `POST /schedules/:id/run-now` — manage scheduled agents; `POST /cron/run` — the host-guarded tick.
14
+ - `GET /connections`, `…/:name/authorize`, `…/:name/callback`, `…/:name/disconnect`, `…/test` — generic MCP connection OAuth + status.
15
+
16
+ `resolveContext(request)` turns a request into your `Principal` (bring-your-own auth). A Hono adapter is exported from the `/hono` subpath.
17
+
18
+ ## OAuth callback identity
19
+
20
+ The connection callback is the one route that does NOT authenticate via
21
+ `resolveContext`. An IdP redirect arrives at a moment you don't control, so a
22
+ degraded session there must never decide where credentials land. Instead,
23
+ `authorize` pins the resolved principal into the OAuth flow state, and the
24
+ callback rebuilds it from that pin:
25
+
26
+ ```ts
27
+ createLatchHandler<Principal>({
28
+ runtime, connections, resolveContext,
29
+ // Validate/re-hydrate the pinned identity on the callback (e.g. re-check
30
+ // org membership). Null → 401. Default: trust the pin as-is.
31
+ reconstructPrincipal: async (identity) => validateMembership(identity),
32
+ oauthFlowMaxAgeMs: 30 * 60_000, // authorize → callback TTL (the default)
33
+ });
34
+ ```
35
+
36
+ The callback does **not** resume a waiting turn — the browser must not sit on a
37
+ blank page for a whole run. Resuming is the client's job after the redirect:
38
+ find the pending `connect_<name>` approval in the chat's history, submit it to
39
+ `POST /agents/:agent/:id/approvals`, and read the continuation stream that
40
+ returns. Two details are easy to miss: the continuation reuses the **last
41
+ assistant message's id** and streams only its new parts, so seed your stream
42
+ reader with a clone of that message or the answer renders as a second,
43
+ half-empty one; and mirror the decision locally first, or the approval card
44
+ stays on screen for the whole run. Reconcile against server history at the end.
45
+
46
+ A callback whose pin is older than `oauthFlowMaxAgeMs` redirects with
47
+ `?error=flow_expired` (the user just re-runs connect). The pin is
48
+ tamper-evident — the full state string is exact-matched against the copy the
49
+ flow stored at `start()` — and principals are IDs-only by contract
50
+ (see `@intentface/latch-core`'s `principal.ts`), so nothing sensitive rides
51
+ in the URL.
52
+
53
+ ## Scheduling over HTTP
54
+
55
+ `POST /schedules` takes everything `runtime.schedule()` does, so the mounted
56
+ handler is not a reduced version of the contract:
57
+
58
+ ```jsonc
59
+ {
60
+ "agent": "briefer", "cron": "0 8 * * 1-5", "timezone": "Europe/Helsinki",
61
+ "prompt": "morning brief",
62
+ // Opaque to core — only your `runSchedule` hook reads it (a Slack thread, a DM).
63
+ "delivery": { "kind": "slack", "appId": "A1", "channel": "C1" },
64
+ // "skip" (default) = don't fire onto a predecessor still waiting on a human;
65
+ // "fire" = always fire. An unknown value is a 400, never a silent default.
66
+ "onParked": "fire"
67
+ }
68
+ ```
69
+
70
+ `POST /schedules/:id/run-now` arms a schedule for the next tick rather than
71
+ firing it inline — it then goes through the ordinary `runDue` path (same claim,
72
+ same occurrence consume, same delivery), so "run now" cannot drift from what
73
+ the cron actually does. `GET /runs` deliberately omits each run's `identity`
74
+ (the serialized principal): it is durable server state and a type you are
75
+ invited to extend, so it never enters a browser payload — read it server-side
76
+ via `runtime.listRuns` if you need it.
77
+
78
+ ## Usage
79
+
80
+ ```ts
81
+ import { createLatchHandler } from "@intentface/latch-server";
82
+
83
+ const handler = createLatchHandler<Principal>({
84
+ runtime, connections,
85
+ resolveContext: (req) => getPrincipalFromHeaders(req.headers),
86
+ });
87
+ // mount at /api/latch/* — every Latch operation flows through this one handler.
88
+ ```
89
+
90
+ ## Where it fits
91
+
92
+ The HTTP tier over `@intentface/latch-core`. The routes are a convenience over the real contract — the Runtime operations — which you can still call directly from your own routes or non-HTTP triggers (cron, queues, chat channels).
package/dist/app.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { type AgentFactory, type ContextDefinition, type Runtime, type RuntimeConfig, type Scheduler, type StorageAdapter } from "@intentface/latch-core";
2
+ import { type ModelCatalog } from "@intentface/latch-core/models";
3
+ import { type LatchHandler, type LatchHandlerConfig } from "./handler.js";
4
+ /**
5
+ * `createLatchApp` — the composition root you'd otherwise write by hand.
6
+ *
7
+ * Given storage, agents and how to resolve the caller, it wires the runtime
8
+ * with the defaults every host wants (durability on, croner cron evaluator,
9
+ * the models.dev catalog with pricing / reasoning / web-tool / prompt-caching
10
+ * hooks, the default harness tools), mounts the HTTP handler over it, and —
11
+ * on a long-running process — drives `runtime.cron()` on an interval. Every
12
+ * default is overridable through `runtime` / `handler`; anything a host wants
13
+ * beyond the defaults (connections, memory, telemetry) goes in the same way.
14
+ *
15
+ * A hello-world is the storage adapter plus one agent; see `apps/example`.
16
+ */
17
+ export interface LatchAppConfig<P, RC = unknown> {
18
+ /** The storage adapter (e.g. `createSqliteAdapter(db, { owner })`). */
19
+ storage: StorageAdapter<P>;
20
+ /**
21
+ * The agent registry — or a function of the loaded model catalog, so agents
22
+ * can pick their model from `models.modelFor(...)` without loading the
23
+ * catalog themselves.
24
+ */
25
+ agents: Record<string, AgentFactory<P, RC>> | ((models: ModelCatalog) => Record<string, AgentFactory<P, RC>>);
26
+ /** Your auth: request → Principal (null → 401). */
27
+ resolveContext: LatchHandlerConfig<P>["resolveContext"];
28
+ /** Per-turn runtime context (default: `{}`). */
29
+ context?: ContextDefinition<P, RC>;
30
+ /** A preloaded model catalog (default: `loadModelCatalog()`). */
31
+ models?: ModelCatalog;
32
+ /** Per-principal cache-key suffix for memory-scoped prompt caching (typically the owner key). */
33
+ principalKey?: (principal: P) => string;
34
+ /** Anything else on the runtime — or overrides of the defaults (listed last wins). */
35
+ runtime?: Partial<Omit<RuntimeConfig<P, RC>, "storage" | "agents" | "context">>;
36
+ /** Handler options beyond `runtime` / `resolveContext` (basePath, connections, cronSecret, …). */
37
+ handler?: Partial<Omit<LatchHandlerConfig<P>, "runtime" | "resolveContext">>;
38
+ /**
39
+ * In-process cron cadence (ms) driving `runtime.cron()`. Default 30s on a
40
+ * long-running process; `false` to disable. Auto-disabled on serverless
41
+ * (VERCEL / AWS_LAMBDA_FUNCTION_NAME set) — point a platform cron at
42
+ * `POST <basePath>/cron/run` there instead.
43
+ */
44
+ cronIntervalMs?: number | false;
45
+ }
46
+ export interface LatchApp<P> {
47
+ runtime: Runtime<P>;
48
+ handler: LatchHandler;
49
+ models: ModelCatalog;
50
+ /** The in-process cron driver, if one is running. */
51
+ scheduler?: Scheduler;
52
+ /** Stop the in-process cron (idempotent). */
53
+ stop(): Promise<void>;
54
+ }
55
+ export declare function createLatchApp<P, RC = unknown>(cfg: LatchAppConfig<P, RC>): Promise<LatchApp<P>>;
56
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAsC,KAAK,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAEtG,OAAO,EAAsB,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAG9F;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,EAAE,EAAE,GAAG,OAAO;IAC7C,uEAAuE;IACvE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IAC3B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9G,mDAAmD;IACnD,cAAc,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC;IACxD,gDAAgD;IAChD,OAAO,CAAC,EAAE,iBAAiB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACnC,iEAAiE;IACjE,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,iGAAiG;IACjG,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,sFAAsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC;IAChF,kGAAkG;IAClG,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,SAAS,GAAG,gBAAgB,CAAC,CAAC,CAAC;IAC7E;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACjC;AAED,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,qDAAqD;IACrD,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,6CAA6C;IAC7C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAQD,wBAAsB,cAAc,CAAC,CAAC,EAAE,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAiDtG"}
package/dist/app.js ADDED
@@ -0,0 +1,56 @@
1
+ import { createIntervalScheduler, createRuntime, defineContext, } from "@intentface/latch-core";
2
+ import { loadModelCatalog, providerDefaults } from "@intentface/latch-core/models";
3
+ import { defaultHarnessTools } from "@intentface/latch-core/harness";
4
+ import { createLatchHandler } from "./handler.js";
5
+ import { createCronerEvaluator } from "./cron.js";
6
+ /** Env access without Node types: this package is Web-standard and may run where `process` is absent. */
7
+ const env = (name) => globalThis.process?.env?.[name];
8
+ const SERVERLESS = !!(env("VERCEL") || env("AWS_LAMBDA_FUNCTION_NAME"));
9
+ export async function createLatchApp(cfg) {
10
+ const models = cfg.models ?? (await loadModelCatalog());
11
+ const agents = typeof cfg.agents === "function" ? cfg.agents(models) : cfg.agents;
12
+ const runtime = createRuntime({
13
+ storage: cfg.storage,
14
+ context: cfg.context ?? defineContext({ build: () => ({}) }),
15
+ agents,
16
+ // Resume-after-crash on by default: it costs a lease heartbeat per turn
17
+ // and is what makes `cron()`'s sweep half do anything.
18
+ durability: { enabled: true },
19
+ cron: createCronerEvaluator(),
20
+ ...providerDefaults(models, { principalKey: cfg.principalKey }),
21
+ harnessTools: defaultHarnessTools(),
22
+ ...cfg.runtime,
23
+ });
24
+ const handler = createLatchHandler({
25
+ runtime,
26
+ resolveContext: cfg.resolveContext,
27
+ // The system-wide cron route is unauthenticated — enabled only when a
28
+ // secret is set (the handler 404s it otherwise).
29
+ cronSecret: env("LATCH_CRON_SECRET"),
30
+ ...cfg.handler,
31
+ });
32
+ let scheduler;
33
+ const interval = cfg.cronIntervalMs ?? (SERVERLESS ? false : 30_000);
34
+ if (interval !== false) {
35
+ scheduler = createIntervalScheduler({
36
+ intervalMs: interval,
37
+ onTick: async () => {
38
+ const r = await runtime.cron();
39
+ for (const e of r.errors)
40
+ console.error("[latch] cron phase failed:", e);
41
+ },
42
+ onError: (e) => console.error("[latch] cron failed:", e),
43
+ });
44
+ scheduler.start();
45
+ }
46
+ return {
47
+ runtime,
48
+ handler,
49
+ models,
50
+ scheduler,
51
+ stop: async () => {
52
+ await scheduler?.stop();
53
+ },
54
+ };
55
+ }
56
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,aAAa,EACb,aAAa,GAOd,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAqB,MAAM,+BAA+B,CAAC;AACtG,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAA8C,MAAM,cAAc,CAAC;AAC9F,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAuDlD,yGAAyG;AACzG,MAAM,GAAG,GAAG,CAAC,IAAY,EAAsB,EAAE,CAC9C,UAAyE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;AAElG,MAAM,UAAU,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,0BAA0B,CAAC,CAAC,CAAC;AAExE,MAAM,CAAC,KAAK,UAAU,cAAc,CAAkB,GAA0B;IAC9E,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,MAAM,gBAAgB,EAAE,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;IAElF,MAAM,OAAO,GAAG,aAAa,CAAQ;QACnC,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,aAAa,CAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAO,EAAE,CAAC;QACzE,MAAM;QACN,wEAAwE;QACxE,uDAAuD;QACvD,UAAU,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;QAC7B,IAAI,EAAE,qBAAqB,EAAE;QAC7B,GAAG,gBAAgB,CAAQ,MAAM,EAAE,EAAE,YAAY,EAAE,GAAG,CAAC,YAAY,EAAE,CAAC;QACtE,YAAY,EAAE,mBAAmB,EAAE;QACnC,GAAG,GAAG,CAAC,OAAO;KACf,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,kBAAkB,CAAI;QACpC,OAAO;QACP,cAAc,EAAE,GAAG,CAAC,cAAc;QAClC,sEAAsE;QACtE,iDAAiD;QACjD,UAAU,EAAE,GAAG,CAAC,mBAAmB,CAAC;QACpC,GAAG,GAAG,CAAC,OAAO;KACf,CAAC,CAAC;IAEH,IAAI,SAAgC,CAAC;IACrC,MAAM,QAAQ,GAAG,GAAG,CAAC,cAAc,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACrE,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;QACvB,SAAS,GAAG,uBAAuB,CAAC;YAClC,UAAU,EAAE,QAAQ;YACpB,MAAM,EAAE,KAAK,IAAI,EAAE;gBACjB,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;gBAC/B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM;oBAAE,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,CAAC,CAAC,CAAC;YAC3E,CAAC;YACD,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,CAAC,CAAC;SACzD,CAAC,CAAC;QACH,SAAS,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;IAED,OAAO;QACL,OAAO;QACP,OAAO;QACP,MAAM;QACN,SAAS;QACT,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,MAAM,SAAS,EAAE,IAAI,EAAE,CAAC;QAC1B,CAAC;KACF,CAAC;AACJ,CAAC"}
package/dist/cron.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The croner-backed next-fire evaluator for scheduled agents — pass as the
3
+ * runtime config's `cron`. One implementation so hosts stop copy-pasting the
4
+ * same three lines (and drift on timezone handling).
5
+ *
6
+ * Expressions are interpreted in the schedule's IANA timezone so "9am daily"
7
+ * means the user's 9am; defaults to UTC when none is set. Returns the next
8
+ * fire time (epoch ms) strictly after `afterMs`, or null when the expression
9
+ * never fires again.
10
+ */
11
+ export declare function createCronerEvaluator(): (expr: string, afterMs: number, timezone?: string) => number | null;
12
+ //# sourceMappingURL=cron.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../src/cron.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,IAAI,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,KACd,MAAM,GAAG,IAAI,CAGjB"}
package/dist/cron.js ADDED
@@ -0,0 +1,15 @@
1
+ import { Cron } from "croner";
2
+ /**
3
+ * The croner-backed next-fire evaluator for scheduled agents — pass as the
4
+ * runtime config's `cron`. One implementation so hosts stop copy-pasting the
5
+ * same three lines (and drift on timezone handling).
6
+ *
7
+ * Expressions are interpreted in the schedule's IANA timezone so "9am daily"
8
+ * means the user's 9am; defaults to UTC when none is set. Returns the next
9
+ * fire time (epoch ms) strictly after `afterMs`, or null when the expression
10
+ * never fires again.
11
+ */
12
+ export function createCronerEvaluator() {
13
+ return (expr, afterMs, timezone) => new Cron(expr, { timezone: timezone || "UTC" }).nextRun(new Date(afterMs))?.getTime() ?? null;
14
+ }
15
+ //# sourceMappingURL=cron.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.js","sourceRoot":"","sources":["../src/cron.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB;IAKnC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,CACjC,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,QAAQ,IAAI,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,IAAI,CAAC;AAClG,CAAC"}
@@ -0,0 +1,107 @@
1
+ import type { Runtime } from "@intentface/latch-core";
2
+ import type { McpConnections } from "@intentface/latch-mcp";
3
+ /**
4
+ * The mountable Latch HTTP surface — one framework-agnostic Web handler.
5
+ *
6
+ * `createLatchHandler(config)` returns `(request) => Promise<Response | undefined>`
7
+ * (`undefined` = "not one of my routes", so the host can 404). Mount it once and
8
+ * it serves the whole runtime: chat streaming, approvals, chat/run lists,
9
+ * history, and the *generic* connection OAuth (authorize/callback) + status/
10
+ * test/disconnect for any MCP connection in the registry. No per-server routes.
11
+ *
12
+ * Auth is BYO: `resolveContext(request)` returns the `Principal` (or null →
13
+ * 401). The core never authenticates; everything is scoped by that principal.
14
+ *
15
+ * Routes (relative to `basePath`):
16
+ * POST agents/:agent/:id run a turn (streams a UIMessage Response)
17
+ * POST agents/:agent/:id/approvals submit tool-approval decisions, continue
18
+ * GET chats list the tenant's chats
19
+ * GET chats/:id client-projected history for one chat
20
+ * GET runs list the tenant's runs (usage/cost)
21
+ * GET agents the registered agents (for a picker)
22
+ * GET schedules list the tenant's scheduled agents
23
+ * GET schedules/:id one schedule (404 if not the tenant's)
24
+ * POST schedules create one ({agent, cron, prompt,
25
+ * timezone?, delivery?, onParked?})
26
+ * POST schedules/:id/run-now make it due on the next tick
27
+ * DELETE schedules/:id delete one
28
+ * POST cron/run fire ALL due schedules (host-guarded, no user auth)
29
+ * GET connections per-connection status for the tenant
30
+ * GET connections/:name/authorize begin OAuth → 302 to the IdP
31
+ * GET connections/:name/callback finish OAuth → 302 back (see below)
32
+ * POST connections/:name/disconnect drop the tenant's stored credentials
33
+ * POST connections/:name/test open the connection, list its tools
34
+ */
35
+ export interface LatchHandlerConfig<P> {
36
+ /**
37
+ * The runtime, or a per-principal resolver. A resolver enables DB-per-tenant
38
+ * isolation (a separate runtime/DB per tenant, e.g. one SQLite file per
39
+ * workspace) — it's called after `resolveContext` yields the principal. A
40
+ * single `Runtime` value fits column-scoped (`owner`) tenancy. Note: `POST
41
+ * cron/run` (no principal) requires a static runtime OR a `cronRun` tick in
42
+ * this config (the resolver-based setup's system-wide sweep).
43
+ */
44
+ runtime: Runtime<P> | ((principal: P) => Runtime<P> | Promise<Runtime<P>>);
45
+ /**
46
+ * Map a request to a Principal (your existing auth). Null → 401.
47
+ *
48
+ * NEVER consulted for identity on the OAuth callback: every flow this
49
+ * handler starts pins the resolved principal into the flow state, and the
50
+ * callback rebuilds identity from that pin via `reconstructPrincipal`, so a
51
+ * degraded session at redirect time can never change where credentials land.
52
+ * A callback whose state carries no pin is rejected as expired.
53
+ */
54
+ resolveContext: (request: Request) => P | null | Promise<P | null>;
55
+ /**
56
+ * Rebuild a Principal from the identity pinned into the OAuth flow state at
57
+ * `authorize` time (the mirror of the runtime's `reconstructPrincipal` for
58
+ * resume/cron). Called on the OAuth callback INSTEAD of `resolveContext` —
59
+ * the pinned identity is the source of truth; the live session is not
60
+ * consulted. Override to validate (e.g. re-check org membership) or
61
+ * re-hydrate; return null to reject the callback (401). Default: cast.
62
+ */
63
+ reconstructPrincipal?: (identity: unknown, request: Request) => P | null | Promise<P | null>;
64
+ /**
65
+ * Maximum age of an OAuth flow, from `authorize` to `callback`. A callback
66
+ * whose pinned flow state is older redirects with `?error=flow_expired`.
67
+ * Default: 30 minutes.
68
+ */
69
+ oauthFlowMaxAgeMs?: number;
70
+ /**
71
+ * Connection registry (enables the connection routes), or a per-principal
72
+ * resolver — the latter lets a consumer build a tenant-scoped registry (e.g.
73
+ * a workspace's installed connectors) after the principal is known.
74
+ */
75
+ connections?: McpConnections<P> | ((principal: P) => McpConnections<P> | Promise<McpConnections<P>>);
76
+ /**
77
+ * Called after a connection is successfully authorized (both the IdP callback
78
+ * and the already-authorized short-circuit in `authorize`). A consumer can use
79
+ * it to flip its own catalog/connector state to active.
80
+ */
81
+ onConnected?: (principal: P, connectionName: string) => void | Promise<void>;
82
+ /**
83
+ * Query param appended to the post-callback redirect signalling which
84
+ * connection just connected (`?<connectedParam>=<name>`). Default "connected".
85
+ */
86
+ connectedParam?: string;
87
+ /** Path prefix the handler is mounted under (e.g. "/api/latch"). Default "/". */
88
+ basePath?: string;
89
+ /**
90
+ * Shared secret guarding `POST /cron/run` (the system-wide schedule tick).
91
+ * When set, the request must send it as `x-latch-cron-secret`. That route is
92
+ * NOT user-authed — it fires all tenants' due schedules — so guard it at the
93
+ * host (cron secret / network). Omit to disable the route entirely.
94
+ */
95
+ cronSecret?: string;
96
+ /**
97
+ * The system-wide tick `POST /cron/run` fires. Required when `runtime` is a
98
+ * resolver (DB-per-tenant): no principal exists on this route, so the host
99
+ * must supply the tick that covers its tenants (each `runtime.cron()`). With
100
+ * a static runtime it defaults to `runtime.cron()` — due schedules fired,
101
+ * stalled runs reaped and resumed, in one call.
102
+ */
103
+ cronRun?: () => Promise<unknown>;
104
+ }
105
+ export type LatchHandler = (request: Request) => Promise<Response | undefined>;
106
+ export declare function createLatchHandler<P>(config: LatchHandlerConfig<P>): LatchHandler;
107
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgC,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC;;;;;;;OAOG;IACH,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3E;;;;;;;;OAQG;IACH,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACnE;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,CACrB,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,OAAO,KACb,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAClC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EACR,cAAc,CAAC,CAAC,CAAC,GACjB,CAAC,CAAC,SAAS,EAAE,CAAC,KAAK,cAAc,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvE;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,MAAM,YAAY,GAAG,CACzB,OAAO,EAAE,OAAO,KACb,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;AA2GnC,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC5B,YAAY,CAuYd"}