@opengeni/api-router 0.5.3 → 0.5.5

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 (49) hide show
  1. package/dist/app.d.ts +9 -1
  2. package/dist/app.js +7 -1
  3. package/dist/{chunk-3HIA43CC.js → chunk-HBEJMWD3.js} +5470 -2223
  4. package/dist/chunk-HBEJMWD3.js.map +1 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +279 -55
  7. package/dist/index.js.map +1 -1
  8. package/package.json +20 -20
  9. package/src/app.ts +583 -166
  10. package/src/auth/managed-auth.ts +32 -16
  11. package/src/http/auth.ts +8 -1
  12. package/src/http/common.ts +6 -2
  13. package/src/http/sse.ts +84 -8
  14. package/src/index.ts +178 -75
  15. package/src/integrations/oauth-client.ts +403 -120
  16. package/src/integrations/provider-domain.ts +4 -1
  17. package/src/mcp/documents.ts +173 -94
  18. package/src/mcp/server.ts +1600 -693
  19. package/src/mcp/session-view.ts +8 -2
  20. package/src/mcp/toolspace.ts +175 -84
  21. package/src/observability.ts +7 -1
  22. package/src/routes/api-keys.ts +39 -23
  23. package/src/routes/billing.ts +180 -65
  24. package/src/routes/capabilities.ts +17 -8
  25. package/src/routes/catalog-assets.ts +5 -2
  26. package/src/routes/codex.ts +244 -63
  27. package/src/routes/connections.ts +71 -33
  28. package/src/routes/documents.ts +242 -92
  29. package/src/routes/enrollments.ts +100 -70
  30. package/src/routes/environments.ts +205 -136
  31. package/src/routes/files.ts +164 -39
  32. package/src/routes/github.ts +123 -50
  33. package/src/routes/install.ts +9 -2
  34. package/src/routes/machines.ts +9 -8
  35. package/src/routes/packs.ts +141 -89
  36. package/src/routes/rigs.ts +189 -0
  37. package/src/routes/scheduled-tasks.ts +51 -9
  38. package/src/routes/sessions.ts +870 -328
  39. package/src/routes/social.ts +50 -38
  40. package/src/routes/workspace-capture.ts +238 -0
  41. package/src/routes/workspaces.ts +146 -13
  42. package/src/sandbox/access.ts +11 -3
  43. package/src/sandbox/auth-callout.ts +5 -1
  44. package/src/sandbox/channel-a.ts +104 -27
  45. package/src/sandbox/enrollment.ts +13 -3
  46. package/src/sandbox/machines.ts +68 -59
  47. package/src/sandbox/metrics-ingestion.ts +238 -17
  48. package/src/sandbox/viewer.ts +172 -46
  49. package/dist/chunk-3HIA43CC.js.map +0 -1
@@ -1,8 +1,5 @@
1
1
  import { GitHubAppManifestCreate } from "@opengeni/contracts";
2
- import {
3
- listGitHubInstallationIdsForWorkspace,
4
- upsertGitHubInstallation,
5
- } from "@opengeni/db";
2
+ import { listGitHubInstallationIdsForWorkspace, upsertGitHubInstallation } from "@opengeni/db";
6
3
  import {
7
4
  buildGitHubAppManifest,
8
5
  convertGitHubAppManifest,
@@ -29,7 +26,7 @@ import type { ApiRouteDeps } from "@opengeni/core";
29
26
  const githubStateCookie = "opengeni_github_state";
30
27
 
31
28
  export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
32
- const { db, settings, githubStateSecret } = deps;
29
+ const { settings, githubStateSecret } = deps;
33
30
 
34
31
  app.get("/v1/workspaces/:workspaceId/github/app", async (c) => {
35
32
  const workspaceId = c.req.param("workspaceId");
@@ -46,7 +43,9 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
46
43
  appId: settings.githubAppId ?? null,
47
44
  clientId: settings.githubClientId ?? null,
48
45
  appSlug: slug,
49
- installUrl: slug ? `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}` : null,
46
+ installUrl: slug
47
+ ? `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`
48
+ : null,
50
49
  missing,
51
50
  });
52
51
  });
@@ -70,10 +69,17 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
70
69
  }
71
70
  const slug = settings.githubAppSlug?.trim();
72
71
  if (!slug) {
73
- throw new HTTPException(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: githubAppMissingSettings(settings) }) });
72
+ throw new HTTPException(409, {
73
+ message: JSON.stringify({
74
+ message: "GitHub App is not configured",
75
+ missing: githubAppMissingSettings(settings),
76
+ }),
77
+ });
74
78
  }
