@opengeni/api-router 0.21.14 → 0.23.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 (54) hide show
  1. package/dist/app.d.ts +1 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +0 -30
  4. package/dist/{chunk-R5PDSH2A.js → chunk-T4T2PGU4.js} +3989 -1356
  5. package/dist/chunk-T4T2PGU4.js.map +1 -0
  6. package/dist/http/sse.d.ts +2 -0
  7. package/dist/index.js +29 -9
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/oauth-client.d.ts +8 -0
  10. package/dist/integrations/slack-interactions.d.ts +7 -1
  11. package/dist/mcp/receipts.d.ts +28 -0
  12. package/dist/mcp/scheduled-task-view.d.ts +350 -0
  13. package/dist/mcp/toolspace.d.ts +9 -0
  14. package/dist/routes/transcription-recordings.d.ts +3 -0
  15. package/dist/sandbox/auth-callout.d.ts +2 -0
  16. package/dist/sandbox/channel-a.d.ts +5 -1
  17. package/dist/transcription/segmenter.d.ts +10 -0
  18. package/dist/transcription/service.d.ts +5 -0
  19. package/package.json +12 -12
  20. package/src/app.ts +39 -6
  21. package/src/auth/managed-auth.ts +0 -16
  22. package/src/http/sse.ts +101 -6
  23. package/src/index.ts +28 -3
  24. package/src/integrations/oauth-client.ts +36 -56
  25. package/src/integrations/slack-interactions.ts +123 -15
  26. package/src/mcp/documents.ts +42 -25
  27. package/src/mcp/receipts.ts +95 -0
  28. package/src/mcp/scheduled-task-view.ts +608 -0
  29. package/src/mcp/server.ts +812 -182
  30. package/src/mcp/toolspace.ts +75 -71
  31. package/src/observability.ts +3 -3
  32. package/src/routes/api-keys.ts +7 -1
  33. package/src/routes/codex.ts +7 -4
  34. package/src/routes/connections.ts +74 -3
  35. package/src/routes/enrollments.ts +54 -12
  36. package/src/routes/environments.ts +60 -11
  37. package/src/routes/files.ts +175 -65
  38. package/src/routes/install.ts +31 -1
  39. package/src/routes/machines.ts +1 -1
  40. package/src/routes/scheduled-tasks.ts +39 -14
  41. package/src/routes/sessions.ts +77 -12
  42. package/src/routes/transcription-recordings.ts +754 -0
  43. package/src/routes/transcriptions.ts +2 -0
  44. package/src/sandbox/auth-callout.ts +16 -4
  45. package/src/sandbox/channel-a.ts +124 -7
  46. package/src/sandbox/enrollment.ts +13 -3
  47. package/src/sandbox/machines.ts +1 -1
  48. package/src/sandbox/viewer.ts +29 -20
  49. package/src/transcription/providers/azure-openai.ts +4 -3
  50. package/src/transcription/providers/codex-subscription.ts +4 -1
  51. package/src/transcription/providers/openai.ts +7 -2
  52. package/src/transcription/segmenter.ts +260 -0
  53. package/src/transcription/service.ts +111 -10
  54. package/dist/chunk-R5PDSH2A.js.map +0 -1
