@opengeni/api-router 0.22.2 → 0.26.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.
Files changed (124) hide show
  1. package/dist/app.d.ts +2 -2
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +0 -30
  4. package/dist/browser-controller-authority.d.ts +43 -0
  5. package/dist/browser-state-authority.d.ts +27 -0
  6. package/dist/{chunk-HWXJW5C7.js → chunk-JIKNR5YL.js} +27993 -14546
  7. package/dist/chunk-JIKNR5YL.js.map +1 -0
  8. package/dist/codemode.d.ts +23 -0
  9. package/dist/editable-artifact-live-hints.d.ts +11 -0
  10. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  11. package/dist/editable-artifact-office-import.d.ts +22 -0
  12. package/dist/editable-artifact-production.d.ts +29 -0
  13. package/dist/editable-artifact-websocket.d.ts +49 -0
  14. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  15. package/dist/github-browser-flow.d.ts +6 -0
  16. package/dist/http/cors.d.ts +1 -0
  17. package/dist/http/sse.d.ts +2 -0
  18. package/dist/index.d.ts +3 -2
  19. package/dist/index.js +1824 -30
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/api-integrations.d.ts +24 -0
  22. package/dist/integrations/atlassian.d.ts +176 -0
  23. package/dist/integrations/github-skill-source.d.ts +5 -0
  24. package/dist/integrations/google-drive.d.ts +85 -0
  25. package/dist/integrations/oauth-client.d.ts +30 -1
  26. package/dist/integrations/provider-oauth.d.ts +19 -0
  27. package/dist/integrations/slack-bot.d.ts +4 -0
  28. package/dist/integrations/slack-interactions.d.ts +14 -2
  29. package/dist/integrations/social-api.d.ts +2 -1
  30. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  31. package/dist/mcp/editable-artifacts.d.ts +13 -0
  32. package/dist/mcp/receipts.d.ts +28 -0
  33. package/dist/mcp/scheduled-task-view.d.ts +518 -0
  34. package/dist/mcp/server.d.ts +14 -3
  35. package/dist/memory-slack-delivery.d.ts +9 -0
  36. package/dist/routes/api-integrations.d.ts +8 -0
  37. package/dist/routes/browser-identities.d.ts +5 -0
  38. package/dist/routes/browser-sessions.d.ts +6 -0
  39. package/dist/routes/company-profile.d.ts +3 -0
  40. package/dist/routes/computer-sessions.d.ts +6 -0
  41. package/dist/routes/editable-artifacts.d.ts +44 -0
  42. package/dist/routes/integration-features.d.ts +3 -0
  43. package/dist/routes/memory-slack-publications.d.ts +6 -0
  44. package/dist/routes/plugins.d.ts +8 -0
  45. package/dist/routes/sessions.d.ts +17 -2
  46. package/dist/routes/skills.d.ts +6 -0
  47. package/dist/routes/video-generation.d.ts +3 -0
  48. package/dist/sandbox/auth-callout.d.ts +2 -0
  49. package/dist/sandbox/channel-a.d.ts +59 -2
  50. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  51. package/dist/sandbox/viewer.d.ts +4 -2
  52. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  53. package/package.json +19 -14
  54. package/src/app.ts +277 -41
  55. package/src/auth/managed-auth.ts +0 -16
  56. package/src/browser-controller-authority.ts +137 -0
  57. package/src/browser-state-authority.ts +236 -0
  58. package/src/codemode.ts +186 -0
  59. package/src/editable-artifact-live-hints.ts +64 -0
  60. package/src/editable-artifact-native-kernel.ts +659 -0
  61. package/src/editable-artifact-office-import.ts +230 -0
  62. package/src/editable-artifact-production.ts +419 -0
  63. package/src/editable-artifact-websocket.ts +311 -0
  64. package/src/editable-artifact-workspace-files.ts +186 -0
  65. package/src/github-browser-flow.ts +35 -6
  66. package/src/http/auth.ts +2 -0
  67. package/src/http/cors.ts +3 -0
  68. package/src/http/sse.ts +101 -6
  69. package/src/index.ts +147 -23
  70. package/src/integrations/api-integrations.ts +350 -0
  71. package/src/integrations/atlassian.ts +1621 -0
  72. package/src/integrations/github-skill-source.ts +142 -0
  73. package/src/integrations/google-drive.ts +1000 -64
  74. package/src/integrations/oauth-client.ts +159 -89
  75. package/src/integrations/provider-oauth.ts +777 -0
  76. package/src/integrations/slack-bot.ts +31 -2
  77. package/src/integrations/slack-interactions.ts +610 -42
  78. package/src/integrations/social-api.ts +11 -0
  79. package/src/mcp/documents.ts +74 -26
  80. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  81. package/src/mcp/editable-artifacts.ts +448 -0
  82. package/src/mcp/receipts.ts +95 -0
  83. package/src/mcp/scheduled-task-view.ts +642 -0
  84. package/src/mcp/server.ts +1718 -310
  85. package/src/memory-slack-delivery.ts +209 -0
  86. package/src/observability.ts +3 -3
  87. package/src/routes/api-integrations.ts +407 -0
  88. package/src/routes/api-keys.ts +7 -1
  89. package/src/routes/browser-identities.ts +136 -0
  90. package/src/routes/browser-sessions.ts +2543 -0
  91. package/src/routes/codex.ts +7 -4
  92. package/src/routes/company-profile.ts +255 -0
  93. package/src/routes/computer-sessions.ts +1247 -0
  94. package/src/routes/connections.ts +358 -102
  95. package/src/routes/documents.ts +22 -3
  96. package/src/routes/editable-artifacts.ts +1159 -0
  97. package/src/routes/enrollments.ts +54 -12
  98. package/src/routes/environments.ts +60 -11
  99. package/src/routes/files.ts +277 -65
  100. package/src/routes/github.ts +18 -2
  101. package/src/routes/install.ts +38 -2
  102. package/src/routes/integration-features.ts +258 -0
  103. package/src/routes/machines.ts +1 -1
  104. package/src/routes/memory-slack-publications.ts +216 -0
  105. package/src/routes/packs.ts +437 -7
  106. package/src/routes/plugins.ts +751 -0
  107. package/src/routes/rigs.ts +77 -20
  108. package/src/routes/scheduled-tasks.ts +94 -42
  109. package/src/routes/sessions.ts +475 -234
  110. package/src/routes/skills.ts +174 -0
  111. package/src/routes/transcription-recordings.ts +65 -33
  112. package/src/routes/video-generation.ts +132 -0
  113. package/src/routes/workspaces.ts +46 -24
  114. package/src/sandbox/auth-callout.ts +16 -4
  115. package/src/sandbox/channel-a.ts +809 -85
  116. package/src/sandbox/enrollment.ts +13 -3
  117. package/src/sandbox/machines.ts +1 -1
  118. package/src/sandbox/metrics-ingestion.ts +121 -3
  119. package/src/sandbox/rematerialize.ts +35 -47
  120. package/src/sandbox/viewer.ts +58 -29
  121. package/src/temporal-schedule-cleanup.ts +135 -0
  122. package/dist/chunk-HWXJW5C7.js.map +0 -1
  123. package/dist/mcp/toolspace.d.ts +0 -62
  124. package/src/mcp/toolspace.ts +0 -1186
