@opengeni/api-router 0.12.7 → 0.12.12

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.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  GitHubAppManifestCreate,
3
3
  type AccessGrant,
4
+ type GitHubInstallationBindingCandidate,
4
5
  type GitHubInstallationBindingProof,
5
6
  } from "@opengeni/contracts";
6
7
  import {
@@ -13,6 +14,7 @@ import {
13
14
  buildGitHubAppManifest,
14
15
  convertGitHubAppManifest,
15
16
  createSignedState,
17
+ discoverGitHubInstallationBindingCandidates,
16
18
  envLinesFromGitHubManifestConversion,
17
19
  GitHubAppApiError,
18
20
  GitHubAppConfigurationError,
@@ -47,7 +49,6 @@ const githubStateCookie = "opengeni_github_state";
47
49
  const githubBindingStateMaxAgeSeconds = 10 * 60;
48
50
  const legacyInstallationChooserDisabledMessage =
49
51
  "The legacy repository-admin GitHub installation chooser is disabled; use the GitHub owner-consent connect flow";
50
-
51
52
  export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
52
53
  const { db, settings, githubStateSecret } = deps;
53
54
 
@@ -61,6 +62,7 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
61
62
  ? await listWorkspaceGitHubInstallationBindings(deps, grant.workspaceId)
62
63
  : [];
63
64
  const status = githubBindingStatus(missing.length === 0, installations);
65
+ const setupMode = settings.productAccessMode === "managed" ? "platform" : "operator";
64
66
  const canManage = hasPermission(grant.permissions, "github:manage");
65
67
  const connectState =
66
68
  missing.length === 0 && slug && canManage
@@ -74,23 +76,29 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
74
76
  const connectUrl = connectState
75
77
  ? `${openGeniBaseUrl(settings, c)}/v1/workspaces/${grant.workspaceId}/github/connect?state=${encodeURIComponent(connectState)}`
76
78
  : null;
79
+ const installationViews = installations.map((installation) => ({
80
+ ...installation,
81
+ configureUrl: connectState
82
+ ? `${openGeniBaseUrl(settings, c)}/v1/workspaces/${grant.workspaceId}/github/installations/${installation.installationId}/configure?state=${encodeURIComponent(connectState)}`
83
+ : null,
84
+ }));
77
85
  return c.json({
78
86
  configured: missing.length === 0,
79
87
  status,
80
- appId: settings.githubAppId ?? null,
81
- clientId: settings.githubClientId ?? null,
82
- appSlug: slug,
88
+ setupMode,
89
+ appId: setupMode === "operator" ? (settings.githubAppId ?? null) : null,
90
+ clientId: setupMode === "operator" ? (settings.githubClientId ?? null) : null,
91
+ appSlug: setupMode === "operator" ? slug : null,
83
92
  installUrl: connectUrl,
84
93
  linkUrl: connectUrl,
85
- installations,
86
- missing,
94
+ installations: installationViews,
95
+ missing: setupMode === "operator" ? missing : [],
87
96
  });
88
97
  });
89
98
 
