@opengeni/api-router 0.5.2 → 0.5.4

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 (48) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-YY6OAEL6.js → chunk-DO2G3JSB.js} +5333 -2205
  3. package/dist/chunk-DO2G3JSB.js.map +1 -0
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +297 -54
  6. package/dist/index.js.map +1 -1
  7. package/package.json +21 -21
  8. package/src/app.ts +415 -147
  9. package/src/auth/managed-auth.ts +32 -16
  10. package/src/http/auth.ts +8 -1
  11. package/src/http/common.ts +6 -2
  12. package/src/http/sse.ts +27 -6
  13. package/src/index.ts +196 -74
  14. package/src/integrations/oauth-client.ts +592 -131
  15. package/src/integrations/provider-domain.ts +4 -1
  16. package/src/mcp/documents.ts +173 -94
  17. package/src/mcp/server.ts +1517 -692
  18. package/src/mcp/session-view.ts +8 -2
  19. package/src/mcp/toolspace.ts +175 -84
  20. package/src/observability.ts +7 -1
  21. package/src/routes/api-keys.ts +39 -23
  22. package/src/routes/billing.ts +180 -65
  23. package/src/routes/capabilities.ts +17 -8
  24. package/src/routes/catalog-assets.ts +5 -2
  25. package/src/routes/codex.ts +244 -63
  26. package/src/routes/connections.ts +72 -34
  27. package/src/routes/documents.ts +242 -92
  28. package/src/routes/enrollments.ts +100 -70
  29. package/src/routes/environments.ts +205 -136
  30. package/src/routes/files.ts +164 -39
  31. package/src/routes/github.ts +123 -50
  32. package/src/routes/install.ts +9 -2
  33. package/src/routes/machines.ts +9 -8
  34. package/src/routes/packs.ts +141 -89
  35. package/src/routes/rigs.ts +189 -0
  36. package/src/routes/scheduled-tasks.ts +51 -9
  37. package/src/routes/sessions.ts +839 -329
  38. package/src/routes/social.ts +50 -38
  39. package/src/routes/workspace-capture.ts +238 -0
  40. package/src/routes/workspaces.ts +159 -13
  41. package/src/sandbox/access.ts +11 -3
  42. package/src/sandbox/auth-callout.ts +5 -1
  43. package/src/sandbox/channel-a.ts +104 -27
  44. package/src/sandbox/enrollment.ts +13 -3
  45. package/src/sandbox/machines.ts +68 -59
  46. package/src/sandbox/metrics-ingestion.ts +238 -17
  47. package/src/sandbox/viewer.ts +172 -46
  48. package/dist/chunk-YY6OAEL6.js.map +0 -1
@@ -19,7 +19,13 @@
19
19
  // lease's recorded data_plane_url (null until P4 mints it).
20
20
 
21
21
  import { createHash } from "node:crypto";
22
- import { applyGitAuthPointerEnvironment, hasGitHubRepositorySelection, resolveStreamTokenSecret, stableSandboxEnvironmentForRun } from "@opengeni/config";
22
+ import {
23
+ applyGitAuthPointerEnvironment,
24
+ hasGitCredentialRepositorySelection,
25
+ hasGitHubRepositorySelection,
26
+ resolveStreamTokenSecret,
27
+ stableSandboxEnvironmentForRun,
28
+ } from "@opengeni/config";
23
29
  import type { Settings } from "@opengeni/config";
24
30
  import { githubAppBotIdentity } from "@opengeni/github";
25
31
  import { type Session, type StreamUrlRotatedPayload } from "@opengeni/contracts";