75
79
  setGitHubStateCookie(c, deps, state);
76
- return c.redirect(`https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`);
80
+ return c.redirect(
81
+ `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`,
82
+ );
77
83
  });
78
84
 
79
85
  app.get("/v1/workspaces/:workspaceId/github/repositories", async (c) => {
@@ -83,9 +89,13 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
83
89
  return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
84
90
  } catch (error) {
85
91
  if (error instanceof GitHubAppConfigurationError) {
86
- throw new HTTPException(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
92
+ throw new HTTPException(409, {
93
+ message: JSON.stringify({ message: error.message, missing: error.missing }),
94
+ });
87
95
  }
88
- throw new HTTPException(502, { message: error instanceof Error ? error.message : String(error) });
96
+ throw new HTTPException(502, {
97
+ message: error instanceof Error ? error.message : String(error),
98
+ });
89
99
  }
90
100
  });
91
101
 
@@ -96,9 +106,13 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
96
106
  return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
97
107
  } catch (error) {
98
108
  if (error instanceof GitHubAppConfigurationError) {
99
- throw new HTTPException(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
109
+ throw new HTTPException(409, {
110
+ message: JSON.stringify({ message: error.message, missing: error.missing }),
111
+ });
100
112
  }
101
- throw new HTTPException(502, { message: error instanceof Error ? error.message : String(error) });
113
+ throw new HTTPException(502, {
114
+ message: error instanceof Error ? error.message : String(error),
115
+ });
102
116
  }
103
117
  });
104
118
 
@@ -106,7 +120,10 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
106
120
  const workspaceId = c.req.param("workspaceId");
107
121
  const grant = await requireAccessGrant(c, deps, workspaceId, "github:manage");
108
122
  const payload = GitHubAppManifestCreate.parse(await c.req.json());
109
- const baseUrl = (settings.githubAppManifestBaseUrl ?? new URL(c.req.url).origin).replace(/\/+$/, "");
123
+ const baseUrl = (settings.githubAppManifestBaseUrl ?? new URL(c.req.url).origin).replace(
124
+ /\/+$/,
125
+ "",
126
+ );
110
127
  const state = createSignedState(githubStateSecret, {
111
128
  accountId: grant.accountId,
112
129
  workspaceId: grant.workspaceId,
@@ -122,7 +139,9 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
122
139
  });
123
140
  const organization = payload.organization?.trim();
124
141
  return c.json({
125
- actionUrl: organization ? organizationAppManifestUrl(organization, state) : personalAppManifestUrl(state),
142
+ actionUrl: organization
143
+ ? organizationAppManifestUrl(organization, state)
144
+ : personalAppManifestUrl(state),
126
145
  state,
127
146
  manifest,
128
147
  });
@@ -141,7 +160,9 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
141
160
  const conversion = await convertGitHubAppManifest(code);
142
161
  const envLines = envLinesFromGitHubManifestConversion(conversion);
143
162
  const slug = String(conversion.slug ?? "");
144
- const installUrl = slug ? `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}` : "";
163
+ const installUrl = slug
164
+ ? `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`
165
+ : "";
145
166
  setGitHubStateCookie(c, deps, state);
146
167
  return c.html(githubSuccessHtml(envLines, installUrl));
147
168
  } catch (error) {
@@ -159,13 +180,19 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
159
180
  throw new HTTPException(400, { message: "missing GitHub installation state" });
160
181
  }
161
182
  const statePayload = readSignedState(state, githubStateSecret);
162
- if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string") {
183
+ if (
184
+ !statePayload ||
185
+ typeof statePayload.accountId !== "string" ||
186
+ typeof statePayload.workspaceId !== "string"
187
+ ) {
163
188
  throw new HTTPException(400, { message: "invalid or expired GitHub installation state" });
164
189
  }
165
190
  requireGitHubStateCookie(c, state);
166
191
  const grant = await requireAccessGrant(c, deps, statePayload.workspaceId, "github:manage");
167
192
  if (grant.accountId !== statePayload.accountId) {
168
- throw new HTTPException(403, { message: "GitHub installation state does not match this workspace" });
193
+ throw new HTTPException(403, {
194
+ message: "GitHub installation state does not match this workspace",
195
+ });
169
196
  }
170
197
  if (setupAction === "request" && !installationIdRaw) {
171
198
  return c.html(githubSetupPendingHtml());
@@ -177,20 +204,31 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
177
204
  if (!code) {
178
205
  const clientId = settings.githubClientId?.trim();
179
206
  if (!clientId) {
180
- throw new HTTPException(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: ["OPENGENI_GITHUB_CLIENT_ID"] }) });
207
+ throw new HTTPException(409, {
208
+ message: JSON.stringify({
209
+ message: "GitHub App is not configured",
210
+ missing: ["OPENGENI_GITHUB_CLIENT_ID"],
211
+ }),
212
+ });
181
213
  }
