@opengeni/api-router 0.5.7 → 0.9.0

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,5 +1,5 @@
1
1
  // apps/api/src/routes/enrollments.ts — the bring-your-own-compute enrollment
2
- // device-flow routes (M5; dossier §10.2 + §18). Mirrors the other sandbox route
2
+ // device-flow routes (M5). Mirrors the other sandbox route
3
3
  // modules (registerSessionRoutes / registerApiKeyRoutes): a thin route over a
4
4
  // focused service (../sandbox/enrollment.ts), requireAccessGrant BEFORE any Zod
5
5
  // parse on the USER-authenticated routes, explicit HTTPException(400) on a parse
@@ -69,7 +69,7 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
69
69
  }
70
70
 
71
71
  // A tiny in-process IP token-bucket for the UNAUTHENTICATED agent routes (start/
72
- // poll). The relay tier owns the heavy stream rate-limiting (dossier §10.5); this
72
+ // poll). The relay tier owns the heavy stream rate-limiting; this
73
73
  // is the application-tier abuse cap on the device-flow endpoints. Per-IP buckets
74
74
  // are pruned lazily. Not a distributed limiter (one replica per bucket) — that is
75
75
  // acceptable for a bounded, access-key-gated, short-TTL flow.
@@ -4,6 +4,13 @@ import {
4
4
  CreateFileUploadResponse,
5
5
  FileAsset,
6
6
  FileDownloadUrlResponse,
7
+ RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
8
+ RETAINED_OUTPUT_MAX_PAGE_BYTES,
9
+ RetainedArtifactMetadataSchema,
10
+ retainedArtifactReferenceFromFile,
11
+ resolveRetainedOutputRange,
12
+ type RetainedArtifactMetadata,
13
+ type RetainedOutputUnavailableReason,
7
14
  } from "@opengeni/contracts";
8
15
  import {
9
16
  claimFileUploadCleanup,
@@ -11,7 +18,9 @@ import {
11
18
  completeFileUpload,
12
19
  createFileUpload,
13
20
  getFileUpload,
21
+ getRetainedFileArtifact,
14
22
  requireFile,
23
+ type RetainedFileArtifact,
15
24
  } from "@opengeni/db";
16
25
  import type { Hono } from "hono";
17
26
  import { HTTPException } from "hono/http-exception";
@@ -235,6 +244,90 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
235
244
  return c.json(FileAsset.parse(file));
236
245
  });
237
246
 
