@opengeni/api-router 0.12.7 → 0.14.3
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.
- package/dist/app.d.ts +18 -38
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth.d.ts +35 -0
- package/dist/{chunk-SD5GJTZ4.js → chunk-36PW33ND.js} +3744 -638
- package/dist/chunk-36PW33ND.js.map +1 -0
- package/dist/codex-redemption-security.d.ts +15 -0
- package/dist/github-access.d.ts +12 -0
- package/dist/github-browser-flow.d.ts +14 -0
- package/dist/http/auth.d.ts +3 -0
- package/dist/http/common.d.ts +3 -0
- package/dist/http/sse.d.ts +70 -0
- package/dist/index.d.ts +11 -13
- package/dist/index.js +1 -1
- package/dist/integrations/google-drive.d.ts +76 -0
- package/dist/integrations/oauth-client.d.ts +42 -0
- package/dist/integrations/provider-domain.d.ts +7 -0
- package/dist/integrations/slack-bot.d.ts +343 -0
- package/dist/mcp/documents.d.ts +8 -0
- package/dist/mcp/files.d.ts +5 -0
- package/dist/mcp/server.d.ts +107 -0
- package/dist/mcp/session-view.d.ts +191 -0
- package/dist/mcp/toolspace.d.ts +61 -0
- package/dist/model-catalog.d.ts +39 -0
- package/dist/observability.d.ts +3 -0
- package/dist/routes/api-keys.d.ts +3 -0
- package/dist/routes/billing.d.ts +16 -0
- package/dist/routes/capabilities.d.ts +3 -0
- package/dist/routes/catalog-assets.d.ts +5 -0
- package/dist/routes/codex.d.ts +10 -0
- package/dist/routes/connections.d.ts +3 -0
- package/dist/routes/documents.d.ts +3 -0
- package/dist/routes/enrollments.d.ts +3 -0
- package/dist/routes/environments.d.ts +5 -0
- package/dist/routes/files.d.ts +4 -0
- package/dist/routes/github.d.ts +3 -0
- package/dist/routes/insights.d.ts +3 -0
- package/dist/routes/install.d.ts +5 -0
- package/dist/routes/machines.d.ts +3 -0
- package/dist/routes/packs.d.ts +3 -0
- package/dist/routes/preference-registry.d.ts +5 -0
- package/dist/routes/rigs.d.ts +3 -0
- package/dist/routes/scheduled-tasks.d.ts +3 -0
- package/dist/routes/sessions.d.ts +15 -0
- package/dist/routes/social.d.ts +3 -0
- package/dist/routes/transcriptions.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +59 -0
- package/dist/routes/workspace-instruction-policies.d.ts +3 -0
- package/dist/routes/workspace-state.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +5 -0
- package/dist/sandbox/access.d.ts +21 -0
- package/dist/sandbox/auth-callout.d.ts +30 -0
- package/dist/sandbox/channel-a.d.ts +37 -0
- package/dist/sandbox/enrollment.d.ts +120 -0
- package/dist/sandbox/machines.d.ts +29 -0
- package/dist/sandbox/metrics-ingestion.d.ts +128 -0
- package/dist/sandbox/rematerialize.d.ts +24 -0
- package/dist/sandbox/viewer.d.ts +251 -0
- package/dist/transcription/providers/azure-openai.d.ts +9 -0
- package/dist/transcription/providers/codex-subscription.d.ts +9 -0
- package/dist/transcription/providers/openai.d.ts +9 -0
- package/dist/transcription/service.d.ts +10 -0
- package/dist/workspace-state-projection.d.ts +16 -0
- package/package.json +13 -13
- package/src/app.ts +75 -5
- package/src/github-access.ts +1 -0
- package/src/integrations/google-drive.ts +869 -0
- package/src/integrations/oauth-client.ts +41 -6
- package/src/integrations/slack-bot.ts +756 -26
- package/src/mcp/server.ts +226 -42
- package/src/mcp/session-view.ts +1 -0
- package/src/mcp/toolspace.ts +17 -9
- package/src/routes/connections.ts +137 -7
- package/src/routes/documents.ts +13 -2
- package/src/routes/github.ts +327 -20
- package/src/routes/insights.ts +30 -0
- package/src/routes/preference-registry.ts +482 -0
- package/src/routes/sessions.ts +22 -23
- package/src/routes/transcriptions.ts +155 -0
- package/src/routes/workspace-capture.ts +108 -8
- package/src/routes/workspace-state.ts +61 -0
- package/src/sandbox/channel-a.ts +2 -0
- package/src/sandbox/rematerialize.ts +111 -4
- package/src/sandbox/viewer.ts +29 -12
- package/src/transcription/providers/azure-openai.ts +45 -0
- package/src/transcription/providers/codex-subscription.ts +100 -0
- package/src/transcription/providers/openai.ts +93 -0
- package/src/transcription/service.ts +121 -0
- package/src/workspace-state-projection.ts +244 -0
- package/dist/chunk-SD5GJTZ4.js.map +0 -1
package/src/routes/github.ts
CHANGED
|
@@ -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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
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
|
|
111
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 !== "
|
|
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,54 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
382
522
|
);
|
|
383
523
|
});
|
|
384
524
|
|
|
525
|
+
// This is browser navigation, like /github/connect and the OAuth callbacks.
|
|
526
|
+
// Keep it GET: a native HTML form cannot attach the API contract header that
|
|
527
|
+
// protects product mutations, and no durable binding is written here.
|
|
528
|
+
app.get("/v1/workspaces/:workspaceId/github/installations/select", async (c) => {
|
|
529
|
+
const workspaceId = c.req.param("workspaceId");
|
|
530
|
+
const state = c.req.query("state");
|
|
531
|
+
if (!state) {
|
|
532
|
+
throw new HTTPException(400, { message: "missing GitHub installation selection state" });
|
|
533
|
+
}
|
|
534
|
+
const statePayload = readSignedState(state, githubStateSecret);
|
|
535
|
+
if (
|
|
536
|
+
!statePayload ||
|
|
537
|
+
statePayload.intent !== "installation_authority_selection" ||
|
|
538
|
+
typeof statePayload.accountId !== "string" ||
|
|
539
|
+
statePayload.accountId.length === 0 ||
|
|
540
|
+
statePayload.workspaceId !== workspaceId ||
|
|
541
|
+
!isFreshGitHubBindingState(statePayload)
|
|
542
|
+
) {
|
|
543
|
+
throw new HTTPException(400, {
|
|
544
|
+
message: "invalid or expired GitHub installation selection state",
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
requireGitHubStateCookie(c, state);
|
|
548
|
+
const grant = await requireGitHubManageGrant(c, deps, workspaceId, statePayload);
|
|
549
|
+
if (grant.accountId !== statePayload.accountId) {
|
|
550
|
+
throw new HTTPException(403, {
|
|
551
|
+
message: "GitHub installation state does not match this workspace",
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
const selected = c.req.query("installation_id");
|
|
555
|
+
if (selected === "new") {
|
|
556
|
+
return redirectToGitHubInstallation(c, deps, state);
|
|
557
|
+
}
|
|
558
|
+
const installationId = parsePositiveInteger(selected);
|
|
559
|
+
if (installationId === null) {
|
|
560
|
+
throw new HTTPException(400, { message: "invalid GitHub installation selection" });
|
|
561
|
+
}
|
|
562
|
+
if (
|
|
563
|
+
!Array.isArray(statePayload.allowedInstallationIds) ||
|
|
564
|
+
!statePayload.allowedInstallationIds.includes(installationId)
|
|
565
|
+
) {
|
|
566
|
+
throw new HTTPException(403, {
|
|
567
|
+
message: "GitHub installation was not in the owner-authorized selection",
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return redirectToExactGitHubAuthorization(c, deps, state, installationId);
|
|
571
|
+
});
|
|
572
|
+
|
|
385
573
|
app.post("/v1/workspaces/:workspaceId/github/installations", async (c) => {
|
|
386
574
|
const workspaceId = c.req.param("workspaceId");
|
|
387
575
|
const form = new URLSearchParams(await c.req.text());
|
|
@@ -398,12 +586,89 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
398
586
|
) {
|
|
399
587
|
throw new HTTPException(400, { message: "invalid or expired GitHub OAuth state" });
|
|
400
588
|
}
|
|
401
|
-
throw
|
|
589
|
+
throw new HTTPException(410, { message: legacyInstallationChooserDisabledMessage });
|
|
402
590
|
});
|
|
403
591
|
}
|
|
404
592
|
|
|
405
|
-
function
|
|
406
|
-
|
|
593
|
+
function assertOperatorGitHubAppSetup(settings: ApiRouteDeps["settings"]): void {
|
|
594
|
+
if (settings.productAccessMode === "managed") {
|
|
595
|
+
throw new HTTPException(404, {
|
|
596
|
+
message: "GitHub App creation is unavailable in platform-managed deployments",
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function redirectToGitHubInstallation(
|
|
602
|
+
c: Context,
|
|
603
|
+
deps: ApiRouteDeps,
|
|
604
|
+
sourceState: string,
|
|
605
|
+
): Response {
|
|
606
|
+
const payload = readSignedState(sourceState, deps.githubStateSecret);
|
|
607
|
+
const slug = deps.settings.githubAppSlug?.trim();
|
|
608
|
+
if (
|
|
609
|
+
!payload ||
|
|
610
|
+
typeof payload.accountId !== "string" ||
|
|
611
|
+
typeof payload.workspaceId !== "string" ||
|
|
612
|
+
!slug
|
|
613
|
+
) {
|
|
614
|
+
throw new HTTPException(409, { message: "GitHub App installation is unavailable" });
|
|
615
|
+
}
|
|
616
|
+
const installState = createSignedState(deps.githubStateSecret, {
|
|
617
|
+
accountId: payload.accountId,
|
|
618
|
+
workspaceId: payload.workspaceId,
|
|
619
|
+
intent: "installation_authority_install",
|
|
620
|
+
...continuedGitHubBrowserGrantClaims(payload),
|
|
621
|
+
});
|
|
622
|
+
setGitHubStateCookie(c, deps, installState);
|
|
623
|
+
return c.redirect(
|
|
624
|
+
`https://github.com/apps/${encodeURIComponent(slug)}/installations/new?state=${encodeURIComponent(installState)}`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function githubInstallationSettingsUrl(installation: {
|
|
629
|
+
installationId: number;
|
|
630
|
+
accountLogin: string | null;
|
|
631
|
+
accountType: string | null;
|
|
632
|
+
}): URL {
|
|
633
|
+
if (installation.accountType === "Organization" && installation.accountLogin) {
|
|
634
|
+
return new URL(
|
|
635
|
+
`https://github.com/organizations/${encodeURIComponent(installation.accountLogin)}/settings/installations/${installation.installationId}`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return new URL(`https://github.com/settings/installations/${installation.installationId}`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function redirectToExactGitHubAuthorization(
|
|
642
|
+
c: Context,
|
|
643
|
+
deps: ApiRouteDeps,
|
|
644
|
+
sourceState: string,
|
|
645
|
+
installationId: number,
|
|
646
|
+
): Response {
|
|
647
|
+
const payload = readSignedState(sourceState, deps.githubStateSecret);
|
|
648
|
+
const clientId = deps.settings.githubClientId?.trim();
|
|
649
|
+
if (
|
|
650
|
+
!payload ||
|
|
651
|
+
typeof payload.accountId !== "string" ||
|
|
652
|
+
typeof payload.workspaceId !== "string" ||
|
|
653
|
+
!clientId
|
|
654
|
+
) {
|
|
655
|
+
throw new HTTPException(409, { message: "GitHub user authorization is unavailable" });
|
|
656
|
+
}
|
|
657
|
+
const oauthState = createSignedState(deps.githubStateSecret, {
|
|
658
|
+
accountId: payload.accountId,
|
|
659
|
+
workspaceId: payload.workspaceId,
|
|
660
|
+
installationId,
|
|
661
|
+
intent: "installation_authority_oauth",
|
|
662
|
+
...continuedGitHubBrowserGrantClaims(payload),
|
|
663
|
+
});
|
|
664
|
+
setGitHubStateCookie(c, deps, oauthState);
|
|
665
|
+
return c.redirect(
|
|
666
|
+
githubOAuthAuthorizeUrl({
|
|
667
|
+
clientId,
|
|
668
|
+
state: oauthState,
|
|
669
|
+
redirectUri: `${openGeniBaseUrl(deps.settings, c)}/v1/github/oauth/callback`,
|
|
670
|
+
}),
|
|
671
|
+
);
|
|
407
672
|
}
|
|
408
673
|
|
|
409
674
|
function setGitHubStateCookie(c: Context, deps: ApiRouteDeps, state: string): void {
|
|
@@ -502,10 +767,52 @@ function githubSetupSuccessHtml(account: string, returnUrl: string): string {
|
|
|
502
767
|
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
768
|
}
|
|
504
769
|
|
|
770
|
+
function githubInstallationChooserHtml(
|
|
771
|
+
candidates: GitHubInstallationBindingCandidate[],
|
|
772
|
+
state: string,
|
|
773
|
+
workspaceId: string,
|
|
774
|
+
baseUrl: string,
|
|
775
|
+
): string {
|
|
776
|
+
const action = `${baseUrl}/v1/workspaces/${encodeURIComponent(workspaceId)}/github/installations/select`;
|
|
777
|
+
const options = candidates
|
|
778
|
+
.map(({ installation, authorityKind }) => {
|
|
779
|
+
const account = escapeHtml(
|
|
780
|
+
installation.accountLogin ?? `installation ${installation.installationId}`,
|
|
781
|
+
);
|
|
782
|
+
const label = authorityKind === "personal_owner" ? "Personal account" : "Organization owner";
|
|
783
|
+
return `<label class="option"><input type="radio" name="installation_id" value="${installation.installationId}" required><span><strong>${account}</strong><small>${label}</small></span></label>`;
|
|
784
|
+
})
|
|
785
|
+
.join("");
|
|
786
|
+
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="get" 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>`;
|
|
787
|
+
}
|
|
788
|
+
|
|
505
789
|
function githubSetupPendingHtml(): string {
|
|
506
790
|
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
791
|
}
|
|
508
792
|
|
|
793
|
+
function isConsistentGitHubBindingCandidates(
|
|
794
|
+
candidates: GitHubInstallationBindingCandidate[],
|
|
795
|
+
): boolean {
|
|
796
|
+
const ids = new Set<number>();
|
|
797
|
+
return candidates.every(({ installation, authorityKind }) => {
|
|
798
|
+
if (
|
|
799
|
+
!Number.isSafeInteger(installation.installationId) ||
|
|
800
|
+
installation.installationId <= 0 ||
|
|
801
|
+
!Number.isSafeInteger(installation.accountId) ||
|
|
802
|
+
installation.accountId <= 0 ||
|
|
803
|
+
!installation.accountLogin?.trim() ||
|
|
804
|
+
installation.suspended ||
|
|
805
|
+
ids.has(installation.installationId)
|
|
806
|
+
) {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
ids.add(installation.installationId);
|
|
810
|
+
return authorityKind === "personal_owner"
|
|
811
|
+
? installation.accountType === "User"
|
|
812
|
+
: installation.accountType === "Organization";
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
509
816
|
function parsePositiveInteger(value: string | undefined | null): number | null {
|
|
510
817
|
if (!value || !/^\d+$/.test(value)) {
|
|
511
818
|
return null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { InsightsRange, WorkspaceInsightsResponse } from "@opengeni/contracts";
|
|
2
|
+
import { getWorkspaceInsights, requireAccessGrant, type ApiRouteDeps } from "@opengeni/core";
|
|
3
|
+
import type { Hono } from "hono";
|
|
4
|
+
import { HTTPException } from "hono/http-exception";
|
|
5
|
+
|
|
6
|
+
export function registerInsightsRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
7
|
+
app.get("/v1/workspaces/:workspaceId/insights", async (c) => {
|
|
8
|
+
const workspaceId = c.req.param("workspaceId");
|
|
9
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
10
|
+
|
|
11
|
+
const rangeRaw = c.req.query("range") ?? "week";
|
|
12
|
+
const rangeParsed = InsightsRange.safeParse(rangeRaw);
|
|
13
|
+
if (!rangeParsed.success) {
|
|
14
|
+
throw new HTTPException(400, {
|
|
15
|
+
message: "range must be one of today|week|month|ytd",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const provider = c.req.query("provider");
|
|
19
|
+
const model = c.req.query("model");
|
|
20
|
+
|
|
21
|
+
const response = await getWorkspaceInsights(deps.db, deps.settings, {
|
|
22
|
+
workspaceId,
|
|
23
|
+
range: rangeParsed.data,
|
|
24
|
+
provider: provider && provider !== "all" ? provider : null,
|
|
25
|
+
model: model && model !== "all" ? model : null,
|
|
26
|
+
});
|
|
27
|
+
c.header("cache-control", "private, no-store");
|
|
28
|
+
return c.json(WorkspaceInsightsResponse.parse(response));
|
|
29
|
+
});
|
|
30
|
+
}
|