@opengeni/api-router 2.6.4 → 2.8.1-canary.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.
Files changed (43) hide show
  1. package/dist/app.d.ts +2 -1
  2. package/dist/app.js +3 -1
  3. package/dist/{chunk-XTLI3CBH.js → chunk-HHKQMVY7.js} +3009 -1699
  4. package/dist/chunk-HHKQMVY7.js.map +1 -0
  5. package/dist/github-access.d.ts +25 -2
  6. package/dist/index.js +14 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/integrations/personal-github-repositories.d.ts +41 -2
  9. package/dist/mcp/scheduled-task-view.d.ts +3 -3
  10. package/dist/model-catalog.d.ts +5 -31
  11. package/dist/routes/codex.d.ts +8 -1
  12. package/dist/routes/github.d.ts +2 -0
  13. package/dist/routes/organization-model-providers.d.ts +3 -0
  14. package/dist/workspace-deletion.d.ts +6 -0
  15. package/package.json +18 -18
  16. package/src/app.ts +55 -9
  17. package/src/auth/organization-user-setup.ts +3 -3
  18. package/src/github-access.ts +117 -1
  19. package/src/http/sse.ts +15 -7
  20. package/src/index.ts +13 -1
  21. package/src/integrations/personal-github-repositories.ts +238 -9
  22. package/src/integrations/slack-app-home.ts +2 -2
  23. package/src/integrations/slack-interactions.ts +77 -37
  24. package/src/mcp/company-brain-governed-writes.ts +2 -2
  25. package/src/mcp/company-profile-agent-admin.ts +1 -1
  26. package/src/mcp/remember.ts +1 -1
  27. package/src/mcp/server.ts +11 -2
  28. package/src/model-catalog.ts +45 -337
  29. package/src/routes/automations.ts +178 -19
  30. package/src/routes/codex.ts +136 -32
  31. package/src/routes/connections.ts +271 -16
  32. package/src/routes/github.ts +116 -15
  33. package/src/routes/organization-memberships.ts +50 -3
  34. package/src/routes/organization-model-providers.ts +231 -0
  35. package/src/routes/packs.ts +1 -0
  36. package/src/routes/personal-github.ts +39 -0
  37. package/src/routes/pr-review.ts +72 -4
  38. package/src/routes/scheduled-tasks.ts +40 -13
  39. package/src/routes/sessions.ts +88 -52
  40. package/src/routes/supergrok.ts +3 -2
  41. package/src/routes/workspaces.ts +405 -62
  42. package/src/workspace-deletion.ts +64 -0
  43. package/dist/chunk-XTLI3CBH.js.map +0 -1
@@ -223,7 +223,7 @@ async function managedCookieHuman(
223
223
  };
224
224
  }
225
225
 