182
214
  const oauthState = createSignedState(githubStateSecret, {
183
215
  accountId: grant.accountId,
184
216
  workspaceId: grant.workspaceId,
185
217
  installationId,
186
218
  });
187
- const baseUrl = (settings.githubAppManifestBaseUrl ?? settings.publicBaseUrl ?? new URL(c.req.url).origin).replace(/\/+$/, "");
219
+ const baseUrl = (
220
+ settings.githubAppManifestBaseUrl ??
221
+ settings.publicBaseUrl ??
222
+ new URL(c.req.url).origin
223
+ ).replace(/\/+$/, "");
188
224
  setGitHubStateCookie(c, deps, oauthState);
189
- return c.redirect(githubOAuthAuthorizeUrl({
190
- clientId,
191
- state: oauthState,
192
- redirectUri: `${baseUrl}/v1/github/oauth/callback`,
193
- }));
225
+ return c.redirect(
226
+ githubOAuthAuthorizeUrl({
227
+ clientId,
228
+ state: oauthState,
229
+ redirectUri: `${baseUrl}/v1/github/oauth/callback`,
230
+ }),
231
+ );
194
232
  }
195
233
  return await completeGitHubInstallationBinding(deps, c, {
196
234
  code,
@@ -213,7 +251,12 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
213
251
  }
214
252
  const statePayload = readSignedState(state, githubStateSecret);
215
253
  const installationId = parsePositiveInteger(String(statePayload?.installationId ?? ""));
216
- if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || installationId === null) {
254
+ if (
255
+ !statePayload ||
256
+ typeof statePayload.accountId !== "string" ||
257
+ typeof statePayload.workspaceId !== "string" ||
258
+ installationId === null
259
+ ) {
217
260
  throw new HTTPException(400, { message: "invalid or expired GitHub OAuth state" });
218
261
  }
219
262
  requireGitHubStateCookie(c, state);
@@ -240,12 +283,19 @@ async function completeGitHubInstallationBinding(
240
283
  }
241
284
  const grant = await requireAccessGrant(c, deps, input.statePayload.workspaceId, "github:manage");
242
285
  if (grant.accountId !== input.statePayload.accountId) {
243
- throw new HTTPException(403, { message: "GitHub installation state does not match this workspace" });
286
+ throw new HTTPException(403, {
287
+ message: "GitHub installation state does not match this workspace",
288
+ });
244
289
  }
245
290
  try {
246
- const installation = await verifyGitHubInstallationAccessForUser(settings, { code: input.code, installationId: input.installationId });
291
+ const installation = await verifyGitHubInstallationAccessForUser(settings, {
292
+ code: input.code,
293
+ installationId: input.installationId,
294
+ });
247
295
  if (!installation) {
248
- throw new HTTPException(404, { message: "GitHub App installation was not found for this app" });
296
+ throw new HTTPException(404, {
297
+ message: "GitHub App installation was not found for this app",
298
+ });
249
299
  }
250
300
  if (installation.suspended) {
251
301
  throw new HTTPException(409, { message: "GitHub App installation is suspended" });
@@ -259,16 +309,25 @@ async function completeGitHubInstallationBinding(
259
309
  });
260
310
  const returnUrl = openGeniReturnUrl(settings, c, input.statePayload.workspaceId);
261
311
  deleteCookie(c, githubStateCookie, { path: "/v1/github" });
262
- return c.html(githubSetupSuccessHtml(installation.accountLogin ?? `installation ${input.installationId}`, returnUrl));
263
- } catch (error) {
264
- if (error instanceof HTTPException) {
265
- throw error;
266
- }
267
- if (error instanceof GitHubAppConfigurationError) {
268
- throw new HTTPException(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
269
- }
270
- throw new HTTPException(502, { message: error instanceof Error ? error.message : String(error) });
312
+ return c.html(
313
+ githubSetupSuccessHtml(
314
+ installation.accountLogin ?? `installation ${input.installationId}`,
315
+ returnUrl,
316
+ ),
317
+ );
318
+ } catch (error) {
319
+ if (error instanceof HTTPException) {
320
+ throw error;
321
+ }
322
+ if (error instanceof GitHubAppConfigurationError) {
323
+ throw new HTTPException(409, {
324
+ message: JSON.stringify({ message: error.message, missing: error.missing }),
325
+ });
271
326
  }
327
+ throw new HTTPException(502, {
328
+ message: error instanceof Error ? error.message : String(error),
329
+ });
330
+ }
272
331
  }
273
332
 
274
333
  function setGitHubStateCookie(c: Context, deps: ApiRouteDeps, state: string): void {
@@ -283,14 +342,18 @@ function setGitHubStateCookie(c: Context, deps: ApiRouteDeps, state: string): vo
283
342
 
284
343
  function requireGitHubStateCookie(c: Context, state: string): void {
285
344
  if (getCookie(c, githubStateCookie) !== state) {
286
- throw new HTTPException(400, { message: "invalid or expired GitHub installation browser state" });
345
+ throw new HTTPException(400, {
346
+ message: "invalid or expired GitHub installation browser state",
347
+ });
287
348
  }
288
349
  }
289
350
 
290
351
  function isSecureRequest(c: Context, deps: ApiRouteDeps): boolean {
291
- return deps.settings.publicBaseUrl?.startsWith("https://")
292
- || c.req.header("x-forwarded-proto") === "https"
293
- || new URL(c.req.url).protocol === "https:";
352
+ return (
353
+ deps.settings.publicBaseUrl?.startsWith("https://") ||
354
+ c.req.header("x-forwarded-proto") === "https" ||
355
+ new URL(c.req.url).protocol === "https:"
356
+ );
294
357
  }
295
358
 
296
359
  export async function listWorkspaceGitHubRepositories(deps: ApiRouteDeps, workspaceId: string) {
@@ -301,7 +364,9 @@ export async function listWorkspaceGitHubRepositories(deps: ApiRouteDeps, worksp
301
364
  function githubSuccessHtml(envLines: string[], installUrl: string): string {
302
365
  const envText = envLines.join("\n");
303
366
  const escaped = escapeHtml(envText);
304
- const install = installUrl ? `<a class="button secondary" href="${escapeHtml(installUrl)}">Install on repositories</a>` : "";
367
+ const install = installUrl
368
+ ? `<a class="button secondary" href="${escapeHtml(installUrl)}">Install on repositories</a>`
369
+ : "";
305
370
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GitHub App Created</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(760px,calc(100vw - 32px));border:1px solid #27272a;border-radius:8px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px;line-height:1.2}p{margin:0 0 18px;color:#d4d4d8}.env-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:22px 0 8px}.env-header h2{margin:0;font-size:13px;line-height:1.2;text-transform:uppercase;letter-spacing:.08em;color:#a1a1aa}pre{white-space:pre-wrap;word-break:break-word;max-height:380px;overflow:auto;background:#09090b;border:1px solid #27272a;border-radius:8px;padding:16px;font-size:13px;line-height:1.5}.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:18px}.button,button{display:inline-flex;align-items:center;justify-content:center;min-height:36px;border-radius:6px;border:1px solid #3f3f46;padding:0 12px;background:#f4f4f5;color:#09090b;font:600 14px system-ui,sans-serif;text-decoration:none;cursor:pointer}.button.secondary{background:transparent;color:#fafafa}.button.secondary:hover,button.secondary:hover{background:#27272a}button:disabled{cursor:not-allowed;opacity:.7}</style></head><body><main><h1>GitHub App created</h1><p>Add these values to .env, then restart API and worker.</p><div class="env-header"><h2>Environment variables</h2><button id="copy-env" type="button">Copy env</button></div><pre id="env-lines">${escaped}</pre><div class="actions">${install}</div><script>(()=>{const button=document.getElementById("copy-env");const env=document.getElementById("env-lines");async function copyText(text){if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(text);return;}const area=document.createElement("textarea");area.value=text;area.setAttribute("readonly","");area.style.position="fixed";area.style.inset="-9999px";document.body.append(area);area.select();document.execCommand("copy");area.remove();}button?.addEventListener("click",async()=>{try{await copyText(env?.textContent||"");button.textContent="Copied";setTimeout(()=>button.textContent="Copy env",1600);}catch{button.textContent="Copy failed";setTimeout(()=>button.textContent="Copy env",2200);}});})();</script></main></body></html>`;
306
371
  }
307
372
 
@@ -322,16 +387,24 @@ function parsePositiveInteger(value: string | undefined | null): number | null {
322
387
  }
323
388
 
324
389
  function escapeHtml(value: string): string {
325
- return value.replace(/[&<>"']/g, (char) => ({
326
- "&": "&amp;",
327
- "<": "&lt;",
328
- ">": "&gt;",
329
- '"': "&quot;",
330
- "'": "&#39;",
331
- }[char] ?? char));
390
+ return value.replace(
391
+ /[&<>"']/g,
392
+ (char) =>
393
+ ({
394
+ "&": "&amp;",
395
+ "<": "&lt;",
396
+ ">": "&gt;",
397
+ '"': "&quot;",
398
+ "'": "&#39;",
399
+ })[char] ?? char,
400
+ );
332
401
  }
333
402
 
334
- function openGeniReturnUrl(settings: ApiRouteDeps["settings"], c: Context, workspaceId: string | undefined): string {
403
+ function openGeniReturnUrl(
404
+ settings: ApiRouteDeps["settings"],
405
+ c: Context,
406
+ workspaceId: string | undefined,
407
+ ): string {
335
408
  const base = (settings.publicBaseUrl ?? new URL(c.req.url).origin).replace(/\/+$/, "");
336
409
  const url = new URL(base || new URL(c.req.url).origin);
337
410
  if (workspaceId) {
@@ -41,7 +41,10 @@ const TEXT_ASSETS: Record<string, { file: string; contentType: string }> = {
41
41
  "/install.sh": { file: "install.sh", contentType: "text/x-shellscript; charset=utf-8" },
42
42
  "/install.ps1": { file: "install.ps1", contentType: "text/plain; charset=utf-8" },
43
43
  "/uninstall.sh": { file: "uninstall.sh", contentType: "text/x-shellscript; charset=utf-8" },
44
- "/opengeni-agent-minisign.pub": { file: "opengeni-agent-minisign.pub", contentType: "text/plain; charset=utf-8" },
44
+ "/opengeni-agent-minisign.pub": {
45
+ file: "opengeni-agent-minisign.pub",
46
+ contentType: "text/plain; charset=utf-8",
47
+ },
45
48
  };
46
49
 
47
50
  const assetCache = new Map<string, string>();
@@ -84,7 +87,11 @@ const DEFAULT_BASE_REWRITES: Record<string, (base: string) => { from: string; to
84
87
  // archive default stands), when the URL is not absolute http(s) (never serve a
85
88
  // script with a broken base), or when the file has no marker (script drift — fail
86
89
  // safe to serving it verbatim rather than crashing the install surface).
87
- function rewriteDefaultBaseUrl(file: string, body: string, publicBaseUrl: string | undefined): string {
90
+ function rewriteDefaultBaseUrl(
91
+ file: string,
92
+ body: string,
93
+ publicBaseUrl: string | undefined,
94
+ ): string {
88
95
  if (!publicBaseUrl || !/^https?:\/\//.test(publicBaseUrl)) {
89
96
  return body;
90
97
  }
@@ -19,10 +19,7 @@ import {
19
19
  SwapActiveSandboxRequest,
20
20
  SwapActiveSandboxResponse,
21
21
  } from "@opengeni/contracts";
22
- import {
23
- getEnrollment,
24
- readMachineMetricsSeries,
25
- } from "@opengeni/db";
22
+ import { getEnrollment, readMachineMetricsSeries } from "@opengeni/db";
26
23
  import type { Hono } from "hono";
27
24
  import { HTTPException } from "hono/http-exception";
28
25
  import { requireAccessGrant } from "@opengeni/core";
@@ -48,7 +45,9 @@ export function registerMachineRoutes(app: Hono, deps: ApiRouteDeps): void {
48
45
  // invisible while disabled — it does not exist for this deployment yet.
49
46
  function assertSelfhostedEnabled(): void {
50
47
  if (!settings.sandboxSelfhostedEnabled) {
51
- throw new HTTPException(404, { message: "selfhosted machines are not enabled for this deployment" });
48
+ throw new HTTPException(404, {
49
+ message: "selfhosted machines are not enabled for this deployment",
50
+ });
52
51
  }
53
52
  }
54
53
 
@@ -79,9 +78,11 @@ export function registerMachineRoutes(app: Hono, deps: ApiRouteDeps): void {
79
78
  const windowMs = SERIES_WINDOWS_MS[c.req.query("window") ?? ""] ?? DEFAULT_SERIES_WINDOW_MS;
80
79
  const since = new Date(Date.now() - windowMs);
81
80
  const rows = await readMachineMetricsSeries(db, { workspaceId, enrollmentId, since });
82
- return c.json(MachineMetricsSeriesResponse.parse({
83
- samples: rows.map(metricRowToSample),
84
- }));
81
+ return c.json(
82
+ MachineMetricsSeriesResponse.parse({
83
+ samples: rows.map(metricRowToSample),
84
+ }),
85
+ );
85
86
  });
86
87
 
87
88
  // ── POST /workspaces/:ws/sessions/:sessionId/active-sandbox (swap) ───────────