@devosurf/tesser-server 0.1.0-alpha.0

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.
Files changed (43) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +18 -0
  3. package/bin/tesser-server.mjs +2 -0
  4. package/dist/main.js +6296 -0
  5. package/dist/main.js.map +7 -0
  6. package/package.json +42 -0
  7. package/src/broker/broker.ts +332 -0
  8. package/src/broker/connect.ts +224 -0
  9. package/src/broker/connections.ts +278 -0
  10. package/src/broker/crypto.ts +39 -0
  11. package/src/broker/masking.ts +32 -0
  12. package/src/broker/oauth.ts +170 -0
  13. package/src/config.ts +128 -0
  14. package/src/db/db.ts +114 -0
  15. package/src/db/migrate.ts +35 -0
  16. package/src/db/migrations.ts +302 -0
  17. package/src/engine/executor.ts +536 -0
  18. package/src/engine/runs.ts +83 -0
  19. package/src/engine/signals.ts +18 -0
  20. package/src/engine/types.ts +53 -0
  21. package/src/events/fanout.ts +73 -0
  22. package/src/gitsync/build.ts +102 -0
  23. package/src/gitsync/deploy-keys.ts +59 -0
  24. package/src/gitsync/reconciler.ts +429 -0
  25. package/src/http/api.ts +425 -0
  26. package/src/http/app.ts +33 -0
  27. package/src/http/connect-view.ts +290 -0
  28. package/src/http/connect.ts +351 -0
  29. package/src/http/ingress.ts +204 -0
  30. package/src/http/status.ts +171 -0
  31. package/src/http/tokens.ts +46 -0
  32. package/src/index.ts +20 -0
  33. package/src/main.ts +26 -0
  34. package/src/queue/queue.ts +133 -0
  35. package/src/queue/worker.ts +85 -0
  36. package/src/registry/loader.ts +41 -0
  37. package/src/scheduler/cron.ts +115 -0
  38. package/src/scheduler/reaper.ts +105 -0
  39. package/src/server.ts +162 -0
  40. package/src/triggers/ingress.ts +154 -0
  41. package/src/triggers/poll.ts +167 -0
  42. package/src/triggers/registrar.ts +274 -0
  43. package/src/triggers/shared.ts +188 -0