226
- async function requireOrganizationCodexHuman(
226
+ export async function requireOrganizationCodexHuman(
227
227
  c: Context,
228
228
  deps: ApiRouteDeps,
229
229
  organizationId: string,
@@ -239,7 +239,9 @@ async function requireOrganizationCodexHuman(
239
239
  };
240
240
  }
241
241
  if (!human) {
242
- throw new HTTPException(401, { message: "organization administrator session required" });
242
+ throw new HTTPException(401, {
243
+ message: "organization administrator session required",
244
+ });
243
245
  }
244
246
  try {
245
247
  await getOrganizationCodexRotationSettings(deps.db, {
@@ -249,7 +251,9 @@ async function requireOrganizationCodexHuman(
249
251
  } catch (error) {
250
252
  const state = nestedPostgresSqlState(error);
251
253
  if (state === "42501") {
252
- throw new HTTPException(403, { message: "organization administration is not authorized" });
254
+ throw new HTTPException(403, {
255
+ message: "organization administration is not authorized",
256
+ });
253
257
  }
254
258
  if (state === "P0002") {
255
259
  throw new HTTPException(404, { message: "organization not found" });
@@ -270,11 +274,13 @@ async function requireWorkspaceCodexManagementSource(
270
274
  });
271
275
  }
272
276
  if (source.effectiveSource === "disabled") {
273
- throw new HTTPException(409, { message: "Codex is disabled for this workspace" });
277
+ throw new HTTPException(409, {
278
+ message: "Codex is disabled for this workspace",
279
+ });
274
280
  }
275
281
  }
276
282
 
277
- function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
283
+ export function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
278
284
  const contentType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
279
285
  if (contentType !== "application/json") {
280
286
  throw new HTTPException(403, {
@@ -287,16 +293,28 @@ function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void
287
293
  });
288
294
  }
289
295
  const origin = c.req.header("origin");
296
+ const localOriginMatches =
297
+ deps.settings.productAccessMode === "local" && localBrowserOriginMatchesRequest(c, origin);
290
298
  if (
291
299
  deps.settings.productAccessMode === "local"
292
- ? !localBrowserOriginMatchesRequest(c, origin)
300
+ ? !localOriginMatches
293
301
  : origin !== new URL(deps.settings.publicBaseUrl!).origin
294
302
  ) {
295
303
  throw new HTTPException(403, {
296
304
  message: "same-origin browser request required",
297
305
  });
298
306
  }
299
- if (c.req.header("sec-fetch-site")?.toLowerCase() !== "same-origin") {
307
+ const fetchSite = c.req.header("sec-fetch-site")?.toLowerCase();
308
+ const localFetchSiteMatches =
309
+ localOriginMatches &&
310
+ (fetchSite === "same-origin" ||
311
+ fetchSite === "same-site" ||
312
+ (fetchSite === "cross-site" && localLoopbackOriginMatchesRequest(c, origin)));
313
+ if (
314
+ deps.settings.productAccessMode === "local"
315
+ ? !localFetchSiteMatches
316
+ : fetchSite !== "same-origin"
317
+ ) {
300
318
  throw new HTTPException(403, {
301
319
  message: "same-origin fetch metadata required",
302
320
  });
@@ -333,11 +351,35 @@ function localBrowserOriginMatchesRequest(c: Context, value: string | undefined)
333
351
  }
334
352
  return (
335
353
  origin.protocol === request.protocol &&
336
- origin.hostname === request.hostname &&
337
- (request.port === "" || origin.port === request.port)
354
+ (origin.hostname === request.hostname ||
355
+ (isLoopbackHostname(origin.hostname) && isLoopbackHostname(request.hostname)))
338
356
  );
339
357
  }
340
358
 
359
+ function localLoopbackOriginMatchesRequest(c: Context, value: string | undefined): boolean {
360
+ if (!value) return false;
361
+ try {
362
+ const origin = new URL(value);
363
+ const forwardedProtocol = c.req.header("x-forwarded-proto")?.trim().toLowerCase();
364
+ const protocol = forwardedProtocol ? `${forwardedProtocol}:` : new URL(c.req.url).protocol;
365
+ const forwardedHost = c.req.header("x-forwarded-host") ?? c.req.header("host");
366
+ if (!forwardedHost || /[\s,/?#@\\]/u.test(forwardedHost)) return false;
367
+ const request = new URL(`${protocol}//${forwardedHost}`);
368
+ return (
369
+ origin.origin === value &&
370
+ origin.protocol === request.protocol &&
371
+ isLoopbackHostname(origin.hostname) &&
372
+ isLoopbackHostname(request.hostname)
373
+ );
374
+ } catch {
375
+ return false;
376
+ }
377
+ }
378
+
379
+ function isLoopbackHostname(value: string): boolean {
380
+ return value === "localhost" || value === "[::1]" || /^127(?:\.[0-9]{1,3}){3}$/u.test(value);
381
+ }
382
+
341
383
  async function requireRedemptionHuman(
342
384
  c: Context,
343
385
  deps: ApiRouteDeps,
@@ -386,11 +428,15 @@ async function requireCodexAppsHuman(
386
428
  requireSameOriginBrowserMutation(c, deps);
387
429
  const human = await managedCookieHuman(c, deps);
388
430
  if (!human) {
389
- throw new HTTPException(401, { message: "managed browser session required" });
431
+ throw new HTTPException(401, {
432
+ message: "managed browser session required",
433
+ });
390
434
  }
391
435
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
392
436
  if (grant.subjectId !== human.subjectId) {
393
- throw new HTTPException(403, { message: "managed browser identity mismatch" });
437
+ throw new HTTPException(403, {
438
+ message: "managed browser identity mismatch",
439
+ });
394
440
  }
395
441
  return { human, accountId: grant.accountId };
396
442
  }
@@ -715,10 +761,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
715
761
  const workspaceId = c.req.param("workspaceId");
716
762
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
717
763
  const parsed = z
718
- .object({ mode: z.enum(["automatic", "workspace", "organization", "disabled"]) })
764
+ .object({
765
+ mode: z.enum(["automatic", "workspace", "organization", "disabled"]),
766
+ })
719
767
  .safeParse(await c.req.json().catch(() => null));
720
768
  if (!parsed.success) {
721
- throw new HTTPException(400, { message: "a valid Codex source mode is required" });
769
+ throw new HTTPException(400, {
770
+ message: "a valid Codex source mode is required",
771
+ });
722
772
  }
723
773
  try {
724
774
  return c.json(
@@ -795,7 +845,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
795
845
  const organizationId = c.req.param("organizationId");
796
846
  requireSameOriginBrowserMutation(c, deps);
797
847
  const human = await requireOrganizationCodexHuman(c, deps, organizationId);
798
- const { state } = (await c.req.json().catch(() => null)) as { state?: string };
848
+ const { state } = (await c.req.json().catch(() => null)) as {
849
+ state?: string;
850
+ };
799
851
  const payload = (state
800
852
  ? readSignedState(state, githubStateSecret)
801
853
  : null) as unknown as CodexConnectState | null;
@@ -806,7 +858,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
806
858
  !payload.deviceAuthId ||
807
859
  !payload.userCode
808
860
  ) {
809
- throw new HTTPException(400, { message: "codex connect state is invalid or expired" });
861
+ throw new HTTPException(400, {
862
+ message: "codex connect state is invalid or expired",
863
+ });
810
864
  }
811
865
  if (
812
866
  typeof payload.iat === "number" &&
@@ -927,7 +981,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
927
981
  const organizationId = c.req.param("organizationId");
928
982
  requireSameOriginBrowserMutation(c, deps);
929
983
  const human = await requireOrganizationCodexHuman(c, deps, organizationId);
930
- const body = (await c.req.json().catch(() => null)) as { label?: unknown } | null;
984
+ const body = (await c.req.json().catch(() => null)) as {
985
+ label?: unknown;
986
+ } | null;
931
987
  const renamed = await renameOrganizationCodexAccount(db, {
932
988
  organizationId,
933
989
  actorSubjectId: human.subjectId,
@@ -967,7 +1023,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
967
1023
  throw error;
968
1024
  }
969
1025
  await signalCodexCapacityTargets(deps, result.wakeTargets);
970
- return c.json({ disconnected: result.removed, newActiveId: result.newActiveCredentialId });
1026
+ return c.json({
1027
+ disconnected: result.removed,
1028
+ newActiveId: result.newActiveCredentialId,
1029
+ });
971
1030
  });
972
1031
 
973
1032
  // Begin device-code login: returns the user code + verification URL and a
@@ -1065,6 +1124,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1065
1124
  upserted: Awaited<ReturnType<typeof upsertCodexSubscriptionCredential>>;
1066
1125
  isActive: boolean;
1067
1126
  }>(db, { workspaceId, reason: "codex_credential_connected" }, async (tx) => {
1127
+ // The capacity mutation holds the source lock before entering this callback.
1128
+ const sourceBeforeConnect = await getWorkspaceCodexSubscriptionSource(tx, workspaceId);
1068
1129
  const upserted = await upsertCodexSubscriptionCredential(tx, {
1069
1130
  accountId: grant.accountId,
1070
1131
  workspaceId,
@@ -1092,12 +1153,12 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1092
1153
  }
1093
1154
  await ensureCodexRotationSettings(tx, grant.accountId, workspaceId);
1094
1155
  await setInitialActiveCodexCredential(tx, workspaceId, upserted.id);
1095
- const source = await getWorkspaceCodexSubscriptionSource(tx, workspaceId);
1096
1156
  await setWorkspaceCodexSubscriptionModeInTransaction(tx, {
1097
1157
  accountId: grant.accountId,
1098
1158
  workspaceId,
1099
1159
  subjectId: grant.subjectId,
1100
- mode: source.workspaceKind === "personal" ? "automatic" : "workspace",
1160
+ mode: sourceBeforeConnect.workspaceKind === "personal" ? "automatic" : "workspace",
1161
+ effectiveSourceBeforeMutation: sourceBeforeConnect.effectiveSource,
1101
1162
  });
1102
1163
  const rotation = await getCodexRotationSettings(tx, workspaceId);
1103
1164
  return {
@@ -1107,6 +1168,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1107
1168
  },
1108
1169
  changed: true,
1109
1170
  };
1171
+ }).catch((error: unknown) => {
1172
+ const cause = (error as { cause?: unknown } | null)?.cause;
1173
+ const message =
1174
+ cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
1175
+ if (message.includes("active turns are using it")) {
1176
+ throw new HTTPException(409, { message });
1177
+ }
1178
+ throw error;
1110
1179
  });
1111
1180
  const { upserted, isActive } = mutation.result;
1112
1181
  if (upserted.kind === "unresolved_redemption") {
@@ -1116,7 +1185,12 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1116
1185
  });
1117
1186
  }
1118
1187
  await signalCodexCapacityTargets(deps, mutation.wakeTargets);
1119
- return c.json({ status: "connected", plan: id.planType, accountId: upserted.id, isActive });
1188
+ return c.json({
1189
+ status: "connected",
1190
+ plan: id.planType,
1191
+ accountId: upserted.id,
1192
+ isActive,
1193
+ });
1120
1194
  });
1121
1195
 
1122
1196
  // Connection health: the cheapest real call is GET /codex/models (a 200 proves
@@ -1229,7 +1303,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1229
1303
  const workspaceId = c.req.param("workspaceId");
1230
1304
  await requireWorkspaceCodexManagementSource(deps, workspaceId);
1231
1305
  if (!settings.codexConnectedAppsEnabled) {
1232
- throw new HTTPException(409, { message: "Codex Apps is disabled for this deployment" });
1306
+ throw new HTTPException(409, {
1307
+ message: "Codex Apps is disabled for this deployment",
1308
+ });
1233
1309
  }
1234
1310
  const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
1235
1311
  const parsed = z
@@ -1239,7 +1315,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1239
1315
  })