@@ -31,6 +37,7 @@ import {
31
37
  getSandboxSessionEnvelope,
32
38
  heartbeatLeaseHolder,
33
39
  loadWorkspaceEnvironmentForRun,
40
+ markWarmLeaseInstanceLost,
34
41
  readLease,
35
42
  recordLeaseDataPlaneUrl,
36
43
  recordLeaseTerminalDataPlaneUrl,
@@ -50,6 +57,8 @@ import {
50
57
  ensureDisplayStack,
51
58
  ensureTerminalServer,
52
59
  establishSandboxSessionFromEnvelope,
60
+ isProviderSandboxNotFoundError,
61
+ SandboxResumeStateUnavailableError,
53
62
  exposeStreamPort,
54
63
  desktopCapableBackend,
55
64
  NatsControlRpc,
@@ -133,12 +142,22 @@ export async function sessionAttachEnvironment(
133
142
  // attach env keyed off the deployment default would cold-create e.g. an e2b
134
143
  // session's box with /workspace-rooted values while its turn declares
135
144
  // /home/user-rooted ones — the same guard-killed first turn all over again.
136
- const settingsForSession = session.sandboxBackend !== services.settings.sandboxBackend
137
- ? { ...services.settings, sandboxBackend: session.sandboxBackend }
138
- : services.settings;
139
- const environment = stableSandboxEnvironmentForRun(settingsForSession, workspaceEnvironment?.values ?? {}, { workspaceId });
140
- if (hasGitHubRepositorySelection(session.resources)) {
141
- applyGitAuthPointerEnvironment(environment, githubAppBotIdentity(services.settings));
145
+ const settingsForSession =
146
+ session.sandboxBackend !== services.settings.sandboxBackend
147
+ ? { ...services.settings, sandboxBackend: session.sandboxBackend }
148
+ : services.settings;
149
+ const environment = stableSandboxEnvironmentForRun(
150
+ settingsForSession,
151
+ workspaceEnvironment?.values ?? {},
152
+ { workspaceId },
153
+ );
154
+ if (hasGitCredentialRepositorySelection(session.resources)) {
155
+ applyGitAuthPointerEnvironment(
156
+ environment,
157
+ hasGitHubRepositorySelection(session.resources)
158
+ ? githubAppBotIdentity(services.settings)
159
+ : null,
160
+ );
142
161
  }
143
162
  return environment;
144
163
  }
@@ -182,13 +201,16 @@ export async function attachViewer(
182
201
  backend: session.sandboxBackend,
183
202
  os: session.sandboxOs,
184
203
  leaseTtlMs,
204
+ warmingLeaseTtlMs: settings.sandboxWarmingTimeoutMs,
185
205
  });
186
206
 
187
207
  // FENCED: a newer epoch re-established the box. Release our just-registered
188
208
  // holder and surface a 409 — the client re-reads capabilities and re-attaches.
189
209
  if (acquired.role === "fenced") {
190
210
  await release();
191
- throw new HTTPException(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); re-read capabilities and re-attach` });
211
+ throw new HTTPException(409, {
212
+ message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); re-read capabilities and re-attach`,
213
+ });
192
214
  }
193
215
 
194
216
  // SPAWNER: we won the cold->warming CAS. Establish the box in-process from the