@@ -12,6 +12,7 @@ import {
12
12
  EditSessionQueueItemRequest,
13
13
  EndSessionRealtimeRequest,
14
14
  FsDeleteRequest,
15
+ FsListBatchRequest,
15
16
  FsListRequest,
16
17
  FsMkdirRequest,
17
18
  FsMoveRequest,
@@ -19,6 +20,7 @@ import {
19
20
  FsWriteRequest,
20
21
  HumanInputRequestStatus,
21
22
  GitDiffRequest,
23
+ GitReadBatchRequest,
22
24
  GitLogRequest,
23
25
  GitShowRequest,
24
26
  GitStatusRequest,
@@ -148,6 +150,7 @@ import { HTTPException } from "hono/http-exception";
148
150
  import type { ContentfulStatusCode } from "hono/utils/http-status";
149
151
  import {
150
152
  requireAccessGrant,
153
+ requirePermission,
151
154
  requireSessionAuthorization,
152
155
  requireSessionAuthorizationListScope,
153
156
  SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
@@ -333,6 +336,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
333
336
  const authorizeSessionHttp: MiddlewareHandler = async (c, next) => {
334
337
  const workspaceId = c.req.param("workspaceId") ?? "";
335
338
  const sessionId = c.req.param("sessionId") ?? "";
339
+ // Reject malformed route identifiers before the authorization resolver
340
+ // reaches UUID-typed persistence queries. Besides avoiding a needless DB
341
+ // round trip, this preserves the session surface's non-enumerating 404
342
+ // contract instead of leaking a driver-level 500.
343
+ if (!z.string().uuid().safeParse(sessionId).success) {
344
+ throw new HTTPException(404, { message: "session not found" });
345
+ }
336
346
  const operation = sessionAuthorizationOperationForHttp(
337
347
  c.req.method,
338
348
  new URL(c.req.url).pathname,
@@ -1342,11 +1352,12 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1342
1352
  if (event) {
1343
1353
  try {
1344
1354
  await bus.publish(workspaceId, sessionId, [event]);
1345
- } catch (error) {
1346
- console.warn(
1347
- `[api] live publish failed for cleared goal ${workspaceId}/${sessionId}; event is durable and reconciles on replay`,
1348
- error,
1349
- );
1355
+ } catch {
1356
+ console.warn("[api] cleared-goal live publish failed; durable event reconciles on replay", {
1357
+ errorClass: "EventPublishOperationError",
1358
+ errorCode: "cleared_goal_live_publish_failed",
1359
+ origin: "api",
1360
+ });
1350
1361
  }
1351
1362
  }
1352
1363
  return c.body(null, 204);
@@ -2190,6 +2201,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2190
2201
  : {}),
2191
2202
  });
2192
2203
 
2204
+ const repositoryRoots = [
2205
+ ...new Set(
2206
+ session.resources.flatMap((resource) =>
2207
+ resource.kind === "repository" && typeof resource.mountPath === "string"
2208
+ ? [resource.mountPath.replace(/^\/+|\/+$/g, "")]
2209
+ : [],
2210
+ ),
2211
+ ),
2212
+ ].filter(Boolean);
2213
+
2193
2214
  // SWAP-CASE desktop transport (BOTH directions): negotiateCapabilities keyed on
2194
2215
  // the HOME backend, but the pixel plane actually runs on the ACTIVE sandbox — and
2195
2216
  // the two backends use DIFFERENT wire transports. The advertised transport MUST
@@ -2209,7 +2230,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2209
2230
  // active sandbox kind is "selfhosted") — EXACTLY mintDesktopStream's routing. When
2210
2231
  // the desktop is available we set the transport from the ACTIVE sandbox in one
2211
2232
  // place (resolveActiveDesktopTransport), covering BOTH swap directions.
2212
- let responseCapabilities = capabilities;
2233
+ let responseCapabilities = {
2234
+ ...capabilities,
2235
+ Git: {
2236
+ ...capabilities.Git,
2237
+ repos: capabilities.Git.available ? repositoryRoots : [],
2238
+ },
2239
+ };
2213
2240
  if (capabilities.DesktopStream.transport !== null) {
2214
2241
  const activeSandbox = session.activeSandboxId
2215
2242
  ? await getSandbox(db, workspaceId, session.activeSandboxId)
@@ -2219,7 +2246,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2219
2246
  settings.sandboxDesktopInteractive !== false,
2220
2247
  );
2221
2248
  responseCapabilities = {
2222
- ...capabilities,
2249
+ ...responseCapabilities,
2223
2250
  DesktopStream: { ...capabilities.DesktopStream, ...wire },
2224
2251
  };
2225
2252
  }
@@ -2273,7 +2300,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2273
2300
  // when cold).
2274
2301
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers", async (c) => {
2275
2302
  const workspaceId = c.req.param("workspaceId");
2276
- const grant = await requireAccessGrant(c, deps, workspaceId, "stream:view");
2303
+ // Authenticate and bind the workspace before parsing. The requested plane
2304
+ // determines the narrower permission below: terminal-only holders must not
2305
+ // require the strictly broader un-redacted Desktop permission.
2306
+ const grant = await requireAccessGrant(c, deps, workspaceId);
2277
2307
  assertOwnershipEnabled();
2278
2308
  const sessionId = c.req.param("sessionId");
2279
2309
  const session = await getSession(db, workspaceId, sessionId);
@@ -2297,6 +2327,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2297
2327
  // read-only sse-events firehose forever ("read only"), and with the desktop tier
2298
2328
  // off by default there was no consent flow to ever clear the gate.
2299
2329
  const wantDesktop = parsed.data.desktop ?? false;
2330
+ requirePermission(grant, wantDesktop ? "stream:view" : "terminal:attach");
2300
2331
  const { shared } = await resolveSharedExposure(workspaceId, session);
2301
2332
  if (wantDesktop) {
2302
2333
  const ack = await getStreamAcknowledgment(db, {
@@ -2443,12 +2474,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2443
2474
  });
2444
2475
 
2445
2476
  // POST .../viewers/:viewerId/heartbeat — refresh the holder TTL (epoch-fenced).
2446
- // The desktop-stream lifecycle is gated on stream:view (the un-redacted plane).
2477
+ // A holder can represent the terminal-only plane, so lifecycle control uses
2478
+ // terminal:attach. Desktop callers continue to pass via workspace:admin.
2447
2479
  app.post(
2448
2480
  "/v1/workspaces/:workspaceId/sessions/:sessionId/viewers/:viewerId/heartbeat",
2449
2481
  async (c) => {
2450
2482
  const workspaceId = c.req.param("workspaceId");
2451
- const grant = await requireAccessGrant(c, deps, workspaceId, "stream:view");
2483
+ const grant = await requireAccessGrant(c, deps, workspaceId, "terminal:attach");
2452
2484
  assertOwnershipEnabled();
2453
2485
  const sessionId = c.req.param("sessionId");
2454
2486
  const session = await getSession(db, workspaceId, sessionId);
@@ -2478,7 +2510,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2478
2510
  // DELETE .../viewers/:viewerId — release the holder (idempotent).
2479
2511
  app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers/:viewerId", async (c) => {
2480
2512
  const workspaceId = c.req.param("workspaceId");
2481
- const grant = await requireAccessGrant(c, deps, workspaceId, "stream:view");
2513
+ const grant = await requireAccessGrant(c, deps, workspaceId, "terminal:attach");
2482
2514
  assertOwnershipEnabled();
2483
2515
  const sessionId = c.req.param("sessionId");
2484
2516
  const session = await getSession(db, workspaceId, sessionId);
@@ -2596,6 +2628,19 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2596
2628
  return c.json(out);
2597
2629
  });
2598
2630
 
2631
+ app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/fs/list-batch", async (c) => {
2632
+ const ctx = await channelAPreamble(c, "files:read");
2633
+ const req = await parseChannelABody(c, FsListBatchRequest);
2634
+ const out = await withChannelA(channelAServices, ctx, async ({ service }) => ({
2635
+ results: await Promise.all(
2636
+ req.requests.map((request) =>
2637
+ Promise.resolve().then(async () => await service.fsList(request)),
2638
+ ),
2639
+ ),
2640
+ }));
2641
+ return c.json(out);
2642
+ });
2643
+
2599
2644
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/fs/read", async (c) => {
2600
2645
  const ctx = await channelAPreamble(c, "files:read");