1240
1316
  .safeParse(await c.req.json().catch(() => null));
1241
1317
  if (!parsed.success) {
1242
- throw new HTTPException(400, { message: "accountId and expectedVersion are required" });
1318
+ throw new HTTPException(400, {
1319
+ message: "accountId and expectedVersion are required",
1320
+ });
1243
1321
  }
1244
1322
  const result = await designateCodexAppsCredential(db, {
1245
1323
  accountId,
@@ -1257,10 +1335,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1257
1335
  });
1258
1336
  }
1259
1337
  if (result.kind === "forbidden") {
1260
- throw new HTTPException(403, { message: "missing permission: connections:write" });
1338
+ throw new HTTPException(403, {
1339
+ message: "missing permission: connections:write",
1340
+ });
1261
1341
  }
1262
1342
  if (result.kind === "unavailable") {
1263
- throw new HTTPException(409, { message: "codex account requires relogin" });
1343
+ throw new HTTPException(409, {
1344
+ message: "codex account requires relogin",
1345
+ });
1264
1346
  }
1265
1347
  const response = {
1266
1348
  credentialId: result.credentialId,
@@ -1288,7 +1370,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1288
1370
  expectedVersion: parsed.data.expectedVersion,
1289
1371
  });
1290
1372
  if (result.kind === "forbidden") {
1291
- throw new HTTPException(403, { message: "missing permission: connections:write" });
1373
+ throw new HTTPException(403, {
1374
+ message: "missing permission: connections:write",
1375
+ });
1292
1376
  }