@@ -0,0 +1,425 @@
1
+ // Control-plane API (ADR-0007: the CLI is a thin shell over this). JSON in/out,
2
+ // bearer-token auth, deterministic error shapes: { error: { code, message } }.
3
+
4
+ import { Hono } from "hono";
5
+ import type { Db } from "../db/db.js";
6
+ import type { Broker } from "../broker/broker.js";
7
+ import {
8
+ computeMissingRequirements,
9
+ connectLinkStatus,
10
+ getConnectLink,
11
+ mintConnectLink,
12
+ } from "../broker/connect.js";
13
+ import { createRun, deliverSignal, cancelRun } from "../engine/runs.js";
14
+ import { enqueue } from "../queue/queue.js";
15
+ import { mintToken, verifyToken } from "./tokens.js";
16
+ import { runtimeStatus, type RuntimeStatusDeps } from "./status.js";
17
+ import {
18
+ generateDeployKeyPair,
19
+ hashWebhookSetupToken,
20
+ mintWebhookSetupToken,
21
+ mintWebhookSigningSecret,
22
+ } from "../gitsync/deploy-keys.js";
23
+
24
+ export interface ApiEnv {
25
+ Variables: { workspaceId: string };
26
+ }
27
+
28
+ export interface ApiDeps extends RuntimeStatusDeps {
29
+ broker: Broker;
30
+ /** Filled by git-sync (task: reconciler); returns latest deploy state for a project. */
31
+ deployStatus?: (projectId: string, env: string) => Promise<unknown>;
32
+ }
33
+
34
+ const err = (code: string, message: string) => ({ error: { code, message } });
35
+
36
+ type ProjectBootstrap = {
37
+ deployKeyPublic: string;
38
+ webhookSetupUrl: string;
39
+ };
40
+
41
+ async function ensureProjectBootstrap(deps: ApiDeps, workspaceId: string, projectId: string): Promise<ProjectBootstrap> {
42
+ const { rows } = await deps.db.query<{
43
+ deploy_key_public: string | null;
44
+ deploy_key_private_cipher: string | null;
45
+ push_webhook_secret_cipher: string | null;
46
+ }>(
47
+ `SELECT deploy_key_public, deploy_key_private_cipher, push_webhook_secret_cipher FROM projects WHERE id=$1 AND workspace_id=$2`,
48
+ [projectId, workspaceId],
49
+ );
50
+ const project = rows[0];
51
+ if (!project) throw new Error(`project ${projectId} not found`);
52
+
53
+ let deployKeyPublic = project.deploy_key_public;
54
+ let deployPrivateCipher = project.deploy_key_private_cipher;
55
+ if (!deployKeyPublic || !deployPrivateCipher) {
56
+ const key = generateDeployKeyPair();
57
+ deployKeyPublic = key.publicKey;
58
+ deployPrivateCipher = await deps.broker.encryptValue(workspaceId, key.privateKey, "deploykey");
59
+ }
60
+
61
+ let pushWebhookSecretCipher = project.push_webhook_secret_cipher;
62
+ if (!pushWebhookSecretCipher) {
63
+ pushWebhookSecretCipher = await deps.broker.encryptValue(workspaceId, mintWebhookSigningSecret(), "project.webhook.signing");
64
+ }
65
+
66
+ const setupToken = mintWebhookSetupToken();
67
+ await deps.db.query(
68
+ `UPDATE projects
69
+ SET deploy_key_public=$3,
70
+ deploy_key_private_cipher=$4,
71
+ push_webhook_secret_cipher=$5,
72
+ push_webhook_setup_token_hash=$6
73
+ WHERE id=$1 AND workspace_id=$2`,
74
+ [projectId, workspaceId, deployKeyPublic, deployPrivateCipher, pushWebhookSecretCipher, hashWebhookSetupToken(setupToken)],
75
+ );
76
+
77
+ return {
78
+ deployKeyPublic,
79
+ webhookSetupUrl: `${deps.baseUrl.replace(/\/$/, "")}/setup/git/${setupToken}`,
80
+ };
81
+ }
82
+
83
+ export function createApi(deps: ApiDeps): Hono<ApiEnv> {
84
+ const api = new Hono<ApiEnv>();
85
+
86
+ api.use("*", async (c, next) => {
87
+ const header = c.req.header("authorization") ?? "";
88
+ const token = header.startsWith("Bearer ") ? header.slice(7) : "";
89
+ const auth = token ? await verifyToken(deps.db, token) : null;
90
+ if (!auth) return c.json(err("unauthorized", "missing or invalid bearer token"), 401);
91
+ c.set("workspaceId", auth.workspaceId);
92
+ await next();
93
+ return;
94
+ });
95
+
96
+ api.get("/health", (c) => c.json({ ok: true }));
97
+ api.get("/status", async (c) => c.json(await runtimeStatus(deps, c.get("workspaceId"))));
98
+
99
+ // ---- projects ----
100
+
101
+ api.post("/projects", async (c) => {
102
+ const body = (await c.req.json().catch(() => ({}))) as { name?: string; repoUrl?: string; prodBranch?: string };
103
+ if (!body.name || !/^[a-z][a-z0-9-]{0,63}$/.test(body.name)) {
104
+ return c.json(err("invalid", "project name must be kebab-case"), 400);
105
+ }
106
+ const { rows } = await deps.db.query<{ id: string }>(
107
+ `INSERT INTO projects (workspace_id, name, repo_url, prod_branch)
108
+ VALUES ($1,$2,$3,$4)
109
+ ON CONFLICT (workspace_id, name) DO UPDATE SET repo_url = COALESCE(EXCLUDED.repo_url, projects.repo_url)
110
+ RETURNING id`,
111
+ [c.get("workspaceId"), body.name, body.repoUrl ?? null, body.prodBranch ?? "main"],
112
+ );
113
+ const projectId = rows[0]!.id;
114
+ await deps.db.query(
115
+ `INSERT INTO repo_state (project_id) VALUES ($1) ON CONFLICT (project_id) DO NOTHING`,
116
+ [projectId],
117
+ );
118
+ const bootstrap = await ensureProjectBootstrap(deps, c.get("workspaceId"), projectId);
119
+ return c.json({ id: projectId, name: body.name, ...bootstrap });
120
+ });
121
+
122
+ api.get("/projects", async (c) => {
123
+ const { rows } = await deps.db.query(
124
+ `SELECT p.id, p.name, p.repo_url, p.prod_branch, r.last_sha, r.status AS sync_status, r.last_synced_at
125
+ FROM projects p LEFT JOIN repo_state r ON r.project_id = p.id
126
+ WHERE p.workspace_id = $1 ORDER BY p.name`,
127
+ [c.get("workspaceId")],
128
+ );
129
+ return c.json({ projects: rows });
130
+ });
131
+
132
+ const findProject = async (workspaceId: string, name: string) => {
133
+ const { rows } = await deps.db.query<{ id: string; name: string; repo_url: string | null; prod_branch: string }>(
134
+ `SELECT id, name, repo_url, prod_branch FROM projects WHERE workspace_id=$1 AND name=$2`,
135
+ [workspaceId, name],
136
+ );
137
+ return rows[0] ?? null;
138
+ };
139
+
140
+ api.post("/projects/:name/sync", async (c) => {
141
+ const project = await findProject(c.get("workspaceId"), c.req.param("name"));
142
+ if (!project) return c.json(err("not-found", "no such project"), 404);
143
+ const body = (await c.req.json().catch(() => ({}))) as { ref?: string; localPath?: string };
144
+ await deps.db.tx((t) =>
145
+ enqueue(t, {
146
+ kind: "reconcile",
147
+ payload: {
148
+ projectId: project.id,
149
+ ...(body.ref !== undefined ? { ref: body.ref } : {}),
150
+ ...(body.localPath !== undefined ? { localPath: body.localPath } : {}),
151
+ },
152
+ dedupeKey: `reconcile:${project.id}:${body.ref ?? "default"}`,
153
+ maxAttempts: 1,
154
+ }),
155
+ );
156
+ return c.json({ queued: true });
157
+ });
158
+
159
+ api.get("/projects/:name/deploys/latest", async (c) => {
160
+ const project = await findProject(c.get("workspaceId"), c.req.param("name"));
161
+ if (!project) return c.json(err("not-found", "no such project"), 404);
162
+ const env = c.req.query("env") ?? "production";
163
+ if (deps.deployStatus) {
164
+ return c.json((await deps.deployStatus(project.id, env)) as Record<string, unknown>);
165
+ }
166
+ const { rows } = await deps.db.query(
167
+ `SELECT status, error, last_sha, last_synced_at FROM repo_state WHERE project_id=$1`,
168
+ [project.id],
169
+ );
170
+ return c.json({ repo: rows[0] ?? null });
171
+ });
172
+
173
+ api.get("/projects/:name/automations", async (c) => {
174
+ const project = await findProject(c.get("workspaceId"), c.req.param("name"));
175
+ if (!project) return c.json(err("not-found", "no such project"), 404);
176
+ const { rows } = await deps.db.query(
177
+ `SELECT a.automation_id, a.env, v.version, v.git_sha, v.status, v.created_at, v.manifest
178
+ FROM aliases a JOIN automation_versions v ON v.id = a.version_id
179
+ WHERE a.project_id = $1 ORDER BY a.automation_id, a.env`,
180
+ [project.id],
181
+ );
182
+ return c.json({ automations: rows });
183
+ });
184
+
185
+ api.post("/projects/:name/rollback", async (c) => {
186
+ const project = await findProject(c.get("workspaceId"), c.req.param("name"));
187
+ if (!project) return c.json(err("not-found", "no such project"), 404);
188
+ const body = (await c.req.json().catch(() => ({}))) as { automation?: string; toVersion?: number; env?: string };
189
+ if (!body.automation || typeof body.toVersion !== "number") {
190
+ return c.json(err("invalid", "need { automation, toVersion }"), 400);
191
+ }
192
+ const env = body.env ?? "production";
193
+ const { rows } = await deps.db.query<{ id: string }>(
194
+ `SELECT id FROM automation_versions WHERE project_id=$1 AND automation_id=$2 AND version=$3`,
195
+ [project.id, body.automation, body.toVersion],
196
+ );
197
+ if (!rows[0]) return c.json(err("not-found", "no such version"), 404);
198
+ const { rowCount } = await deps.db.query(
199
+ `UPDATE aliases SET version_id=$4, updated_at=now()
200
+ WHERE project_id=$1 AND automation_id=$2 AND env=$3`,
201
+ [project.id, body.automation, env, rows[0].id],
202
+ );
203
+ if (rowCount === 0) return c.json(err("not-found", "automation has no live alias in this env"), 404);
204
+ return c.json({ rolledBack: true, automation: body.automation, env, toVersion: body.toVersion });
205
+ });
206
+
207
+ // ---- runs ----
208
+
209
+ api.post("/runs", async (c) => {
210
+ const body = (await c.req.json().catch(() => ({}))) as {
211
+ project?: string;
212
+ automation?: string;
213
+ input?: unknown;
214
+ env?: string;
215
+ };
216
+ if (!body.project || !body.automation) return c.json(err("invalid", "need { project, automation }"), 400);
217
+ const project = await findProject(c.get("workspaceId"), body.project);
218
+ if (!project) return c.json(err("not-found", "no such project"), 404);
219
+ const env = body.env ?? "production";
220
+ const { rows } = await deps.db.query<{ version_id: string }>(
221
+ `SELECT version_id FROM aliases WHERE project_id=$1 AND automation_id=$2 AND env=$3`,
222
+ [project.id, body.automation, env],
223
+ );
224
+ if (!rows[0]) return c.json(err("not-found", `automation "${body.automation}" has no live version in ${env}`), 404);
225
+ const runId = await createRun(deps.db, {
226
+ projectId: project.id,
227
+ automationId: body.automation,
228
+ versionId: rows[0].version_id,
229
+ env,
230
+ trigger: { kind: "manual" },
231
+ ...(body.input !== undefined ? { input: body.input } : {}),
232
+ });
233
+ return c.json({ runId }, 202);
234
+ });
235
+
236
+ api.get("/runs", async (c) => {
237
+ const limit = Math.min(Number(c.req.query("limit") ?? 50), 200);
238
+ const params: unknown[] = [c.get("workspaceId")];
239
+ let filter = "";
240
+ if (c.req.query("project")) {
241
+ params.push(c.req.query("project"));
242
+ filter += ` AND p.name = $${params.length}`;
243
+ }
244
+ if (c.req.query("automation")) {
245
+ params.push(c.req.query("automation"));
246
+ filter += ` AND r.automation_id = $${params.length}`;
247
+ }
248
+ if (c.req.query("status")) {
249
+ params.push(c.req.query("status"));
250
+ filter += ` AND r.status = $${params.length}`;
251
+ }
252
+ params.push(limit);
253
+ const { rows } = await deps.db.query(
254
+ `SELECT r.id, p.name AS project, r.automation_id, r.env, r.status, r.attempt,
255
+ r.trigger->>'kind' AS trigger_kind, r.created_at, r.started_at, r.finished_at
256
+ FROM runs r JOIN projects p ON p.id = r.project_id
257
+ WHERE p.workspace_id = $1 ${filter}
258
+ ORDER BY r.created_at DESC LIMIT $${params.length}`,
259
+ params,
260
+ );
261
+ return c.json({ runs: rows });
262
+ });
263
+
264
+ api.get("/runs/:id", async (c) => {
265
+ const { rows } = await deps.db.query(
266
+ `SELECT r.*, p.name AS project FROM runs r JOIN projects p ON p.id = r.project_id
267
+ WHERE r.id = $1 AND p.workspace_id = $2`,
268
+ [c.req.param("id"), c.get("workspaceId")],
269
+ );
270
+ if (!rows[0]) return c.json(err("not-found", "no such run"), 404);
271
+ const steps = await deps.db.query(
272
+ `SELECT name, occurrence, status, attempts, result, error, undone, started_at, finished_at
273
+ FROM run_steps WHERE run_id = $1 ORDER BY started_at, occurrence`,
274
+ [c.req.param("id")],
275
+ );
276
+ const logs = await deps.db.query(
277
+ `SELECT step, level, msg, meta, created_at FROM run_logs WHERE run_id = $1 ORDER BY id LIMIT 500`,
278
+ [c.req.param("id")],
279
+ );
280
+ return c.json({ run: rows[0], steps: steps.rows, logs: logs.rows });
281
+ });
282
+
283
+ api.get("/runs/:id/replay", async (c) => {
284
+ const { rows } = await deps.db.query(
285
+ `SELECT r.id, r.automation_id, r.env, r.trigger, r.input, r.output, r.status, r.error
286
+ FROM runs r JOIN projects p ON p.id = r.project_id
287
+ WHERE r.id = $1 AND p.workspace_id = $2`,
288
+ [c.req.param("id"), c.get("workspaceId")],
289
+ );
290
+ if (!rows[0]) return c.json(err("not-found", "no such run"), 404);
291
+ const steps = await deps.db.query(
292
+ `SELECT name, occurrence, status, attempts, result, error FROM run_steps WHERE run_id=$1 ORDER BY started_at, occurrence`,
293
+ [c.req.param("id")],
294
+ );
295
+ return c.json({ replay: { ...rows[0], journal: steps.rows } });
296
+ });
297
+
298
+ api.post("/runs/:id/signals/:name", async (c) => {
299
+ const body = (await c.req.json().catch(() => ({}))) as { payload?: unknown };
300
+ const ok = await deliverSignal(deps.db, {
301
+ runId: c.req.param("id"),
302
+ name: c.req.param("name"),
303
+ payload: body.payload,
304
+ });
305
+ if (!ok) return c.json(err("not-found", "no such run"), 404);
306
+ return c.json({ delivered: true });
307
+ });
308
+
309
+ api.post("/runs/:id/cancel", async (c) => {
310
+ const ok = await cancelRun(deps.db, c.req.param("id"), "cancelled via API");
311
+ return c.json({ cancelled: ok });
312
+ });
313
+
314
+ // ---- connections & secrets (agent lane: names/status only — never values) ----
315
+
316
+ api.get("/connections", async (c) => {
317
+ return c.json({ connections: await deps.broker.listConnections(c.get("workspaceId")) });
318
+ });
319
+
320
+ api.get("/secrets", async (c) => {
321
+ return c.json({ secrets: await deps.broker.listSecretNames(c.get("workspaceId")) });
322
+ });
323
+
324
+ // The piped human/CI lane (`tesser secrets set NAME --value-stdin`): value travels
325
+ // request-body → TLS → encrypted store; it is never echoed back.
326
+ api.put("/secrets/:name", async (c) => {
327
+ const name = c.req.param("name");
328
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(name)) return c.json(err("invalid", "bad secret name"), 400);
329
+ const body = (await c.req.json().catch(() => ({}))) as { value?: string };
330
+ if (typeof body.value !== "string" || body.value.length === 0) {
331
+ return c.json(err("invalid", "need { value }"), 400);
332
+ }
333
+ await deps.broker.setSecret(c.get("workspaceId"), name, body.value);
334
+ return c.json({ set: name });
335
+ });
336
+
337
+ api.delete("/secrets/:name", async (c) => {
338
+ const ok = await deps.broker.deleteSecret(c.get("workspaceId"), c.req.param("name"));
339
+ return c.json({ deleted: ok });
340
+ });
341
+
342
+ // ---- connect links ----
343
+
344
+ api.post("/connect-links", async (c) => {
345
+ const body = (await c.req.json().catch(() => ({}))) as {
346
+ project?: string;
347
+ env?: string;
348
+ };
349
+ let projectId: string | undefined;
350
+ let requirements: import("../broker/connect.js").Requirement[] = [];
351
+ if (body.project) {
352
+ const project = await findProject(c.get("workspaceId"), body.project);
353
+ if (!project) return c.json(err("not-found", "no such project"), 404);
354
+ projectId = project.id;
355
+ const env = body.env ?? "production";
356
+ const versions = await deps.db.query<{ manifest: { connections?: never; secrets?: never; id: string }; automation_id: string }>(
357
+ `SELECT v.manifest, a.automation_id FROM aliases a JOIN automation_versions v ON v.id=a.version_id
358
+ WHERE a.project_id=$1 AND a.env=$2`,
359
+ [project.id, env],
360
+ );
361
+ const staged = await deps.db.query<{ manifest: never; automation_id: string }>(
362
+ `SELECT DISTINCT ON (automation_id) manifest, automation_id FROM automation_versions
363
+ WHERE project_id=$1 AND status='staged' ORDER BY automation_id, version DESC`,
364
+ [project.id],
365
+ );
366
+ const sources = [...versions.rows, ...staged.rows].map((r) => {
367
+ const m = r.manifest as { connections?: Record<string, { connector: string; scope: "workspace" | "per_user" }>; secrets?: Record<string, { describe?: string }> };
368
+ return { automationId: r.automation_id, connections: m.connections ?? {}, secrets: m.secrets ?? {} };
369
+ });
370
+ const connectorManifests = await collectConnectorManifests(deps.db, project.id);
371
+ requirements = await computeMissingRequirements({
372
+ db: deps.db,
373
+ broker: deps.broker,
374
+ workspaceId: c.get("workspaceId"),
375
+ projectId: project.id,
376
+ env,
377
+ automations: sources,
378
+ connectorManifests,
379
+ });
380
+ }
381
+ if (requirements.length === 0) return c.json({ url: null, requirements: [] });
382
+ const token = await mintConnectLink({
383
+ db: deps.db,
384
+ workspaceId: c.get("workspaceId"),
385
+ projectId,
386
+ requirements,
387
+ });
388
+ return c.json({ url: `${deps.baseUrl}/connect/${token}`, token, requirements });
389
+ });
390
+
391
+ api.get("/connect-links/:token/status", async (c) => {
392
+ const link = await getConnectLink(deps.db, c.req.param("token"));
393
+ if (!link || link.workspace_id !== c.get("workspaceId")) return c.json(err("not-found", "no such link"), 404);
394
+ const status = await connectLinkStatus(deps.db, deps.broker, link);
395
+ return c.json(status);
396
+ });
397
+
398
+ // ---- tokens ----
399
+
400
+ api.post("/tokens", async (c) => {
401
+ const body = (await c.req.json().catch(() => ({}))) as { name?: string };
402
+ const token = await mintToken(deps.db, c.get("workspaceId"), body.name ?? "cli");
403
+ return c.json({ token });
404
+ });
405
+
406
+ return api;
407
+ }
408
+
409
+ /** Connector manifests embedded in this project's built versions (per-project facts). */
410
+ export async function collectConnectorManifests(
411
+ db: Db,
412
+ projectId: string,
413
+ ): Promise<Record<string, import("@devosurf/tesser-sdk/internal").ConnectorManifest>> {
414
+ const { rows } = await db.query<{ manifest: { connectors?: Record<string, never> } }>(
415
+ `SELECT manifest FROM automation_versions WHERE project_id=$1 ORDER BY created_at DESC LIMIT 50`,
416
+ [projectId],
417
+ );
418
+ const out: Record<string, never> = {};
419
+ for (const row of rows) {
420
+ for (const [id, m] of Object.entries(row.manifest?.connectors ?? {})) {
421
+ if (!(id in out)) out[id as never] = m;
422
+ }
423
+ }
424
+ return out;
425
+ }
@@ -0,0 +1,33 @@
1
+ // Assemble the HTTP surface: /api (control plane), connect pages + OAuth callback,
2
+ // trigger ingress, and a minimal index.
3
+
4
+ import { Hono } from "hono";
5
+ import { createApi, type ApiDeps } from "./api.js";
6
+ import { createConnectRoutes, type ConnectDeps } from "./connect.js";
7
+ import { createIngress, type IngressDeps } from "./ingress.js";
8
+ import { readinessStatus, RUNTIME_VERSION, type RuntimeStatusDeps } from "./status.js";
9
+
10
+ export interface HttpDeps extends ApiDeps, ConnectDeps, IngressDeps, RuntimeStatusDeps {}
11
+
12
+ export function createApp(deps: HttpDeps): Hono {
13
+ const app = new Hono();
14
+ const version = deps.version ?? RUNTIME_VERSION;
15
+ app.get("/", (c) =>
16
+ c.json({
17
+ service: "tesser",
18
+ docs: "https://github.com/tesser — self-hosted automation instance",
19
+ api: "/api/health",
20
+ health: "/healthz",
21
+ readiness: "/readyz",
22
+ }),
23
+ );
24
+ app.get("/healthz", (c) => c.json({ ok: true, service: "tesser", version }));
25
+ app.get("/readyz", async (c) => {
26
+ const status = await readinessStatus(deps);
27
+ return c.json(status, status.ok ? 200 : 503);
28
+ });
29
+ app.route("/api", createApi(deps));
30
+ app.route("/", createConnectRoutes(deps));
31
+ app.route("/", createIngress(deps));
32
+ return app;
33
+ }