@opengeni/api-router 0.5.6 → 0.7.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 +21 -3
- package/dist/app.js +3 -1
- package/dist/{chunk-HBEJMWD3.js → chunk-EYYTFA7N.js} +2396 -676
- package/dist/chunk-EYYTFA7N.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/app.ts +47 -4
- package/src/github-access.ts +46 -0
- package/src/github-browser-flow.ts +83 -0
- package/src/http/auth.ts +12 -4
- package/src/http/sse.ts +526 -92
- package/src/index.ts +2 -1
- package/src/mcp/server.ts +774 -146
- package/src/mcp/session-view.ts +622 -203
- package/src/mcp/toolspace.ts +110 -25
- package/src/routes/codex.ts +17 -14
- package/src/routes/enrollments.ts +2 -2
- package/src/routes/github.ts +63 -202
- package/src/routes/install.ts +1 -1
- package/src/routes/machines.ts +2 -2
- package/src/routes/sessions.ts +639 -76
- package/src/routes/workspace-capture.ts +56 -38
- package/src/routes/workspaces.ts +30 -11
- package/src/sandbox/access.ts +1 -1
- package/src/sandbox/auth-callout.ts +1 -1
- package/src/sandbox/channel-a.ts +14 -1
- package/src/sandbox/enrollment.ts +4 -4
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +1 -1
- package/src/sandbox/viewer.ts +1 -1
- package/dist/chunk-HBEJMWD3.js.map +0 -1
package/src/routes/github.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GitHubAppManifestCreate } from "@opengeni/contracts";
|
|
2
|
-
import {
|
|
2
|
+
import { deleteGitHubInstallationBinding } from "@opengeni/db";
|
|
3
3
|
import {
|
|
4
4
|
buildGitHubAppManifest,
|
|
5
5
|
convertGitHubAppManifest,
|
|
@@ -7,56 +7,52 @@ import {
|
|
|
7
7
|
envLinesFromGitHubManifestConversion,
|
|
8
8
|
GitHubAppApiError,
|
|
9
9
|
GitHubAppConfigurationError,
|
|
10
|
-
githubOAuthAuthorizeUrl,
|
|
11
10
|
githubAppMissingSettings,
|
|
12
|
-
listGitHubAppRepositories,
|
|
13
11
|
organizationAppManifestUrl,
|
|
14
12
|
personalAppManifestUrl,
|
|
15
13
|
readSignedState,
|
|
16
14
|
stateMaxAgeSeconds,
|
|
17
|
-
verifyGitHubInstallationAccessForUser,
|
|
18
15
|
verifySignedState,
|
|
19
16
|
} from "@opengeni/github";
|
|
20
17
|
import type { Context, Hono } from "hono";
|
|
21
|
-
import {
|
|
18
|
+
import { setCookie } from "hono/cookie";
|
|
22
19
|
import { HTTPException } from "hono/http-exception";
|
|
23
20
|
import { requireAccessGrant } from "@opengeni/core";
|
|
24
21
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
22
|
+
import {
|
|
23
|
+
listWorkspaceGitHubInstallationBindings,
|
|
24
|
+
listWorkspaceGitHubRepositories,
|
|
25
|
+
} from "../github-access";
|
|
25
26
|
|
|
26
27
|
const githubStateCookie = "opengeni_github_state";
|
|
28
|
+
const installationBindingDisabledMessage =
|
|
29
|
+
"Connecting a GitHub App installation is disabled until GitHub installation authority can be proven";
|
|
27
30
|
|
|
28
31
|
export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
29
|
-
const { settings, githubStateSecret } = deps;
|
|
32
|
+
const { db, settings, githubStateSecret } = deps;
|
|
30
33
|
|
|
31
34
|
app.get("/v1/workspaces/:workspaceId/github/app", async (c) => {
|
|
32
35
|
const workspaceId = c.req.param("workspaceId");
|
|
33
36
|
const grant = await requireAccessGrant(c, deps, workspaceId, "github:use");
|
|
34
37
|
const missing = githubAppMissingSettings(settings);
|
|
35
38
|
const slug = settings.githubAppSlug?.trim() || null;
|
|
36
|
-
const state = createSignedState(githubStateSecret, {
|
|
37
|
-
accountId: grant.accountId,
|
|
38
|
-
workspaceId: grant.workspaceId,
|
|
39
|
-
});
|
|
40
|
-
setGitHubStateCookie(c, deps, state);
|
|
41
39
|
return c.json({
|
|
42
40
|
configured: missing.length === 0,
|
|
43
41
|
appId: settings.githubAppId ?? null,
|
|
44
42
|
clientId: settings.githubClientId ?? null,
|
|
45
43
|
appSlug: slug,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
// Kept nullable for SDK compatibility. GitHub's setup callback contains
|
|
45
|
+
// a spoofable installation_id, while user-installation visibility and
|
|
46
|
+
// repository admin permission do not prove that this human may bind it.
|
|
47
|
+
installUrl: null,
|
|
48
|
+
linkUrl: null,
|
|
49
|
+
installations: await listWorkspaceGitHubInstallationBindings(deps, grant.workspaceId),
|
|
49
50
|
missing,
|
|
50
51
|
});
|
|
51
52
|
});
|
|
52
53
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// cookie the install/OAuth callbacks require and forwards to GitHub.
|
|
56
|
-
// Deliberately unauthenticated: the signed state is only ever minted for
|
|
57
|
-
// grants holding github:use, expires after stateMaxAgeSeconds, and is bound
|
|
58
|
-
// to this workspace; completing the installation binding still requires an
|
|
59
|
-
// authenticated github:manage grant in the same browser at the callback.
|
|
54
|
+
// Retain the entry route so already-issued links fail closed with an
|
|
55
|
+
// explicit terminal response instead of falling through to another intent.
|
|
60
56
|
app.get("/v1/workspaces/:workspaceId/github/connect", async (c) => {
|
|
61
57
|
const workspaceId = c.req.param("workspaceId");
|
|
62
58
|
const state = c.req.query("state");
|
|
@@ -67,19 +63,7 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
67
63
|
if (!statePayload || statePayload.workspaceId !== workspaceId) {
|
|
68
64
|
throw new HTTPException(400, { message: "invalid or expired GitHub installation state" });
|
|
69
65
|
}
|
|
70
|
-
|
|
71
|
-
if (!slug) {
|
|
72
|
-
throw new HTTPException(409, {
|
|
73
|
-
message: JSON.stringify({
|
|
74
|
-
message: "GitHub App is not configured",
|
|
75
|
-
missing: githubAppMissingSettings(settings),
|
|
76
|
-
}),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
setGitHubStateCookie(c, deps, state);
|
|
80
|
-
return c.redirect(
|
|
81
|
-
`https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`,
|
|
82
|
-
);
|
|
66
|
+
throw installationBindingDisabled();
|
|
83
67
|
});
|
|
84
68
|
|
|
85
69
|
app.get("/v1/workspaces/:workspaceId/github/repositories", async (c) => {
|
|
@@ -116,6 +100,24 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
116
100
|
}
|
|
117
101
|
});
|
|
118
102
|
|
|
103
|
+
app.delete("/v1/workspaces/:workspaceId/github/installations/:installationId", async (c) => {
|
|
104
|
+
const workspaceId = c.req.param("workspaceId");
|
|
105
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "github:manage");
|
|
106
|
+
const installationId = parsePositiveInteger(c.req.param("installationId"));
|
|
107
|
+
if (installationId === null) {
|
|
108
|
+
throw new HTTPException(400, { message: "invalid GitHub installation id" });
|
|
109
|
+
}
|
|
110
|
+
const deleted = await deleteGitHubInstallationBinding(db, {
|
|
111
|
+
accountId: grant.accountId,
|
|
112
|
+
workspaceId: grant.workspaceId,
|
|
113
|
+
installationId,
|
|
114
|
+
});
|
|
115
|
+
if (!deleted) {
|
|
116
|
+
throw new HTTPException(404, { message: "GitHub installation binding not found" });
|
|
117
|
+
}
|
|
118
|
+
return c.body(null, 204);
|
|
119
|
+
});
|
|
120
|
+
|
|
119
121
|
app.post("/v1/workspaces/:workspaceId/github/app-manifest", async (c) => {
|
|
120
122
|
const workspaceId = c.req.param("workspaceId");
|
|
121
123
|
const grant = await requireAccessGrant(c, deps, workspaceId, "github:manage");
|
|
@@ -159,12 +161,8 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
159
161
|
try {
|
|
160
162
|
const conversion = await convertGitHubAppManifest(code);
|
|
161
163
|
const envLines = envLinesFromGitHubManifestConversion(conversion);
|
|
162
|
-
const slug = String(conversion.slug ?? "");
|
|
163
|
-
const installUrl = slug
|
|
164
|
-
? `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`
|
|
165
|
-
: "";
|
|
166
164
|
setGitHubStateCookie(c, deps, state);
|
|
167
|
-
return c.html(githubSuccessHtml(envLines
|
|
165
|
+
return c.html(githubSuccessHtml(envLines));
|
|
168
166
|
} catch (error) {
|
|
169
167
|
const message = error instanceof GitHubAppApiError ? error.message : String(error);
|
|
170
168
|
throw new HTTPException(502, { message });
|
|
@@ -172,10 +170,7 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
172
170
|
});
|
|
173
171
|
|
|
174
172
|
const handleGitHubInstallCallback = async (c: Context) => {
|
|
175
|
-
const code = c.req.query("code");
|
|
176
173
|
const state = c.req.query("state");
|
|
177
|
-
const installationIdRaw = c.req.query("installation_id");
|
|
178
|
-
const setupAction = c.req.query("setup_action") ?? null;
|
|
179
174
|
if (!state) {
|
|
180
175
|
throw new HTTPException(400, { message: "missing GitHub installation state" });
|
|
181
176
|
}
|
|
@@ -187,147 +182,50 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
187
182
|
) {
|
|
188
183
|
throw new HTTPException(400, { message: "invalid or expired GitHub installation state" });
|
|
189
184
|
}
|
|
190
|
-
|
|
191
|
-
const grant = await requireAccessGrant(c, deps, statePayload.workspaceId, "github:manage");
|
|
192
|
-
if (grant.accountId !== statePayload.accountId) {
|
|
193
|
-
throw new HTTPException(403, {
|
|
194
|
-
message: "GitHub installation state does not match this workspace",
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
if (setupAction === "request" && !installationIdRaw) {
|
|
198
|
-
return c.html(githubSetupPendingHtml());
|
|
199
|
-
}
|
|
200
|
-
const installationId = parsePositiveInteger(installationIdRaw);
|
|
201
|
-
if (installationId === null) {
|
|
202
|
-
throw new HTTPException(400, { message: "missing or invalid GitHub installation_id" });
|
|
203
|
-
}
|
|
204
|
-
if (!code) {
|
|
205
|
-
const clientId = settings.githubClientId?.trim();
|
|
206
|
-
if (!clientId) {
|
|
207
|
-
throw new HTTPException(409, {
|
|
208
|
-
message: JSON.stringify({
|
|
209
|
-
message: "GitHub App is not configured",
|
|
210
|
-
missing: ["OPENGENI_GITHUB_CLIENT_ID"],
|
|
211
|
-
}),
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
const oauthState = createSignedState(githubStateSecret, {
|
|
215
|
-
accountId: grant.accountId,
|
|
216
|
-
workspaceId: grant.workspaceId,
|
|
217
|
-
installationId,
|
|
218
|
-
});
|
|
219
|
-
const baseUrl = (
|
|
220
|
-
settings.githubAppManifestBaseUrl ??
|
|
221
|
-
settings.publicBaseUrl ??
|
|
222
|
-
new URL(c.req.url).origin
|
|
223
|
-
).replace(/\/+$/, "");
|
|
224
|
-
setGitHubStateCookie(c, deps, oauthState);
|
|
225
|
-
return c.redirect(
|
|
226
|
-
githubOAuthAuthorizeUrl({
|
|
227
|
-
clientId,
|
|
228
|
-
state: oauthState,
|
|
229
|
-
redirectUri: `${baseUrl}/v1/github/oauth/callback`,
|
|
230
|
-
}),
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
return await completeGitHubInstallationBinding(deps, c, {
|
|
234
|
-
code,
|
|
235
|
-
statePayload,
|
|
236
|
-
installationId,
|
|
237
|
-
});
|
|
185
|
+
throw installationBindingDisabled();
|
|
238
186
|
};
|
|
239
187
|
|
|
240
188
|
app.get("/v1/github/setup", handleGitHubInstallCallback);
|
|
241
189
|
app.get("/v1/github/install/callback", handleGitHubInstallCallback);
|
|
242
190
|
|
|
243
191
|
app.get("/v1/github/oauth/callback", async (c) => {
|
|
244
|
-
const code = c.req.query("code");
|
|
245
192
|
const state = c.req.query("state");
|
|
246
|
-
if (!code) {
|
|
247
|
-
throw new HTTPException(400, { message: "missing GitHub OAuth code" });
|
|
248
|
-
}
|
|
249
193
|
if (!state) {
|
|
250
194
|
throw new HTTPException(400, { message: "missing GitHub OAuth state" });
|
|
251
195
|
}
|
|
252
196
|
const statePayload = readSignedState(state, githubStateSecret);
|
|
253
|
-
const installationId = parsePositiveInteger(String(statePayload?.installationId ?? ""));
|
|
254
197
|
if (
|
|
255
198
|
!statePayload ||
|
|
256
199
|
typeof statePayload.accountId !== "string" ||
|
|
257
|
-
typeof statePayload.workspaceId !== "string"
|
|
258
|
-
installationId === null
|
|
200
|
+
typeof statePayload.workspaceId !== "string"
|
|
259
201
|
) {
|
|
260
202
|
throw new HTTPException(400, { message: "invalid or expired GitHub OAuth state" });
|
|
261
203
|
}
|
|
262
|
-
|
|
263
|
-
return await completeGitHubInstallationBinding(deps, c, {
|
|
264
|
-
code,
|
|
265
|
-
statePayload,
|
|
266
|
-
installationId,
|
|
267
|
-
});
|
|
204
|
+
throw installationBindingDisabled();
|
|
268
205
|
});
|
|
269
|
-
}
|
|
270
206
|
|
|
271
|
-
async
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
installationId: number;
|
|
278
|
-
},
|
|
279
|
-
) {
|
|
280
|
-
const { db, settings } = deps;
|
|
281
|
-
if (!input.statePayload.workspaceId || !input.statePayload.accountId) {
|
|
282
|
-
throw new HTTPException(400, { message: "invalid or expired GitHub installation state" });
|
|
283
|
-
}
|
|
284
|
-
const grant = await requireAccessGrant(c, deps, input.statePayload.workspaceId, "github:manage");
|
|
285
|
-
if (grant.accountId !== input.statePayload.accountId) {
|
|
286
|
-
throw new HTTPException(403, {
|
|
287
|
-
message: "GitHub installation state does not match this workspace",
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
try {
|
|
291
|
-
const installation = await verifyGitHubInstallationAccessForUser(settings, {
|
|
292
|
-
code: input.code,
|
|
293
|
-
installationId: input.installationId,
|
|
294
|
-
});
|
|
295
|
-
if (!installation) {
|
|
296
|
-
throw new HTTPException(404, {
|
|
297
|
-
message: "GitHub App installation was not found for this app",
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
if (installation.suspended) {
|
|
301
|
-
throw new HTTPException(409, { message: "GitHub App installation is suspended" });
|
|
302
|
-
}
|
|
303
|
-
await upsertGitHubInstallation(db, {
|
|
304
|
-
accountId: grant.accountId,
|
|
305
|
-
workspaceId: grant.workspaceId,
|
|
306
|
-
installationId: input.installationId,
|
|
307
|
-
accountLogin: installation.accountLogin,
|
|
308
|
-
accountType: installation.accountType,
|
|
309
|
-
});
|
|
310
|
-
const returnUrl = openGeniReturnUrl(settings, c, input.statePayload.workspaceId);
|
|
311
|
-
deleteCookie(c, githubStateCookie, { path: "/v1/github" });
|
|
312
|
-
return c.html(
|
|
313
|
-
githubSetupSuccessHtml(
|
|
314
|
-
installation.accountLogin ?? `installation ${input.installationId}`,
|
|
315
|
-
returnUrl,
|
|
316
|
-
),
|
|
317
|
-
);
|
|
318
|
-
} catch (error) {
|
|
319
|
-
if (error instanceof HTTPException) {
|
|
320
|
-
throw error;
|
|
207
|
+
app.post("/v1/workspaces/:workspaceId/github/installations", async (c) => {
|
|
208
|
+
const workspaceId = c.req.param("workspaceId");
|
|
209
|
+
const form = new URLSearchParams(await c.req.text());
|
|
210
|
+
const state = form.get("oauth_state");
|
|
211
|
+
if (!state) {
|
|
212
|
+
throw new HTTPException(400, { message: "missing GitHub OAuth state" });
|
|
321
213
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
214
|
+
const statePayload = readSignedState(state, githubStateSecret);
|
|
215
|
+
if (
|
|
216
|
+
!statePayload ||
|
|
217
|
+
typeof statePayload.accountId !== "string" ||
|
|
218
|
+
statePayload.accountId.length === 0 ||
|
|
219
|
+
statePayload.workspaceId !== workspaceId
|
|
220
|
+
) {
|
|
221
|
+
throw new HTTPException(400, { message: "invalid or expired GitHub OAuth state" });
|
|
326
222
|
}
|
|
327
|
-
throw
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
223
|
+
throw installationBindingDisabled();
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function installationBindingDisabled(): HTTPException {
|
|
228
|
+
return new HTTPException(410, { message: installationBindingDisabledMessage });
|
|
331
229
|
}
|
|
332
230
|
|
|
333
231
|
function setGitHubStateCookie(c: Context, deps: ApiRouteDeps, state: string): void {
|
|
@@ -335,19 +233,11 @@ function setGitHubStateCookie(c: Context, deps: ApiRouteDeps, state: string): vo
|
|
|
335
233
|
httpOnly: true,
|
|
336
234
|
sameSite: "Lax",
|
|
337
235
|
secure: isSecureRequest(c, deps),
|
|
338
|
-
path: "/v1
|
|
236
|
+
path: "/v1",
|
|
339
237
|
maxAge: stateMaxAgeSeconds,
|
|
340
238
|
});
|
|
341
239
|
}
|
|
342
240
|
|
|
343
|
-
function requireGitHubStateCookie(c: Context, state: string): void {
|
|
344
|
-
if (getCookie(c, githubStateCookie) !== state) {
|
|
345
|
-
throw new HTTPException(400, {
|
|
346
|
-
message: "invalid or expired GitHub installation browser state",
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
241
|
function isSecureRequest(c: Context, deps: ApiRouteDeps): boolean {
|
|
352
242
|
return (
|
|
353
243
|
deps.settings.publicBaseUrl?.startsWith("https://") ||
|
|
@@ -356,26 +246,10 @@ function isSecureRequest(c: Context, deps: ApiRouteDeps): boolean {
|
|
|
356
246
|
);
|
|
357
247
|
}
|
|
358
248
|
|
|
359
|
-
|
|
360
|
-
const installationIds = await listGitHubInstallationIdsForWorkspace(deps.db, workspaceId);
|
|
361
|
-
return await listGitHubAppRepositories(deps.settings, { installationIds });
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function githubSuccessHtml(envLines: string[], installUrl: string): string {
|
|
249
|
+
function githubSuccessHtml(envLines: string[]): string {
|
|
365
250
|
const envText = envLines.join("\n");
|
|
366
251
|
const escaped = escapeHtml(envText);
|
|
367
|
-
const
|
|
368
|
-
? `<a class="button secondary" href="${escapeHtml(installUrl)}">Install on repositories</a>`
|
|
369
|
-
: "";
|
|
370
|
-
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GitHub App Created</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(760px,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}.env-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:22px 0 8px}.env-header h2{margin:0;font-size:13px;line-height:1.2;text-transform:uppercase;letter-spacing:.08em;color:#a1a1aa}pre{white-space:pre-wrap;word-break:break-word;max-height:380px;overflow:auto;background:#09090b;border:1px solid #27272a;border-radius:8px;padding:16px;font-size:13px;line-height:1.5}.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:18px}.button,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;cursor:pointer}.button.secondary{background:transparent;color:#fafafa}.button.secondary:hover,button.secondary:hover{background:#27272a}button:disabled{cursor:not-allowed;opacity:.7}</style></head><body><main><h1>GitHub App created</h1><p>Add these values to .env, then restart API and worker.</p><div class="env-header"><h2>Environment variables</h2><button id="copy-env" type="button">Copy env</button></div><pre id="env-lines">${escaped}</pre><div class="actions">${install}</div><script>(()=>{const button=document.getElementById("copy-env");const env=document.getElementById("env-lines");async function copyText(text){if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(text);return;}const area=document.createElement("textarea");area.value=text;area.setAttribute("readonly","");area.style.position="fixed";area.style.inset="-9999px";document.body.append(area);area.select();document.execCommand("copy");area.remove();}button?.addEventListener("click",async()=>{try{await copyText(env?.textContent||"");button.textContent="Copied";setTimeout(()=>button.textContent="Copy env",1600);}catch{button.textContent="Copy failed";setTimeout(()=>button.textContent="Copy env",2200);}});})();</script></main></body></html>`;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function githubSetupSuccessHtml(account: string, returnUrl: string): string {
|
|
374
|
-
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}.button:hover{background:#e4e4e7}</style></head><body><main><h1>GitHub App connected</h1><p>${escapeHtml(account)} is now available to this OpenGeni workspace.</p><a class="button" href="${escapeHtml(returnUrl)}">Back to OpenGeni</a></main></body></html>`;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function githubSetupPendingHtml(): string {
|
|
378
|
-
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>An organization administrator must approve the installation before OpenGeni can connect it to this workspace.</p></main></body></html>`;
|
|
252
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GitHub App Created</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(760px,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}.env-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:22px 0 8px}.env-header h2{margin:0;font-size:13px;line-height:1.2;text-transform:uppercase;letter-spacing:.08em;color:#a1a1aa}pre{white-space:pre-wrap;word-break:break-word;max-height:380px;overflow:auto;background:#09090b;border:1px solid #27272a;border-radius:8px;padding:16px;font-size:13px;line-height:1.5}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;cursor:pointer}button:disabled{cursor:not-allowed;opacity:.7}</style></head><body><main><h1>GitHub App created</h1><p>Add these values to .env, then restart API and worker.</p><div class="env-header"><h2>Environment variables</h2><button id="copy-env" type="button">Copy env</button></div><pre id="env-lines">${escaped}</pre><script>(()=>{const button=document.getElementById("copy-env");const env=document.getElementById("env-lines");async function copyText(text){if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(text);return;}const area=document.createElement("textarea");area.value=text;area.setAttribute("readonly","");area.style.position="fixed";area.style.inset="-9999px";document.body.append(area);area.select();document.execCommand("copy");area.remove();}button?.addEventListener("click",async()=>{try{await copyText(env?.textContent||"");button.textContent="Copied";setTimeout(()=>button.textContent="Copy env",1600);}catch{button.textContent="Copy failed";setTimeout(()=>button.textContent="Copy env",2200);}});})();</script></main></body></html>`;
|
|
379
253
|
}
|
|
380
254
|
|
|
381
255
|
function parsePositiveInteger(value: string | undefined | null): number | null {
|
|
@@ -399,16 +273,3 @@ function escapeHtml(value: string): string {
|
|
|
399
273
|
})[char] ?? char,
|
|
400
274
|
);
|
|
401
275
|
}
|
|
402
|
-
|
|
403
|
-
function openGeniReturnUrl(
|
|
404
|
-
settings: ApiRouteDeps["settings"],
|
|
405
|
-
c: Context,
|
|
406
|
-
workspaceId: string | undefined,
|
|
407
|
-
): string {
|
|
408
|
-
const base = (settings.publicBaseUrl ?? new URL(c.req.url).origin).replace(/\/+$/, "");
|
|
409
|
-
const url = new URL(base || new URL(c.req.url).origin);
|
|
410
|
-
if (workspaceId) {
|
|
411
|
-
url.searchParams.set("workspaceId", workspaceId);
|
|
412
|
-
}
|
|
413
|
-
return url.toString();
|
|
414
|
-
}
|
package/src/routes/install.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Hono } from "hono";
|
|
|
3
3
|
import { HTTPException } from "hono/http-exception";
|
|
4
4
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
5
5
|
|
|
6
|
-
// The get.<domain> install-serving routes
|
|
6
|
+
// The get.<domain> install-serving routes. These are
|
|
7
7
|
// UNAUTHENTICATED (see http/auth.ts isAuthExempt — the `installExemptPaths` set)
|
|
8
8
|
// so a fresh machine with no credentials can `curl -fsSL https://get.<domain>/install.sh`,
|
|
9
9
|
// read it first, then pipe to sh. They serve the IN-REPO committed script bodies
|
package/src/routes/machines.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// apps/api/src/routes/machines.ts — the M10 Machines-dashboard + per-machine
|
|
2
|
-
// metrics-series ROUTES
|
|
2
|
+
// metrics-series ROUTES. Mirrors registerEnrollmentRoutes: thin
|
|
3
3
|
// routes over a focused service (../sandbox/machines.ts), requireAccessGrant
|
|
4
4
|
// BEFORE any work, the whole router gated behind sandboxSelfhostedEnabled
|
|
5
5
|
// (default OFF → 404, invisible). Both routes need perm enrollments:read.
|
|
@@ -28,7 +28,7 @@ import { buildFleetContextForSession, swapActiveSandbox } from "@opengeni/core";
|
|
|
28
28
|
import { listMachines, metricRowToSample } from "../sandbox/machines";
|
|
29
29
|
|
|
30
30
|
// The supported series windows → milliseconds. An unknown/absent window defaults
|
|
31
|
-
// to 1h (the
|
|
31
|
+
// to 1h (the default). Bounded so a caller cannot request an unbounded
|
|
32
32
|
// scan; longer ranges are a later concern (retention is ~N days).
|
|
33
33
|
const SERIES_WINDOWS_MS: Record<string, number> = {
|
|
34
34
|
"15m": 15 * 60_000,
|