@@ -216,6 +238,7 @@ export async function attachViewer(
216
238
  const spawnEnvelope = acquired.lease.resumeState ?? envelope;
217
239
  established = await establishSandboxSessionFromEnvelope(settings, spawnEnvelope, {
218
240
  sessionId: session.id,
241
+ recovery: "create-or-restore",
219
242
  backendOverride: session.sandboxBackend,
220
243
  environment,
221
244
  });
@@ -224,7 +247,8 @@ export async function attachViewer(
224
247
  // call, the reaper) resumes THIS box by id instead of cold-creating a
225
248
  // rival. Fall back to the session envelope only when serialize is
226
249
  // unavailable. (Without this the box churned: each op spawned its own box.)
227
- const resumeEnvelope = (await serializeEstablishedSandboxEnvelope(established)) ?? envelope ?? null;
250
+ const resumeEnvelope =
251
+ (await serializeEstablishedSandboxEnvelope(established)) ?? envelope ?? null;
228
252
  const committed = await commitWarmingToWarm(db, {
229
253
  accountId,
230
254
  workspaceId,
@@ -254,7 +278,9 @@ export async function attachViewer(
254
278
  };
255
279
  } catch (error) {
256
280
  if (error instanceof SandboxLeaseSupersededError) {
257
- throw new HTTPException(409, { message: `sandbox lease superseded (epoch ${error.leaseEpoch}); re-read capabilities and re-attach` });
281
+ throw new HTTPException(409, {
282
+ message: `sandbox lease superseded (epoch ${error.leaseEpoch}); re-read capabilities and re-attach`,
283
+ });
258
284
  }
259
285
  // Caught spawn failure: roll the warming row back to cold so the next
260
286
  // arrival (a turn or another viewer) re-acquires and re-spawns. Holders
@@ -268,7 +294,9 @@ export async function attachViewer(
268
294
  // to cold, so a re-attach re-acquires and re-spawns. Preserve an already-typed
269
295
  // HTTPException unchanged.
270
296
  if (error instanceof HTTPException) throw error;
271
- throw new HTTPException(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
297
+ throw new HTTPException(409, {
298
+ message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})`,
299
+ });
272
300
  } finally {
273
301
  // Drop the in-process handle: the API resumed BY ID for the cold-spawn,
274
302
  // it does NOT own the box. The lease's refcount (this viewer holder) keeps
@@ -297,7 +325,13 @@ export async function attachViewer(
297
325
  */
298
326
  export async function heartbeatViewer(
299
327
  services: ViewerServices,
300
- input: { accountId: string; workspaceId: string; sandboxGroupId: string; viewerId: string; expectedEpoch: number },
328
+ input: {
329
+ accountId: string;
330
+ workspaceId: string;
331
+ sandboxGroupId: string;
332
+ viewerId: string;
333
+ expectedEpoch: number;
334
+ },
301
335
  ): Promise<boolean> {
302
336
  return await heartbeatLeaseHolder(services.db, {
303
337
  accountId: input.accountId,
@@ -357,12 +391,52 @@ export function viewerHeartbeatIntervalMs(settings: Settings): number {
357
391
  // (the lease showed warm while Modal showed the box gone; reads 404'd against a
358
392
  // fresh box). We therefore DO NOT call session.close()/shutdown()/delete() — the
359
393
  // reaper (provider stop at refcount 0) is the ONLY sanctioned box terminator.
360
- async function dropEstablishedHandle(established: EstablishedSandboxSession | undefined): Promise<void> {
394
+ async function dropEstablishedHandle(
395
+ established: EstablishedSandboxSession | undefined,
396
+ ): Promise<void> {
361
397
  // Intentionally a no-op beyond dropping the reference: terminating the box here
362
398
  // is wrong (see above). The lease owns lifecycle; the reaper owns teardown.
363
399
  void established;
364
400
  }
365
401
 
402
+ /** A stream resolver is an attached observer, never a box creator. If its
403
+ * resume-only call proves the exact warm provider instance gone, retire that
404
+ * epoch atomically so the next ordinary attach elects one replacement owner. */
405
+ async function retireMissingWarmLease(
406
+ services: ViewerServices,
407
+ input: {
408
+ accountId: string;
409
+ workspaceId: string;
410
+ session: Session;
411
+ lease: LeaseSnapshot;
412
+ },
413
+ error: unknown,
414
+ ): Promise<boolean> {
415
+ if (
416
+ input.lease.instanceId === null ||
417
+ (!(error instanceof SandboxResumeStateUnavailableError) &&
418
+ !isProviderSandboxNotFoundError(input.session.sandboxBackend, error))
419
+ ) {
420
+ return false;
421
+ }
422
+ const marked = await markWarmLeaseInstanceLost(services.db, {
423
+ accountId: input.accountId,
424
+ workspaceId: input.workspaceId,
425
+ sandboxGroupId: input.session.sandboxGroupId,
426
+ expectedEpoch: input.lease.leaseEpoch,
427
+ expectedInstanceId: input.lease.instanceId,
428
+ });
429
+ if (marked.status === "marked" && services.bus) {
430
+ await appendAndPublishEvents(services.db, services.bus, input.workspaceId, input.session.id, [
431
+ {
432
+ type: "sandbox.box.lost",
433
+ payload: { sandboxId: input.lease.instanceId },
434
+ },
435
+ ]);
436
+ }
437
+ return true;
438
+ }
439
+
366
440
  // ============================================================================
367
441
  // P4.2 — the pixel DATA PLANE, served API-DIRECT.
368
442
  //
@@ -397,7 +471,11 @@ async function dropEstablishedHandle(established: EstablishedSandboxSession | un
397
471
  export function resolveActiveDesktopTransport(
398
472
  selfhostedActive: boolean,
399
473
  interactive: boolean,
400
- ): { transport: "relay-frames" | "vnc-ws"; client: "frames" | "novnc"; mode: "read-only" | "interactive" } {
474
+ ): {
475
+ transport: "relay-frames" | "vnc-ws";
476
+ client: "frames" | "novnc";
477
+ mode: "read-only" | "interactive";
478
+ } {
401
479
  if (selfhostedActive) {
402
480
  return { transport: "relay-frames", client: "frames", mode: "read-only" };
403
481
  }
@@ -432,12 +510,12 @@ export type MintDesktopStreamInput = {
432
510
  * real leaf `establishSandboxSessionFromEnvelope`. Production NEVER passes
433
511
  * this; it exists so a real-lease integration test can inject a fake provider
434
512
  * session carrying `resolveExposedPort` without a live cloud box. */
435
- establish?: (
436
- envelope: Record<string, unknown> | null,
437
- ) => Promise<EstablishedSandboxSession>;
513
+ establish?: (envelope: Record<string, unknown> | null) => Promise<EstablishedSandboxSession>;
438
514
  /** Test seam: inject a fake relay-resolving session for the selfhosted-active
439
515
  * branch. Production NEVER passes this. */
440
- resolveSelfhostedSession?: (sandbox: SandboxRecord) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>;
516
+ resolveSelfhostedSession?: (
517
+ sandbox: SandboxRecord,
518
+ ) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>;
441
519
  };
442
520
 
443
521
  /**
@@ -486,9 +564,27 @@ export async function mintDesktopStream(
486
564
  if (session.activeSandboxId) {
487
565
  const active = await getSandbox(db, workspaceId, session.activeSandboxId);
488
566
  if (active?.kind === "selfhosted") {
489
- const m = await tryMintActiveSelfhostedStream(services, { session, viewerId: input.viewerId, workspaceId, port: DESKTOP_STREAM_PORT, sandbox: active }, input.resolveSelfhostedSession);
567
+ const m = await tryMintActiveSelfhostedStream(
568
+ services,
569
+ {
570
+ session,
571
+ viewerId: input.viewerId,
572
+ workspaceId,
573
+ port: DESKTOP_STREAM_PORT,
574
+ sandbox: active,
575
+ },
576
+ input.resolveSelfhostedSession,
577
+ );
490
578
  // mintSelfhostedStream returns no resolution; the desktop cell needs it.
491
- return m ? { url: m.url, token: m.token, expiresAt: m.expiresAt, resolution: defaultResolution(settings), leaseEpoch: m.leaseEpoch } : null;
579
+ return m
580
+ ? {
581
+ url: m.url,
582
+ token: m.token,
583
+ expiresAt: m.expiresAt,
584
+ resolution: defaultResolution(settings),
585
+ leaseEpoch: m.leaseEpoch,
586
+ }
587
+ : null;
492
588
  }
493
589
  // A Modal swap target (or unknown) falls through to the existing group-box path.
494
590
  }
@@ -526,22 +622,28 @@ export async function mintDesktopStream(
526
622
  };
527
623
  }
528
624
 
529
- // Resume the LIVE box by id. The lease's resume_state is authoritative (it is
530
- // the box the lease currently fences); fall back to the session envelope only
531
- // when the lease has none (a freshly-warmed lease always has it).
532
- const envelope = lease.resumeState ?? (await getSandboxSessionEnvelope(db, workspaceId, session.id));
625
+ // Resume the LIVE box by id from the authoritative lease descriptor. An
626
+ // attached stream resolver has no creation authority and never substitutes a
627
+ // per-session envelope for missing lease state.
628
+ const envelope = lease.resumeState;
533
629
  let established: EstablishedSandboxSession | undefined;
534
630
  try {
535
631
  // On a cold-restore (the lease's box is gone) this create() must carry the
536
632
  // SAME stable run-env the turn declares, so a later turn finds no env delta.
537
633
  const environment = await sessionAttachEnvironment(services, workspaceId, session);
538
- established = input.establish
539
- ? await input.establish(envelope)
540
- : await establishSandboxSessionFromEnvelope(settings, envelope, {
541
- sessionId: session.id,
542
- backendOverride: session.sandboxBackend,
543
- environment,
544
- });
634
+ try {
635
+ established = input.establish
636
+ ? await input.establish(envelope)
637
+ : await establishSandboxSessionFromEnvelope(settings, envelope, {
638
+ sessionId: session.id,
639
+ recovery: "resume-only",
640
+ backendOverride: session.sandboxBackend,
641
+ environment,
642
+ });
643
+ } catch (error) {
644
+ await retireMissingWarmLease(services, { accountId, workspaceId, session, lease }, error);
645
+ return null;
646
+ }
545
647
 
546
648
  // Idempotent display stack (flock-guarded; a no-op when already up). A box
547
649
  // that genuinely can't run the stack degrades to transport:null, not a throw.
@@ -666,12 +768,12 @@ export type MintTerminalStreamInput = {
666
768
  lease?: LeaseSnapshot;
667
769
  /** Test seam: override how the box is re-established by id (see
668
770
  * MintDesktopStreamInput.establish). Production NEVER passes this. */
669
- establish?: (
670
- envelope: Record<string, unknown> | null,
671
- ) => Promise<EstablishedSandboxSession>;
771
+ establish?: (envelope: Record<string, unknown> | null) => Promise<EstablishedSandboxSession>;
672
772
  /** Test seam: inject a fake relay-resolving session for the selfhosted-active
673
773
  * branch. Production NEVER passes this. */
674
- resolveSelfhostedSession?: (sandbox: SandboxRecord) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>;
774
+ resolveSelfhostedSession?: (
775
+ sandbox: SandboxRecord,
776
+ ) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>;
675
777
  };
676
778
 
677
779
  /**
@@ -712,7 +814,17 @@ export async function mintTerminalStream(
712
814
  if (session.activeSandboxId) {
713
815
  const active = await getSandbox(db, workspaceId, session.activeSandboxId);
714
816
  if (active?.kind === "selfhosted") {
715
- return await tryMintActiveSelfhostedStream(services, { session, viewerId: input.viewerId, workspaceId, port: TERMINAL_STREAM_PORT, sandbox: active }, input.resolveSelfhostedSession);
817
+ return await tryMintActiveSelfhostedStream(
818
+ services,
819
+ {
820
+ session,
821
+ viewerId: input.viewerId,
822
+ workspaceId,
823
+ port: TERMINAL_STREAM_PORT,
824
+ sandbox: active,
825
+ },
826
+ input.resolveSelfhostedSession,
827
+ );
716
828
  }
717
829
  // A Modal swap target (or unknown) falls through to the existing group-box path
718
830
  // (unchanged — Modal swap-target streaming is out of scope for this fix).
@@ -748,19 +860,25 @@ export async function mintTerminalStream(
748
860
 
749
861
  // Resume the LIVE box by id (lease.resume_state authoritative), ensure ttyd, and
750
862
  // resolve the 7681 tunnel + mint the scoped token, IN-PROCESS.
751
- const envelope = lease.resumeState ?? (await getSandboxSessionEnvelope(db, workspaceId, session.id));
863
+ const envelope = lease.resumeState;
752
864
  let established: EstablishedSandboxSession | undefined;
753
865
  try {
754
866
  // On a cold-restore this create() must carry the SAME stable run-env the turn
755
867
  // declares, so a later turn finds no manifest-env delta.
756
868
  const environment = await sessionAttachEnvironment(services, workspaceId, session);
757
- established = input.establish
758
- ? await input.establish(envelope)
759
- : await establishSandboxSessionFromEnvelope(settings, envelope, {
760
- sessionId: session.id,
761
- backendOverride: session.sandboxBackend,
762
- environment,
763
- });
869
+ try {
870
+ established = input.establish
871
+ ? await input.establish(envelope)
872
+ : await establishSandboxSessionFromEnvelope(settings, envelope, {
873
+ sessionId: session.id,
874
+ recovery: "resume-only",
875
+ backendOverride: session.sandboxBackend,
876
+ environment,
877
+ });
878
+ } catch (error) {
879
+ await retireMissingWarmLease(services, { accountId, workspaceId, session, lease }, error);
880
+ return null;
881
+ }
764
882
 
765
883
  // Idempotent ttyd launch (flock-guarded; a no-op when already up). A box that
766
884
  // genuinely can't run it degrades to the sse-events firehose, not a throw.
@@ -853,10 +971,18 @@ function controlRpc(bus: EventBus | undefined): ControlRpc {
853
971
  */
854
972
  async function tryMintActiveSelfhostedStream(
855
973
  services: ViewerServices,
856
- input: { session: Session; viewerId: string; workspaceId: string; port: number; sandbox: SandboxRecord },
974
+ input: {
975
+ session: Session;
976
+ viewerId: string;
977
+ workspaceId: string;
978
+ port: number;
979
+ sandbox: SandboxRecord;
980
+ },
857
981
  // optional test seam (mirrors the existing `establish?` seam pattern): inject a
858
982
  // fake relay-resolving session; production NEVER passes it.
859
- resolveSelfhostedSession?: (sandbox: SandboxRecord) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>,
983
+ resolveSelfhostedSession?: (
984
+ sandbox: SandboxRecord,
985
+ ) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>,
860
986
  ): Promise<TerminalStreamMint | null> {
861
987
  const { settings, bus } = services;
862
988
  const { session, workspaceId, port, sandbox } = input;