247
+ app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId", async (c) => {
248
+ const workspaceId = c.req.param("workspaceId");
249
+ await requireAccessGrant(c, deps, workspaceId, "files:read");
250
+ const artifactId = retainedArtifactId(c.req.param("artifactId"));
251
+ const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
252
+ if (!artifact) {
253
+ return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
254
+ }
255
+ return c.json(retainedArtifactMetadata(artifact));
256
+ });
257
+
258
+ app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId/content", async (c) => {
259
+ const workspaceId = c.req.param("workspaceId");
260
+ await requireAccessGrant(c, deps, workspaceId, "files:read");
261
+ const artifactId = retainedArtifactId(c.req.param("artifactId"));
262
+ const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
263
+ if (!artifact) {
264
+ return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
265
+ }
266
+
267
+ const metadata = retainedArtifactMetadata(artifact);
268
+ if (!metadata.available) {
269
+ return c.json(metadata, retainedArtifactUnavailableStatus(metadata.reason));
270
+ }
271
+ if (!objectStorage) {
272
+ return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 503);
273
+ }
274
+
275
+ const rangeHeader = c.req.header("range");
276
+ const range = resolveRetainedOutputRange(
277
+ rangeHeader,
278
+ metadata.originalBytes,
279
+ rangeHeader ? RETAINED_OUTPUT_MAX_PAGE_BYTES : RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
280
+ );
281
+ if (range.kind === "invalid") {
282
+ return c.json(
283
+ {
284
+ message: "invalid retained artifact byte range",
285
+ reason: range.reason,
286
+ maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES,
287
+ },
288
+ 400,
289
+ );
290
+ }
291
+ if (range.kind === "unsatisfiable") {
292
+ return c.json(
293
+ { message: "retained artifact byte range is not satisfiable", reason: range.reason },
294
+ 416,
295
+ {
296
+ "Accept-Ranges": "bytes",
297
+ "Content-Range": range.contentRange,
298
+ "Cache-Control": "private, no-store",
299
+ },
300
+ );
301
+ }
302
+
303
+ const headers = {
304
+ "Accept-Ranges": range.acceptRanges,
305
+ "Cache-Control": "private, no-store",
306
+ "Content-Length": String(range.length),
307
+ "Content-Type": metadata.contentType,
308
+ "X-Content-Type-Options": "nosniff",
309
+ ...(range.contentRange ? { "Content-Range": range.contentRange } : {}),
310
+ };
311
+ if (range.kind === "empty") {
312
+ if (!(await objectStorage.fileExists(artifact.file))) {
313
+ return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
314
+ }
315
+ return c.body(null, 200, headers);
316
+ }
317
+
318
+ const bytes = await objectStorage.getFileRange(artifact.file, {
319
+ start: range.start,
320
+ end: range.end,
321
+ });
322
+ if (!bytes) {
323
+ return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
324
+ }
325
+ if (bytes.byteLength !== range.length) {
326
+ throw new HTTPException(502, { message: "object storage returned an invalid byte range" });
327
+ }
328
+ return c.body(new Uint8Array(bytes), range.status, headers);
329
+ });
330
+
238
331
  app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => {
239
332
  const workspaceId = c.req.param("workspaceId");
240
333
  await requireAccessGrant(c, deps, workspaceId, "files:read");
@@ -271,3 +364,63 @@ function sanitizeFilename(filename: string): string {
271
364
  function publicFileUploadStatus(status: string): string {
272
365
  return status === "cleanup_pending" ? "failed" : status;
273
366
  }
367
+
368
+ function retainedArtifactId(value: string): string {
369
+ const parsed = FileAsset.shape.id.safeParse(value);
370
+ if (!parsed.success) {
371
+ throw new HTTPException(404, { message: "artifact not found" });
372
+ }
373
+ return parsed.data;
374
+ }
375
+
376
+ function retainedArtifactUnavailable(
377
+ artifactId: string,
378
+ reason: RetainedOutputUnavailableReason,
379
+ ): RetainedArtifactMetadata {
380
+ return RetainedArtifactMetadataSchema.parse({ available: false, artifactId, reason });
381
+ }
382
+
383
+ function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtifactMetadata {
384
+ const reference = retainedArtifactReferenceFromFile(artifact.file);
385
+ if (reference) return reference;
386
+
387
+ const { file, uploadStatus, uploadExpiresAt } = artifact;
388
+ if (file.status === "deleted") {
389
+ return retainedArtifactUnavailable(file.id, "deleted");
390
+ }
391
+ if (
392
+ file.status === "expired" ||
393
+ uploadStatus === "expired" ||
394
+ (uploadStatus === "pending" &&
395
+ uploadExpiresAt !== null &&
396
+ uploadExpiresAt.getTime() < Date.now())
397
+ ) {
398
+ return retainedArtifactUnavailable(file.id, "expired");
399
+ }
400
+ if (file.status === "failed" || uploadStatus === "failed" || uploadStatus === "cleanup_pending") {
401
+ return retainedArtifactUnavailable(file.id, "failed");
402
+ }
403
+ if (file.status === "pending_upload" || uploadStatus === "pending") {
404
+ return retainedArtifactUnavailable(file.id, "pending");
405
+ }
406
+ return retainedArtifactUnavailable(file.id, "unsupported");
407
+ }
408
+
409
+ function retainedArtifactUnavailableStatus(
410
+ reason: RetainedOutputUnavailableReason,
411
+ ): 404 | 409 | 410 | 422 {
412
+ switch (reason) {
413
+ case "deleted":
414
+ return 404;
415
+ case "expired":
416
+ case "missing_storage":
417
+ return 410;
418
+ case "unsupported":
419
+ case "not_retained":
420
+ case "storage_write_failed":
421
+ return 422;
422
+ case "pending":
423
+ case "failed":
424
+ return 409;
425
+ }
426
+ }
@@ -1,5 +1,5 @@
1
1
  import { GitHubAppManifestCreate } from "@opengeni/contracts";
2
- import { listGitHubInstallationIdsForWorkspace, upsertGitHubInstallation } from "@opengeni/db";
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 { deleteCookie, getCookie, setCookie } from "hono/cookie";
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
- installUrl: slug
47
- ? `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`
48
- : null,
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
- // Browser entry point for install links issued outside a browser context
54
- // (the first-party MCP github_connect_link tool): it plants the CSRF state
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
- const slug = settings.githubAppSlug?.trim();
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, installUrl));
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
- requireGitHubStateCookie(c, state);
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
- requireGitHubStateCookie(c, state);
263
- return await completeGitHubInstallationBinding(deps, c, {
264
- code,
265
- statePayload,
266
- installationId,
267
- });
204
+ throw installationBindingDisabled();
268
205
  });
269
- }
270
206
 
271
- async function completeGitHubInstallationBinding(
272
- deps: ApiRouteDeps,
273
- c: Context,
274
- input: {
275
- code: string;
276
- statePayload: { accountId?: string; workspaceId?: string };
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
- if (error instanceof GitHubAppConfigurationError) {
323
- throw new HTTPException(409, {
324
- message: JSON.stringify({ message: error.message, missing: error.missing }),
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 new HTTPException(502, {
328
- message: error instanceof Error ? error.message : String(error),
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/github",
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
- export async function listWorkspaceGitHubRepositories(deps: ApiRouteDeps, workspaceId: string) {
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 install = installUrl
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
- }
@@ -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 (dossier §23.1). These are
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
@@ -1,5 +1,5 @@
1
1
  // apps/api/src/routes/machines.ts — the M10 Machines-dashboard + per-machine
2
- // metrics-series ROUTES (dossier §10.7). Mirrors registerEnrollmentRoutes: thin
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 dossier default). Bounded so a caller cannot request an unbounded
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,