90
- // The signed state is a short browser handoff minted only for an OpenGeni
91
- // github:manage grant. GitHub remains responsible for installation/config
92
- // consent; the later OAuth callback independently proves current owner
93
- // authority before any workspace binding write.
99
+ // Start with user authorization, not GitHub's install/configure selector.
100
+ // This lets an owner link an already-installed App without relying on
101
+ // GitHub's Configure page to preserve or return OpenGeni state.
94
102
  app.get("/v1/workspaces/:workspaceId/github/connect", async (c) => {
95
103
  const workspaceId = c.req.param("workspaceId");
96
104
  const state = c.req.query("state");
@@ -107,8 +115,8 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
107
115
  ) {
108
116
  throw new HTTPException(400, { message: "invalid or expired GitHub installation state" });
109
117
  }
110
- const slug = settings.githubAppSlug?.trim();
111
- if (!slug || githubAppMissingSettings(settings).length > 0) {
118
+ const clientId = settings.githubClientId?.trim();
119
+ if (!clientId || githubAppMissingSettings(settings).length > 0) {
112
120
  throw new HTTPException(409, {
113
121
  message: JSON.stringify({
114
122
  message: "GitHub App is not configured",
@@ -116,9 +124,19 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
116
124
  }),
117
125
  });
118
126
  }
119
- setGitHubStateCookie(c, deps, state);
127
+ const discoveryState = createSignedState(githubStateSecret, {
128
+ accountId: statePayload.accountId,
129
+ workspaceId,
130
+ intent: "installation_authority_discovery",
131
+ ...continuedGitHubBrowserGrantClaims(statePayload),
132
+ });
133
+ setGitHubStateCookie(c, deps, discoveryState);
120
134
  return c.redirect(
121
- `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`,
135
+ githubOAuthAuthorizeUrl({
136
+ clientId,
137
+ state: discoveryState,
138
+ redirectUri: `${openGeniBaseUrl(settings, c)}/v1/github/oauth/callback`,
139
+ }),
122
140
  );
123
141
  });
124
142
 
@@ -174,7 +192,55 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
174
192
  return c.body(null, 204);
175
193
  });
176
194
 
195
+ app.get(
196
+ "/v1/workspaces/:workspaceId/github/installations/:installationId/configure",
197
+ async (c) => {
198
+ const workspaceId = c.req.param("workspaceId");
199
+ const installationId = parsePositiveInteger(c.req.param("installationId"));
200
+ const state = c.req.query("state");
201
+ if (installationId === null || !state) {
202
+ throw new HTTPException(400, { message: "invalid GitHub installation configuration" });
203
+ }
204
+ const statePayload = readSignedState(state, githubStateSecret);
205
+ if (
206
+ !statePayload ||
207
+ statePayload.intent !== "installation_authority" ||
208
+ statePayload.workspaceId !== workspaceId ||
209
+ typeof statePayload.accountId !== "string" ||
210
+ !isFreshGitHubBindingState(statePayload)
211
+ ) {
212
+ throw new HTTPException(400, {
213
+ message: "invalid or expired GitHub installation configuration state",
214
+ });
215
+ }
216
+ const grant = await requireGitHubManageGrant(c, deps, workspaceId, statePayload);
217
+ if (grant.accountId !== statePayload.accountId) {
218
+ throw new HTTPException(403, {
219
+ message: "GitHub installation state does not match this workspace",
220
+ });
221
+ }
222
+ const installation = (
223
+ await listWorkspaceGitHubInstallationBindings(deps, grant.workspaceId)
224
+ ).find((candidate) => candidate.installationId === installationId);
225
+ if (!installation) {
226
+ throw new HTTPException(404, { message: "GitHub installation binding not found" });
227
+ }
228
+ const configureState = createSignedState(githubStateSecret, {
229
+ accountId: grant.accountId,
230
+ workspaceId: grant.workspaceId,
231
+ expectedInstallationId: installationId,
232
+ intent: "installation_authority_install",
233
+ ...continuedGitHubBrowserGrantClaims(statePayload),
234
+ });
235
+ setGitHubStateCookie(c, deps, configureState);
236
+ const configureUrl = githubInstallationSettingsUrl(installation);
237
+ configureUrl.searchParams.set("state", configureState);
238
+ return c.redirect(configureUrl.toString());
239
+ },
240
+ );
241
+
177
242
  app.post("/v1/workspaces/:workspaceId/github/app-manifest", async (c) => {
243
+ assertOperatorGitHubAppSetup(settings);
178
244
  const workspaceId = c.req.param("workspaceId");
179
245
  const grant = await requireAccessGrant(c, deps, workspaceId, "github:manage");
180
246
  const payload = GitHubAppManifestCreate.parse(await c.req.json());
@@ -206,6 +272,7 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
206
272
  });
207
273
 
