@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 +21 -0
- package/README.md +92 -0
- package/dist/app.d.ts +56 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +56 -0
- package/dist/app.js.map +1 -0
- package/dist/cron.d.ts +12 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +15 -0
- package/dist/cron.js.map +1 -0
- package/dist/handler.d.ts +107 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +457 -0
- package/dist/handler.js.map +1 -0
- package/dist/handler.test.d.ts +2 -0
- package/dist/handler.test.d.ts.map +1 -0
- package/dist/handler.test.js +238 -0
- package/dist/handler.test.js.map +1 -0
- package/dist/hono.d.ts +20 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +21 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +61 -0
- package/dist/server.js.map +1 -0
- package/package.json +67 -0
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
|
package/dist/app.js.map
ADDED
|
@@ -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
|
package/dist/cron.js.map
ADDED
|
@@ -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"}
|