2601
2646
  const req = await parseChannelABody(c, FsReadRequest);
@@ -2646,6 +2691,26 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2646
2691
  return c.json(out);
2647
2692
  });
2648
2693
 
2694
+ app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/git/read-batch", async (c) => {
2695
+ const ctx = await channelAPreamble(c, "files:read");
2696
+ const req = await parseChannelABody(c, GitReadBatchRequest);
2697
+ const out = await withChannelA(channelAServices, ctx, async ({ service }) => ({
2698
+ results: await Promise.all(
2699
+ req.requests.map(async (request) => {
2700
+ const diffRequest = request.diff;
2701
+ const [status, diff] = await Promise.all([
2702
+ Promise.resolve().then(async () => await service.gitStatus(request.status)),
2703
+ diffRequest
2704
+ ? Promise.resolve().then(async () => await service.gitDiff(diffRequest))
2705
+ : Promise.resolve(undefined),
2706
+ ]);
2707
+ return { status, ...(diff ? { diff } : {}) };
2708
+ }),
2709
+ ),
2710
+ }));
2711
+ return c.json(out);
2712
+ });
2713
+
2649
2714
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/git/log", async (c) => {
2650
2715
  const ctx = await channelAPreamble(c, "files:read");
2651
2716
  const req = await parseChannelABody(c, GitLogRequest);
@@ -3101,7 +3166,7 @@ export function sessionAuthorizationOperationForHttp(
3101
3166
  if (suffix.startsWith("/viewers/") && ["POST", "DELETE"].includes(verb)) {
3102
3167
  return "session.viewer.control";
3103
3168
  }
3104
- if (suffix === "/fs/list" || suffix === "/fs/read") {
3169
+ if (suffix === "/fs/list" || suffix === "/fs/list-batch" || suffix === "/fs/read") {
3105
3170
  return verb === "POST" ? "session.files.read" : null;
3106
3171
  }
3107
3172
  if (["/fs/write", "/fs/delete", "/fs/move", "/fs/mkdir"].includes(suffix)) {