208
274
  app.get("/v1/github/app-manifest/callback", async (c) => {
275
+ assertOperatorGitHubAppSetup(settings);
209
276
  const code = c.req.query("code");
210
277
  const state = c.req.query("state");
211
278
  if (!code) {
@@ -226,14 +293,21 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
226
293
  });
227
294
 
228
295
  const handleGitHubInstallCallback = async (c: Context) => {
229
- const state = c.req.query("state");
296
+ const state =
297
+ c.req.query("state") ??
298
+ allCookieValues(c, githubStateCookie).find((candidate) => {
299
+ const payload = readSignedState(candidate, githubStateSecret);
300
+ return (
301
+ payload?.intent === "installation_authority_install" && isFreshGitHubBindingState(payload)
302
+ );
303
+ });
230
304
  if (!state) {
231
305
  throw new HTTPException(400, { message: "missing GitHub installation state" });
232
306
  }
233
307
  const statePayload = readSignedState(state, githubStateSecret);
234
308
  if (
235
309
  !statePayload ||
236
- statePayload.intent !== "installation_authority" ||
310
+ statePayload.intent !== "installation_authority_install" ||
237
311
  typeof statePayload.accountId !== "string" ||
238
312
  typeof statePayload.workspaceId !== "string" ||
239
313
  !isFreshGitHubBindingState(statePayload)
@@ -258,6 +332,14 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
258
332
  if (installationId === null) {
259
333
  throw new HTTPException(400, { message: "missing or invalid GitHub installation_id" });
260
334
  }
335
+ if (
336
+ statePayload.expectedInstallationId !== undefined &&
337
+ statePayload.expectedInstallationId !== installationId
338
+ ) {
339
+ throw new HTTPException(409, {
340
+ message: "GitHub returned a different installation than the one being configured",
341
+ });
342
+ }
261
343
  const clientId = settings.githubClientId?.trim();
262
344
  if (!clientId) {
263
345
  throw new HTTPException(409, {
@@ -299,13 +381,71 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
299
381
  const statePayload = readSignedState(state, githubStateSecret);
300
382
  if (
301
383
  !statePayload ||
302
- statePayload.intent !== "installation_authority_oauth" ||
303
384
  typeof statePayload.accountId !== "string" ||
304
385
  typeof statePayload.workspaceId !== "string" ||
305
386
  !isFreshGitHubBindingState(statePayload)
306
387
  ) {
307
388
  throw new HTTPException(400, { message: "invalid or expired GitHub OAuth state" });
308
389
  }
390
+ if (statePayload.intent === "installation_authority_discovery") {
391
+ requireGitHubStateCookie(c, state);
392
+ const grant = await requireGitHubManageGrant(c, deps, statePayload.workspaceId, statePayload);
393
+ if (grant.accountId !== statePayload.accountId) {
394
+ throw new HTTPException(403, {
395
+ message: "GitHub OAuth state does not match this workspace",
396
+ });
397
+ }
398
+ let candidates: GitHubInstallationBindingCandidate[] | null;
399
+ try {
400
+ candidates = deps.githubAppApi?.discoverInstallationBindingCandidates
401
+ ? await deps.githubAppApi.discoverInstallationBindingCandidates({ code })
402
+ : deps.githubAppApi
403
+ ? null
404
+ : await discoverGitHubInstallationBindingCandidates(settings, { code });
405
+ } catch (error) {
406
+ throw githubAuthorityHttpError(error);
407
+ }
408
+ if (!candidates) {
409
+ throw new HTTPException(409, {
410
+ message: "The configured GitHub provider cannot discover owner-authorized installations",
411
+ });
412
+ }
413
+ if (!isConsistentGitHubBindingCandidates(candidates)) {
414
+ throw new HTTPException(409, {
415
+ message: "GitHub installation discovery proof is stale or invalid",
416
+ });
417
+ }
418
+ const selectionState = createSignedState(githubStateSecret, {
419
+ accountId: grant.accountId,
420
+ workspaceId: grant.workspaceId,
421
+ intent: "installation_authority_selection",
422
+ allowedInstallationIds: candidates.map(({ installation }) => installation.installationId),
423
+ ...continuedGitHubBrowserGrantClaims(statePayload),
424
+ });
425
+ if (candidates.length === 0) {
426
+ return redirectToGitHubInstallation(c, deps, selectionState);
427
+ }
428
+ if (candidates.length === 1) {
429
+ return redirectToExactGitHubAuthorization(
430
+ c,
431
+ deps,
432
+ selectionState,
433
+ candidates[0]!.installation.installationId,
434
+ );
435
+ }
436
+ setGitHubStateCookie(c, deps, selectionState);
437
+ return c.html(
438
+ githubInstallationChooserHtml(
439
+ candidates,
440
+ selectionState,
441
+ grant.workspaceId,
442
+ openGeniBaseUrl(settings, c),
443
+ ),
444
+ );
445
+ }
446
+ if (statePayload.intent !== "installation_authority_oauth") {
447
+ throw new HTTPException(400, { message: "invalid or expired GitHub OAuth state" });
448
+ }
309
449
  const installationId = parsePositiveInteger(String(statePayload.installationId ?? ""));
310
450
  if (installationId === null) {
311
451
  throw new HTTPException(400, { message: "invalid GitHub installation id" });
@@ -382,6 +522,52 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
382
522
  );
383
523
  });
384
524
 
525
+ app.post("/v1/workspaces/:workspaceId/github/installations/select", async (c) => {
526
+ const workspaceId = c.req.param("workspaceId");
527
+ const form = new URLSearchParams(await c.req.text());
528
+ const state = form.get("state");
529
+ if (!state) {
530
+ throw new HTTPException(400, { message: "missing GitHub installation selection state" });
531
+ }
532
+ const statePayload = readSignedState(state, githubStateSecret);
533
+ if (
534
+ !statePayload ||
535
+ statePayload.intent !== "installation_authority_selection" ||
536
+ typeof statePayload.accountId !== "string" ||
537
+ statePayload.accountId.length === 0 ||
538
+ statePayload.workspaceId !== workspaceId ||
539
+ !isFreshGitHubBindingState(statePayload)
540
+ ) {
541
+ throw new HTTPException(400, {
542
+ message: "invalid or expired GitHub installation selection state",
543
+ });
544
+ }
545
+ requireGitHubStateCookie(c, state);
546
+ const grant = await requireGitHubManageGrant(c, deps, workspaceId, statePayload);
547
+ if (grant.accountId !== statePayload.accountId) {
548
+ throw new HTTPException(403, {
549
+ message: "GitHub installation state does not match this workspace",
550
+ });
551
+ }
552
+ const selected = form.get("installation_id");
553
+ if (selected === "new") {
554
+ return redirectToGitHubInstallation(c, deps, state);
555
+ }
556
+ const installationId = parsePositiveInteger(selected);
557
+ if (installationId === null) {
558
+ throw new HTTPException(400, { message: "invalid GitHub installation selection" });
559
+ }
560
+ if (
561
+ !Array.isArray(statePayload.allowedInstallationIds) ||
562
+ !statePayload.allowedInstallationIds.includes(installationId)
563
+ ) {
564
+ throw new HTTPException(403, {
565
+ message: "GitHub installation was not in the owner-authorized selection",
566
+ });
567
+ }
568
+ return redirectToExactGitHubAuthorization(c, deps, state, installationId);
569
+ });
570
+
385
571
  app.post("/v1/workspaces/:workspaceId/github/installations", async (c) => {
386
572
  const workspaceId = c.req.param("workspaceId");
387
573
  const form = new URLSearchParams(await c.req.text());
@@ -398,12 +584,89 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
398
584
  ) {
399
585
  throw new HTTPException(400, { message: "invalid or expired GitHub OAuth state" });
400
586
  }
401
- throw legacyInstallationChooserDisabled();
587
+ throw new HTTPException(410, { message: legacyInstallationChooserDisabledMessage });
402
588
  });
403
589
  }
404
590
 
405
- function legacyInstallationChooserDisabled(): HTTPException {
406
- return new HTTPException(410, { message: legacyInstallationChooserDisabledMessage });
591
+ function assertOperatorGitHubAppSetup(settings: ApiRouteDeps["settings"]): void {
592
+ if (settings.productAccessMode === "managed") {
593
+ throw new HTTPException(404, {
594
+ message: "GitHub App creation is unavailable in platform-managed deployments",
595
+ });
596
+ }
597
+ }
598
+
599
+ function redirectToGitHubInstallation(
600
+ c: Context,
601
+ deps: ApiRouteDeps,
602
+ sourceState: string,
603
+ ): Response {
604
+ const payload = readSignedState(sourceState, deps.githubStateSecret);
605
+ const slug = deps.settings.githubAppSlug?.trim();
606
+ if (
607
+ !payload ||
608
+ typeof payload.accountId !== "string" ||
609
+ typeof payload.workspaceId !== "string" ||
610
+ !slug
611
+ ) {
612
+ throw new HTTPException(409, { message: "GitHub App installation is unavailable" });
613
+ }
614
+ const installState = createSignedState(deps.githubStateSecret, {
615
+ accountId: payload.accountId,
616
+ workspaceId: payload.workspaceId,
617
+ intent: "installation_authority_install",
618
+ ...continuedGitHubBrowserGrantClaims(payload),
619
+ });
620
+ setGitHubStateCookie(c, deps, installState);
621
+ return c.redirect(
622
+ `https://github.com/apps/${encodeURIComponent(slug)}/installations/new?state=${encodeURIComponent(installState)}`,
623
+ );
624
+ }
625
+
626
+ function githubInstallationSettingsUrl(installation: {
627
+ installationId: number;
628
+ accountLogin: string | null;
629
+ accountType: string | null;
630
+ }): URL {
631
+ if (installation.accountType === "Organization" && installation.accountLogin) {
632
+ return new URL(
633
+ `https://github.com/organizations/${encodeURIComponent(installation.accountLogin)}/settings/installations/${installation.installationId}`,
634
+ );
635
+ }
636
+ return new URL(`https://github.com/settings/installations/${installation.installationId}`);
637
+ }
638
+
639
+ function redirectToExactGitHubAuthorization(
640
+ c: Context,
641
+ deps: ApiRouteDeps,
642
+ sourceState: string,
643
+ installationId: number,
644
+ ): Response {
645
+ const payload = readSignedState(sourceState, deps.githubStateSecret);
646
+ const clientId = deps.settings.githubClientId?.trim();
647
+ if (
648
+ !payload ||
649
+ typeof payload.accountId !== "string" ||
650
+ typeof payload.workspaceId !== "string" ||
651
+ !clientId
652
+ ) {
653
+ throw new HTTPException(409, { message: "GitHub user authorization is unavailable" });
654
+ }
655
+ const oauthState = createSignedState(deps.githubStateSecret, {
656
+ accountId: payload.accountId,
657
+ workspaceId: payload.workspaceId,
658
+ installationId,
659
+ intent: "installation_authority_oauth",
660
+ ...continuedGitHubBrowserGrantClaims(payload),
661
+ });
662
+ setGitHubStateCookie(c, deps, oauthState);
663
+ return c.redirect(
664
+ githubOAuthAuthorizeUrl({
665
+ clientId,
666
+ state: oauthState,
667
+ redirectUri: `${openGeniBaseUrl(deps.settings, c)}/v1/github/oauth/callback`,
668
+ }),
669
+ );
407
670
  }
408
671
 
409
672
  function setGitHubStateCookie(c: Context, deps: ApiRouteDeps, state: string): void {
@@ -502,10 +765,52 @@ function githubSetupSuccessHtml(account: string, returnUrl: string): string {
502
765
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GitHub App Connected</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(640px,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}.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}</style></head><body><main><h1>GitHub App connected</h1><p>${escapeHtml(account)} is now available to this OpenGeni workspace through an explicit repository allowlist.</p><a class="button" href="${escapeHtml(returnUrl)}">Back to OpenGeni</a></main></body></html>`;
503
766
  }
504
767
 
768
+ function githubInstallationChooserHtml(
769
+ candidates: GitHubInstallationBindingCandidate[],
770
+ state: string,
771
+ workspaceId: string,
772
+ baseUrl: string,
773
+ ): string {
774
+ const action = `${baseUrl}/v1/workspaces/${encodeURIComponent(workspaceId)}/github/installations/select`;
775
+ const options = candidates
776
+ .map(({ installation, authorityKind }) => {
777
+ const account = escapeHtml(
778
+ installation.accountLogin ?? `installation ${installation.installationId}`,
779
+ );
780
+ const label = authorityKind === "personal_owner" ? "Personal account" : "Organization owner";
781
+ return `<label class="option"><input type="radio" name="installation_id" value="${installation.installationId}" required><span><strong>${account}</strong><small>${label}</small></span></label>`;
782
+ })
783
+ .join("");
784
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Choose GitHub installation</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(640px,calc(100vw - 32px));border:1px solid #27272a;border-radius:12px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px}p{margin:0 0 18px;color:#d4d4d8}.options{display:grid;gap:8px;margin-bottom:18px}.option{display:flex;align-items:center;gap:12px;border:1px solid #3f3f46;border-radius:8px;padding:12px;cursor:pointer}.option span{display:grid;gap:2px}.option small{color:#a1a1aa}button{min-height:38px;border-radius:7px;border:1px solid #3f3f46;padding:0 14px;background:#f4f4f5;color:#09090b;font:600 14px system-ui,sans-serif;cursor:pointer}.secondary{margin-left:8px;background:transparent;color:#f4f4f5}</style></head><body><main><h1>Choose a GitHub account</h1><p>Only installations where GitHub proved you are the personal owner or an active organization owner are shown.</p><form method="post" action="${escapeHtml(action)}"><input type="hidden" name="state" value="${escapeHtml(state)}"><div class="options">${options}</div><button type="submit">Connect selected</button><button class="secondary" type="submit" name="installation_id" value="new" formnovalidate>Install on another account</button></form></main></body></html>`;
785
+ }
786
+
505
787
  function githubSetupPendingHtml(): string {
506
788
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GitHub App Requested</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(640px,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;color:#d4d4d8}</style></head><body><main><h1>GitHub App request sent</h1><p>A GitHub organization owner must approve the installation. OpenGeni has not created a workspace binding.</p></main></body></html>`;
507
789
  }
508
790
 
791
+ function isConsistentGitHubBindingCandidates(
792
+ candidates: GitHubInstallationBindingCandidate[],
793
+ ): boolean {
794
+ const ids = new Set<number>();
795
+ return candidates.every(({ installation, authorityKind }) => {
796
+ if (
797
+ !Number.isSafeInteger(installation.installationId) ||
798
+ installation.installationId <= 0 ||
799
+ !Number.isSafeInteger(installation.accountId) ||
800
+ installation.accountId <= 0 ||
801
+ !installation.accountLogin?.trim() ||
802
+ installation.suspended ||
803
+ ids.has(installation.installationId)
804
+ ) {
805
+ return false;
806
+ }
807
+ ids.add(installation.installationId);
808
+ return authorityKind === "personal_owner"
809
+ ? installation.accountType === "User"
810
+ : installation.accountType === "Organization";
811
+ });
812
+ }
813
+
509
814
  function parsePositiveInteger(value: string | undefined | null): number | null {
510
815
  if (!value || !/^\d+$/.test(value)) {
511
816
  return null;
@@ -102,6 +102,7 @@ import {
102
102
  SessionContextBusyError,
103
103
  HumanInputResponseValidationError,
104
104
  latestWorkspaceCapture,
105
+ sessionLatestWorkspaceCapture,
105
106
  workspaceCaptureAtRevision,
106
107
  type AppendEventInput,
107
108
  type SandboxOpenPtySessionRow,
@@ -167,12 +168,17 @@ import {
167
168
  } from "@opengeni/core";
168
169
  import { assertSessionExists, boundedLimit } from "../http/common";
169
170
  import { sseSessionStream } from "../http/sse";
170
- import { serveWorkspaceCapture, serveWorkspaceCaptureFile } from "./workspace-capture";
171
+ import {
172
+ serveWorkspaceCapture,
173
+ serveWorkspaceCaptureFile,
174
+ WorkspaceCaptureManifestCache,
175
+ } from "./workspace-capture";
171
176
 
172
177
  type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
173
178
 
174
179
  export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
175
180
  const { settings, db, bus, workflowClient, objectStorage } = deps;
181
+ const workspaceCaptureManifestCache = new WorkspaceCaptureManifestCache();
176
182
  const ptyIdentity = (pty: SandboxOpenPtySessionRow): SandboxPtyProcessIdentity => ({
177
183
  leaseId: pty.leaseId,
178
184
  sandboxGroupId: pty.sandboxGroupId,
@@ -1276,8 +1282,6 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1276
1282
  text: payload.text,
1277
1283
  turnInstructions: payload.turnInstructions ?? null,
1278
1284
  resources: payload.resources,
1279
- tools: payload.tools,
1280
- toolsProvided: userMessagePayloadHasOwnProperty({ payload: raw }, "tools"),
1281
1285
  model: payload.model ?? null,
1282
1286
  reasoningEffort: payload.reasoningEffort ?? null,
1283
1287
  mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
@@ -1320,8 +1324,6 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1320
1324
  text: event.payload.text,
1321
1325
  turnInstructions: event.payload.turnInstructions ?? null,
1322
1326
  resources: event.payload.resources ?? [],
1323
- tools: event.payload.tools ?? [],
1324
- toolsProvided: userMessagePayloadHasOwnProperty(rawEvent, "tools"),
1325
1327
  model: event.payload.model ?? null,
1326
1328
  reasoningEffort: event.payload.reasoningEffort ?? null,
1327
1329
  mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
@@ -2120,16 +2122,17 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2120
2122
  const workspaceId = c.req.param("workspaceId") ?? "";
2121
2123
  await requireAccessGrant(c, deps, workspaceId, "files:read");
2122
2124
  const sessionId = c.req.param("sessionId") ?? "";
2123
- const session = await getSession(db, workspaceId, sessionId);
2124
- if (!session) {
2125
+ const lookup = await sessionLatestWorkspaceCapture(db, workspaceId, sessionId);
2126
+ if (!lookup.sessionExists) {
2125
2127
  throw new HTTPException(404, { message: "session not found" });
2126
2128
  }
2127
2129
  if (!objectStorage) {
2128
2130
  // No storage configured → no captures can exist. Cold-fallback, not an error.
2129
2131
  return c.json({ available: false });
2130
2132
  }
2131
- const row = await latestWorkspaceCapture(db, workspaceId, sessionId);
2132
- return c.json(await serveWorkspaceCapture(row, objectStorage));
2133
+ return c.json(
2134
+ await serveWorkspaceCapture(lookup.capture, objectStorage, workspaceCaptureManifestCache),
2135
+ );
2133
2136
  });
2134
2137
 
2135
2138
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/workspace/capture/file", async (c) => {
@@ -2650,20 +2653,6 @@ function optionalEventSequence(raw: string | undefined): number | undefined {
2650
2653
  return Math.floor(sequence);
2651
2654
  }
2652
2655
 
2653
- function hasOwnProperty(value: unknown, key: string): boolean {
2654
- return Boolean(
2655
- value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key),
2656
- );
2657
- }
2658
-
2659
- function userMessagePayloadHasOwnProperty(value: unknown, key: string): boolean {
2660
- if (!value || typeof value !== "object") {
2661
- return false;
2662
- }
2663
- const payload = (value as { payload?: unknown }).payload;
2664
- return hasOwnProperty(payload, key);
2665
- }
2666
-
2667
2656
  /** Stable, value-free JSON errors for only the create-session boundary. */
2668
2657
  export function sessionCreateErrorResponse(c: Context, error: unknown): Response {
2669
2658
  if (error instanceof SessionSpawnDeniedError) {