1293
1377
  const response = {
1294
1378
  credentialId: result.credentialId,
@@ -1345,7 +1429,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1345
1429
  if (patch.rotationEnabled === undefined) {
1346
1430
  // Strategy-only writes are a deprecated no-op (no db touch): report the
1347
1431
  // (only) truth. Callers that also flip rotationEnabled fall through.
1348
- return c.json({ rotationStrategy: "sharded", rotationStrategyDeprecated: true });
1432
+ return c.json({
1433
+ rotationStrategy: "sharded",
1434
+ rotationStrategyDeprecated: true,
1435
+ });
1349
1436
  }
1350
1437
  await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
1351
1438
  const mutation = await withCodexCapacityMutation(
@@ -1454,7 +1541,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1454
1541
  });
1455
1542
  }
1456
1543
  await signalCodexCapacityTargets(deps, mutation.wakeTargets);
1457
- return c.json({ disconnected: result.removed, newActiveId: result.newActiveCredentialId });
1544
+ return c.json({
1545
+ disconnected: result.removed,
1546
+ newActiveId: result.newActiveCredentialId,
1547
+ });
1458
1548
  });
1459
1549
 
1460
1550
  // Legacy "disconnect all" (old workspace-wide behavior), deprecated in favor of
@@ -1752,10 +1842,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1752
1842
  });