@@ -8,6 +8,10 @@ import {
8
8
  RETAINED_OUTPUT_MAX_PAGE_BYTES,
9
9
  RetainedArtifactMetadataSchema,
10
10
  retainedArtifactReferenceFromFile,
11
+ retainedGeneratedImageReferenceFromFile,
12
+ retainedGeneratedVideoReferenceFromFile,
13
+ retainedScreenshotReferenceFromFile,
14
+ VideoArtifactPlaybackSource,
11
15
  resolveRetainedOutputRange,
12
16
  type RetainedArtifactMetadata,
13
17
  type RetainedOutputUnavailableReason,
@@ -18,11 +22,17 @@ import {
18
22
  completeFileUpload,
19
23
  createFileUpload,
20
24
  getFileUpload,
25
+ getGeneratedImageArtifact,
26
+ getGeneratedVideoArtifact,
21
27
  getRetainedFileArtifact,
28
+ getRetainedScreenshotArtifact,
22
29
  requireFile,
23
30
  type RetainedFileArtifact,
31
+ type GeneratedImageArtifact,
32
+ type GeneratedVideoArtifact,
33
+ type RetainedScreenshotArtifact,
24
34
  } from "@opengeni/db";
25
- import type { Hono } from "hono";
35
+ import type { Context, Hono } from "hono";
26
36
  import { HTTPException } from "hono/http-exception";
27
37
  import { requireAccessGrant } from "@opengeni/core";
28
38
  import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
@@ -36,7 +46,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
36
46
  app.all("/v1/workspaces/:workspaceId/mcp/files", async (c) => {
37
47
  const workspaceId = c.req.param("workspaceId");
38
48
  const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
39
- const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
49
+ const transport = new WebStandardStreamableHTTPServerTransport({
50
+ enableJsonResponse: true,
51
+ });
40
52
  const server = buildFilesMcpServer(deps, grant);
41
53
  await server.connect(transport);
42
54
  return await transport.handleRequest(c.req.raw);
@@ -46,7 +58,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
46
58
  const workspaceId = c.req.param("workspaceId");
47
59
  const grant = await requireAccessGrant(c, deps, workspaceId, "files:upload");
48
60
  if (!objectStorage) {
49
- throw new HTTPException(503, { message: "object storage is not configured" });
61
+ throw new HTTPException(503, {
62
+ message: "object storage is not configured",
63
+ });
50
64
  }
51
65
  const payload = CreateFileUploadRequest.parse(await c.req.json());
52
66
  await requireLimit(deps, {
@@ -98,7 +112,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
98
112
  const workspaceId = c.req.param("workspaceId");
99
113
  const grant = await requireAccessGrant(c, deps, workspaceId, "files:upload");
100
114
  if (!objectStorage) {
101
- throw new HTTPException(503, { message: "object storage is not configured" });
115
+ throw new HTTPException(503, {
116
+ message: "object storage is not configured",
117
+ });
102
118
  }
103
119
  const upload = await getFileUpload(db, workspaceId, c.req.param("uploadId"));
104
120
  if (!upload) {
@@ -186,7 +202,9 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
186
202
  terminalStatus,
187
203
  });
188
204
  if (!settled) {
189
- throw new HTTPException(409, { message: "file upload cleanup claim was superseded" });
205
+ throw new HTTPException(409, {
206
+ message: "file upload cleanup claim was superseded",
207
+ });
190
208
  }
191
209
  throw new HTTPException(status, { message });
192
210
  };
@@ -259,91 +277,112 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
259
277
  const workspaceId = c.req.param("workspaceId");
260
278
  await requireAccessGrant(c, deps, workspaceId, "files:read");
261
279
  const artifactId = retainedArtifactId(c.req.param("artifactId"));
262
- const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
280
+ const artifact = await getWorkspaceArtifact(db, workspaceId, artifactId);
263
281
  if (!artifact) {
264
282
  return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
265
283
  }
266
- return c.json(retainedArtifactMetadata(artifact));
284
+ return c.json(artifact.metadata);
267
285
  });
268
286
 
269
287
  app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId/content", async (c) => {
270
288
  const workspaceId = c.req.param("workspaceId");
271
289
  await requireAccessGrant(c, deps, workspaceId, "files:read");
272
290
  const artifactId = retainedArtifactId(c.req.param("artifactId"));
273
- const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
291
+ const artifact = await getWorkspaceArtifact(db, workspaceId, artifactId);
274
292
  if (!artifact) {
275
293
  return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
276
294
  }
277
295
 
278
- const metadata = retainedArtifactMetadata(artifact);
279
- if (!metadata.available) {
280
- return c.json(metadata, retainedArtifactUnavailableStatus(metadata.reason));
281
- }
296
+ return await serveRetainedArtifactContent(
297
+ c,
298
+ objectStorage,
299
+ artifactId,
300
+ artifact.file,
301
+ artifact.metadata,
302
+ );
303
+ });
304
+
305
+ app.post("/v1/workspaces/:workspaceId/artifacts/:artifactId/playback-source", async (c) => {
306
+ const workspaceId = c.req.param("workspaceId");
307
+ await requireAccessGrant(c, deps, workspaceId, "files:read");
282
308
  if (!objectStorage) {
283
- return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 503);
309
+ throw new HTTPException(503, { message: "object storage is not configured" });
284
310
  }
285
-
286
- const rangeHeader = c.req.header("range");
287
- const range = resolveRetainedOutputRange(
288
- rangeHeader,
289
- metadata.originalBytes,
290
- rangeHeader ? RETAINED_OUTPUT_MAX_PAGE_BYTES : RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
291
- );
292
- if (range.kind === "invalid") {
293
- return c.json(
294
- {
295
- message: "invalid retained artifact byte range",
296
- reason: range.reason,
297
- maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES,
298
- },
299
- 400,
300
- );
311
+ const artifactId = retainedArtifactId(c.req.param("artifactId"));
312
+ const retained = await getGeneratedVideoArtifact(db, workspaceId, artifactId);
313
+ if (!retained || retained.artifact.deletedAt) {
314
+ throw new HTTPException(404, { message: "generated video not found" });
301
315
  }
302
- if (range.kind === "unsatisfiable") {
303
- return c.json(
304
- { message: "retained artifact byte range is not satisfiable", reason: range.reason },
305
- 416,
306
- {
307
- "Accept-Ranges": "bytes",
308
- "Content-Range": range.contentRange,
309
- "Cache-Control": "private, no-store",
310
- },
311
- );
316
+ if (retained.file.status !== "ready") {
317
+ throw new HTTPException(409, { message: `generated video is ${retained.file.status}` });
312
318
  }
313
-
314
- const headers = {
315
- "Accept-Ranges": range.acceptRanges,
316
- "Cache-Control": "private, no-store",
317
- "Content-Length": String(range.length),
318
- "Content-Type": metadata.contentType,
319
- "X-Content-Type-Options": "nosniff",
320
- ...(range.contentRange ? { "Content-Range": range.contentRange } : {}),
321
- };
322
- if (range.kind === "empty") {
323
- if (!(await objectStorage.fileExists(artifact.file))) {
324
- return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
325
- }
326
- return c.body(null, 200, headers);
319
+ const file = generatedVideoFileAsset(retained.file);
320
+ if (!(await objectStorage.fileExists(file))) {
321
+ throw new HTTPException(410, { message: "generated video bytes are unavailable" });
327
322
  }
323
+ const signed = await objectStorage.createGetUrl({ key: file.objectKey });
324
+ return c.json(
325
+ VideoArtifactPlaybackSource.parse({
326
+ schemaVersion: 1,
327
+ artifactId,
328
+ url: signed.url,
329
+ expiresAt: signed.expiresAt.toISOString(),
330
+ contentType: "video/mp4",
331
+ sizeBytes: file.sizeBytes,
332
+ sha256: file.sha256,
333
+ acceptRanges: "bytes",
334
+ }),
335
+ );
336
+ });
328
337
 
329
- const bytes = await objectStorage.getFileRange(artifact.file, {
330
- start: range.start,
331
- end: range.end,
332
- });
333
- if (!bytes) {
334
- return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
335
- }
336
- if (bytes.byteLength !== range.length) {
337
- throw new HTTPException(502, { message: "object storage returned an invalid byte range" });
338
+ app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/:artifactId", async (c) => {
339
+ const workspaceId = c.req.param("workspaceId");
340
+ await requireAccessGrant(c, deps, workspaceId, "files:read");
341
+ const artifactId = retainedArtifactId(c.req.param("artifactId"));
342
+ const artifact = await getRetainedScreenshotArtifact(
343
+ db,
344
+ workspaceId,
345
+ c.req.param("sessionId"),
346
+ artifactId,
347
+ );
348
+ if (!artifact) {
349
+ return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
338
350
  }
339
- return c.body(new Uint8Array(bytes), range.status, headers);
351
+ return c.json(retainedScreenshotMetadata(artifact));
340
352
  });
341
353
 
354
+ app.get(
355
+ "/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/:artifactId/content",
356
+ async (c) => {
357
+ const workspaceId = c.req.param("workspaceId");
358
+ await requireAccessGrant(c, deps, workspaceId, "files:read");
359
+ const artifactId = retainedArtifactId(c.req.param("artifactId"));
360
+ const artifact = await getRetainedScreenshotArtifact(
361
+ db,
362
+ workspaceId,
363
+ c.req.param("sessionId"),
364
+ artifactId,
365
+ );
366
+ if (!artifact) {
367
+ return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
368
+ }
369
+ return await serveRetainedArtifactContent(
370
+ c,
371
+ objectStorage,
372
+ artifactId,
373
+ artifact.file,
374
+ retainedScreenshotMetadata(artifact),
375
+ );
376
+ },
377
+ );
378
+
342
379
  app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => {
343
380
  const workspaceId = c.req.param("workspaceId");
344
381
  await requireAccessGrant(c, deps, workspaceId, "files:read");
345
382
  if (!objectStorage) {
346
- throw new HTTPException(503, { message: "object storage is not configured" });
383
+ throw new HTTPException(503, {
384
+ message: "object storage is not configured",
385
+ });
347
386
  }
348
387
  const file = await requireFile(db, workspaceId, c.req.param("fileId")).catch(() => null);
349
388
  if (!file) {
@@ -362,6 +401,81 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
362
401
  });
363
402
  }
364
403
 
404
+ async function serveRetainedArtifactContent(
405
+ c: Context,
406
+ objectStorage: ApiRouteDeps["objectStorage"],
407
+ artifactId: string,
408
+ file: RetainedFileArtifact["file"],
409
+ metadata: RetainedArtifactMetadata,
410
+ ) {
411
+ if (!metadata.available) {
412
+ return c.json(metadata, retainedArtifactUnavailableStatus(metadata.reason));
413
+ }
414
+ if (!objectStorage) {
415
+ return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 503);
416
+ }
417
+
418
+ const rangeHeader = c.req.header("range");
419
+ const range = resolveRetainedOutputRange(
420
+ rangeHeader,
421
+ metadata.originalBytes,
422
+ rangeHeader ? RETAINED_OUTPUT_MAX_PAGE_BYTES : RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
423
+ );
424
+ if (range.kind === "invalid") {
425
+ return c.json(
426
+ {
427
+ message: "invalid retained artifact byte range",
428
+ reason: range.reason,
429
+ maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES,
430
+ },
431
+ 400,
432
+ );
433
+ }
434
+ if (range.kind === "unsatisfiable") {
435
+ return c.json(
436
+ {
437
+ message: "retained artifact byte range is not satisfiable",
438
+ reason: range.reason,
439
+ },
440
+ 416,
441
+ {
442
+ "Accept-Ranges": "bytes",
443
+ "Content-Range": range.contentRange,
444
+ "Cache-Control": "private, no-store",
445
+ },
446
+ );
447
+ }
448
+
449
+ const headers = {
450
+ "Accept-Ranges": range.acceptRanges,
451
+ "Cache-Control": "private, no-store",
452
+ "Content-Length": String(range.length),
453
+ "Content-Type": metadata.contentType,
454
+ "X-Content-Type-Options": "nosniff",
455
+ ...(range.contentRange ? { "Content-Range": range.contentRange } : {}),
456
+ };
457
+ if (range.kind === "empty") {
458
+ if (!(await objectStorage.fileExists(file))) {
459
+ return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
460
+ }
461
+ return c.body(null, 200, headers);
462
+ }
463
+
464
+ const bytes = await objectStorage.getFileRange(file, {
465
+ start: range.start,
466
+ end: range.end,
467
+ });
468
+ if (!bytes) {
469
+ return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
470
+ }
471
+ if (bytes.byteLength !== range.length) {
472
+ throw new HTTPException(502, {
473
+ message: "object storage returned an invalid byte range",
474
+ });
475
+ }
476
+ return c.body(new Uint8Array(bytes), range.status, headers);
477
+ }
478
+
365
479
  export function sanitizeFilename(filename: string): string {
366
480
  const trimmed = filename.trim().replace(/[/\\]/g, "_");
367
481
  const safe = trimmed
@@ -388,7 +502,11 @@ function retainedArtifactUnavailable(
388
502
  artifactId: string,
389
503
  reason: RetainedOutputUnavailableReason,
390
504
  ): RetainedArtifactMetadata {
391
- return RetainedArtifactMetadataSchema.parse({ available: false, artifactId, reason });
505
+ return RetainedArtifactMetadataSchema.parse({
506
+ available: false,
507
+ artifactId,
508
+ reason,
509
+ });
392
510
  }
393
511
 
394
512
  function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtifactMetadata {
@@ -417,6 +535,97 @@ function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtif
417
535
  return retainedArtifactUnavailable(file.id, "unsupported");
418
536
  }
419
537
 
538
+ async function getWorkspaceArtifact(
539
+ db: ApiRouteDeps["db"],
540
+ workspaceId: string,
541
+ artifactId: string,
542
+ ): Promise<{ file: RetainedFileArtifact["file"]; metadata: RetainedArtifactMetadata } | null> {
543
+ const generated = await getGeneratedImageArtifact(db, workspaceId, artifactId);
544
+ if (generated) {
545
+ return { file: generated.file, metadata: retainedGeneratedImageMetadata(generated) };
546
+ }
547
+ const generatedVideo = await getGeneratedVideoArtifact(db, workspaceId, artifactId);
548
+ if (generatedVideo && !generatedVideo.artifact.deletedAt) {
549
+ const file = generatedVideoFileAsset(generatedVideo.file);
550
+ return {
551
+ file,
552
+ metadata: retainedGeneratedVideoMetadata({
553
+ artifact: generatedVideo.artifact,
554
+ file,
555
+ }),
556
+ };
557
+ }
558
+ const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
559
+ return artifact ? { file: artifact.file, metadata: retainedArtifactMetadata(artifact) } : null;
560
+ }
561
+
562
+ function generatedVideoFileAsset(
563
+ file: NonNullable<Awaited<ReturnType<typeof getGeneratedVideoArtifact>>>["file"],
564
+ ): RetainedFileArtifact["file"] {
565
+ return FileAsset.parse({
566
+ ...file,
567
+ createdAt: file.createdAt.toISOString(),
568
+ updatedAt: file.updatedAt.toISOString(),
569
+ });
570
+ }
571
+
572
+ function retainedGeneratedVideoMetadata(artifact: {
573
+ artifact: GeneratedVideoArtifact;
574
+ file: RetainedFileArtifact["file"];
575
+ }): RetainedArtifactMetadata {
576
+ const reference = retainedGeneratedVideoReferenceFromFile({
577
+ ...artifact.file,
578
+ id: artifact.artifact.id,
579
+ width: artifact.artifact.width,
580
+ height: artifact.artifact.height,
581
+ updatedAt: artifact.artifact.readyAt.toISOString(),
582
+ });
583
+ return reference ?? retainedArtifactUnavailable(artifact.artifact.id, "failed");
584
+ }
585
+
586
+ function retainedGeneratedImageMetadata(
587
+ artifact: GeneratedImageArtifact,
588
+ ): RetainedArtifactMetadata {
589
+ if (artifact.status === "ready") {
590
+ const reference = retainedGeneratedImageReferenceFromFile({
591
+ ...artifact.file,
592
+ width: artifact.width,
593
+ height: artifact.height,
594
+ });
595
+ if (reference) return reference;
596
+ }
597
+ return retainedArtifactUnavailable(artifact.artifactId, "pending");
598
+ }
599
+
600
+ function retainedScreenshotMetadata(
601
+ artifact: RetainedScreenshotArtifact,
602
+ ): RetainedArtifactMetadata {
603
+ if (
604
+ artifact.status === "ready" &&
605
+ artifact.sessionId &&
606
+ artifact.retentionExpiresAt.getTime() > Date.now()
607
+ ) {
608
+ const reference = retainedScreenshotReferenceFromFile({
609
+ ...artifact.file,
610
+ sessionId: artifact.sessionId,
611
+ width: artifact.width,
612
+ height: artifact.height,
613
+ expiresAt: artifact.retentionExpiresAt.toISOString(),
614
+ });
615
+ if (reference) return reference;
616
+ }
617
+ if (artifact.status === "deleted") {
618
+ return retainedArtifactUnavailable(artifact.artifactId, "deleted");
619
+ }
620
+ if (artifact.status === "expired" || artifact.retentionExpiresAt.getTime() <= Date.now()) {
621
+ return retainedArtifactUnavailable(artifact.artifactId, "expired");
622
+ }
623
+ if (artifact.status === "pending" || artifact.status === "reconciling") {
624
+ return retainedArtifactUnavailable(artifact.artifactId, "pending");
625
+ }
626
+ return retainedArtifactUnavailable(artifact.artifactId, "failed");
627
+ }
628
+
420
629
  function retainedArtifactUnavailableStatus(
421
630
  reason: RetainedOutputUnavailableReason,
422
631
  ): 404 | 409 | 410 | 422 {
@@ -429,6 +638,9 @@ function retainedArtifactUnavailableStatus(
429
638
  case "unsupported":
430
639
  case "not_retained":
431
640
  case "storage_write_failed":
641
+ case "quota_exceeded":
642
+ case "invalid_content":
643
+ case "oversized":
432
644
  return 422;
433
645
  case "pending":
434
646
  case "failed":
@@ -38,6 +38,7 @@ import {
38
38
  githubBrowserBaseUrl,
39
39
  githubBrowserGrantClaims,
40
40
  githubBrowserGrantFromState,
41
+ githubSessionReturnPath,
41
42
  } from "../github-browser-flow";
42
43
  import {
43
44
  githubBindingStatus,
@@ -64,12 +65,14 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
64
65
  const status = githubBindingStatus(missing.length === 0, installations);
65
66
  const setupMode = settings.productAccessMode === "managed" ? "platform" : "operator";
66
67
  const canManage = hasPermission(grant.permissions, "github:manage");
68
+ const returnPath = githubSessionReturnPath(c.req.query("returnPath"), grant.workspaceId);
67
69
  const connectState =
68
70
  missing.length === 0 && slug && canManage
69
71
  ? createSignedState(githubStateSecret, {
70
72
  accountId: grant.accountId,
71
73
  workspaceId: grant.workspaceId,
72
74
  intent: "installation_authority",
75
+ ...(returnPath ? { returnPath } : {}),
73
76
  ...githubBrowserGrantClaims(settings, grant),
74
77
  })
75
78
  : null;
@@ -517,7 +520,12 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
517
520
  return c.html(
518
521
  githubSetupSuccessHtml(
519
522
  proof.installation.accountLogin ?? `installation ${installationId}`,
520
- openGeniReturnUrl(settings, c, grant.workspaceId),
523
+ openGeniReturnUrl(
524
+ settings,
525
+ c,
526
+ grant.workspaceId,
527
+ typeof statePayload.returnPath === "string" ? statePayload.returnPath : null,
528
+ ),
521
529
  ),
522
530
  );
523
531
  });
@@ -879,8 +887,16 @@ function openGeniReturnUrl(
879
887
  settings: ApiRouteDeps["settings"],
880
888
  c: Context,
881
889
  workspaceId: string,
890
+ returnPath: string | null = null,
882
891
  ): string {
883
- const url = new URL(openGeniBaseUrl(settings, c) || new URL(c.req.url).origin);
892
+ const baseUrl = openGeniBaseUrl(settings, c) || new URL(c.req.url).origin;
893
+ const safeReturnPath = githubSessionReturnPath(returnPath, workspaceId);
894
+ const url = safeReturnPath ? new URL(safeReturnPath, `${baseUrl}/`) : new URL(baseUrl);
895
+ if (safeReturnPath) {
896
+ url.searchParams.delete("capability_auth");
897
+ url.searchParams.set("github", "connected");
898
+ return url.toString();
899
+ }
884
900
  url.searchParams.set("workspaceId", workspaceId);
885
901
  return url.toString();
886
902
  }
@@ -1,3 +1,4 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { readFile, stat } from "node:fs/promises";
2
3
  import type { Hono } from "hono";
3
4
  import { HTTPException } from "hono/http-exception";
@@ -27,7 +28,12 @@ import type { ApiRouteDeps } from "@opengeni/core";
27
28
  // The committed install artifacts, resolved relative to this module so the API
28
29
  // (run from source under /app via bun) locates the sibling agent/install/ dir at
29
30
  // runtime. apps/api/src/routes -> ../../../../agent/install.
30
- const INSTALL_DIR = new URL("../../../../agent/install/", import.meta.url);
31
+ const INSTALL_DIR =
32
+ [
33
+ new URL("./assets/agent-install/", import.meta.url),
34
+ new URL("../../../../agent/install/", import.meta.url),
35
+ ].find((candidate) => existsSync(candidate)) ??
36
+ new URL("../../../../agent/install/", import.meta.url);
31
37
 
32
38
  // The baked release-binary dir (a sibling of the committed scripts). The build's
33
39
  // signing step writes the per-SHA Linux musl binaries + their `.sha256`/`.minisig`
@@ -200,6 +206,31 @@ export function registerInstallRoutes(app: Hono, deps: ApiRouteDeps): void {
200
206
  return serveAsset(asset, `${releasesBase}/download/${stableAgentTag}/${asset}`);
201
207
  });
202
208
 
209
+ // Signed self-update channel manifests. Each deployment exposes its own
210
+ // immutable promotion pointer, so an enrolled machine never depends on the
211
+ // public get.opengeni.ai hostname. The manifest and signature are ordinary
212
+ // assets on the selected immutable agent release.
213
+ for (const [channel, version] of [
214
+ ["stable", deps.settings.agentStableVersion],
215
+ ["beta", deps.settings.agentBetaVersion],
216
+ ] as const) {
217
+ app.get(`/agent/${channel}/:manifestAsset`, (c) => {
218
+ const manifestAsset = c.req.param("manifestAsset");
219
+ if (manifestAsset !== "manifest.json" && manifestAsset !== "manifest.json.minisig") {
220
+ throw new HTTPException(404, { message: "update manifest asset not found" });
221
+ }
222
+ if (!version) {
223
+ throw new HTTPException(404, { message: `${channel} agent channel is not configured` });
224
+ }
225
+ return new Response(null, {
226
+ status: 302,
227
+ headers: {
228
+ location: `${releasesBase}/download/agent-v${version}/${manifestAsset}`,
229
+ },
230
+ });
231
+ });
232
+ }
233
+
203
234
  // The version segment is the literal `v<ver>` (e.g. `v1.2.3`) — Hono cannot bind
204
235
  // a param glued to a literal prefix, so the whole segment is the param and the
205
236
  // `v` prefix is validated/stripped here. The release tag is `agent-v<ver>`.
@@ -220,5 +251,10 @@ export function registerInstallRoutes(app: Hono, deps: ApiRouteDeps): void {
220
251
  export const installExactPaths: ReadonlySet<string> = new Set(Object.keys(TEXT_ASSETS));
221
252
 
222
253
  export function isInstallRedirectPath(path: string): boolean {
223
- return path.startsWith("/agent/latest/") || path.startsWith("/agent/v");
254
+ return (
255
+ path.startsWith("/agent/latest/") ||
256
+ path.startsWith("/agent/v") ||
257
+ path.startsWith("/agent/stable/") ||
258
+ path.startsWith("/agent/beta/")
259
+ );
224
260
  }