1753
1843
  }
1754
1844
  if (adoption.kind === "not_found") {
1755
- throw new HTTPException(409, { message: "redemption recovery state changed" });
1845
+ throw new HTTPException(409, {
1846
+ message: "redemption recovery state changed",
1847
+ });
1756
1848
  }
1757
1849
  if (adoption.kind === "forbidden") {
1758
- throw new HTTPException(403, { message: "redemption owner is unavailable" });
1850
+ throw new HTTPException(403, {
1851
+ message: "redemption owner is unavailable",
1852
+ });
1759
1853
  }
1760
1854
  if (adoption.kind === "conflict") {
1761
1855
  throw new HTTPException(409, {
@@ -1769,7 +1863,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1769
1863
  // and a lost HTTP response must remain replayable after a later health
1770
1864
  // transition without another consume call.
1771
1865
  if (account.status !== "active" && existing?.status !== "completed") {
1772
- throw new HTTPException(403, { message: "redemption credential is unavailable" });
1866
+ throw new HTTPException(403, {
1867
+ message: "redemption credential is unavailable",
1868
+ });
1773
1869
  }
1774
1870
  const secret = settings.betterAuthSecret;
1775
1871
  if (!secret) {
@@ -1968,13 +2064,21 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1968
2064
  if (fenced.kind !== "ready") {
1969
2065
  if (fenced.reason === "confirmation_expired") {
1970
2066
  return c.json(
1971
- { status: "confirmation_expired", attemptId: attempt.id, retryable: true },
2067
+ {
2068
+ status: "confirmation_expired",
2069
+ attemptId: attempt.id,
2070
+ retryable: true,
2071
+ },
1972
2072
  403,
1973
2073
  );
1974
2074
  }
1975
2075
  if (fenced.reason === "credential_unavailable") {
1976
2076
  return c.json(
1977
- { status: "provider_unavailable", attemptId: attempt.id, retryable: true },
2077
+ {
2078
+ status: "provider_unavailable",
2079
+ attemptId: attempt.id,
2080
+ retryable: true,
2081
+ },
1978
2082
  503,
1979
2083
  );
1980
2084
  }