@opengeni/api-router 0.5.3 → 0.5.5

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 (49) hide show
  1. package/dist/app.d.ts +9 -1
  2. package/dist/app.js +7 -1
  3. package/dist/{chunk-3HIA43CC.js → chunk-HBEJMWD3.js} +5470 -2223
  4. package/dist/chunk-HBEJMWD3.js.map +1 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +279 -55
  7. package/dist/index.js.map +1 -1
  8. package/package.json +20 -20
  9. package/src/app.ts +583 -166
  10. package/src/auth/managed-auth.ts +32 -16
  11. package/src/http/auth.ts +8 -1
  12. package/src/http/common.ts +6 -2
  13. package/src/http/sse.ts +84 -8
  14. package/src/index.ts +178 -75
  15. package/src/integrations/oauth-client.ts +403 -120
  16. package/src/integrations/provider-domain.ts +4 -1
  17. package/src/mcp/documents.ts +173 -94
  18. package/src/mcp/server.ts +1600 -693
  19. package/src/mcp/session-view.ts +8 -2
  20. package/src/mcp/toolspace.ts +175 -84
  21. package/src/observability.ts +7 -1
  22. package/src/routes/api-keys.ts +39 -23
  23. package/src/routes/billing.ts +180 -65
  24. package/src/routes/capabilities.ts +17 -8
  25. package/src/routes/catalog-assets.ts +5 -2
  26. package/src/routes/codex.ts +244 -63
  27. package/src/routes/connections.ts +71 -33
  28. package/src/routes/documents.ts +242 -92
  29. package/src/routes/enrollments.ts +100 -70
  30. package/src/routes/environments.ts +205 -136
  31. package/src/routes/files.ts +164 -39
  32. package/src/routes/github.ts +123 -50
  33. package/src/routes/install.ts +9 -2
  34. package/src/routes/machines.ts +9 -8
  35. package/src/routes/packs.ts +141 -89
  36. package/src/routes/rigs.ts +189 -0
  37. package/src/routes/scheduled-tasks.ts +51 -9
  38. package/src/routes/sessions.ts +870 -328
  39. package/src/routes/social.ts +50 -38
  40. package/src/routes/workspace-capture.ts +238 -0
  41. package/src/routes/workspaces.ts +146 -13
  42. package/src/sandbox/access.ts +11 -3
  43. package/src/sandbox/auth-callout.ts +5 -1
  44. package/src/sandbox/channel-a.ts +104 -27
  45. package/src/sandbox/enrollment.ts +13 -3
  46. package/src/sandbox/machines.ts +68 -59
  47. package/src/sandbox/metrics-ingestion.ts +238 -17
  48. package/src/sandbox/viewer.ts +172 -46
  49. package/dist/chunk-3HIA43CC.js.map +0 -1
@@ -33,14 +33,18 @@ import {
33
33
  fetchCodexUsageForAccount,
34
34
  getCodexCredentialStatus,
35
35
  getCodexRotationSettings,
36
+ listPendingCodexCapacityWakeTargets,
36
37
  listCodexAccountStatuses,
37
38
  loadCodexCredentialForRun,
38
39
  renameCodexAccount,
39
40
  setActiveCodexCredential,
41
+ setInitialActiveCodexCredential,
40
42
  updateCodexRotationSettings,
41
43
  upsertCodexSubscriptionCredential,
44
+ withCodexCapacityMutation,
42
45
  CODEX_ROTATION_STRATEGIES,
43
46
  type CodexAccountStatus,
47
+ type CodexCapacityWakeTarget,
44
48
  type CodexRotationStrategy,
45
49
  } from "@opengeni/db";
46
50
 
@@ -63,8 +67,16 @@ function codexAccountJson(row: CodexAccountStatus) {
63
67
  expiresAt: row.expiresAt,
64
68
  lastRefreshAt: row.lastRefreshAt,
65
69
  lastError: row.lastError,
66
- fiveHour: buildCodexUsageWindowFromCache(row.primaryUsedPercent, row.primaryResetAt, CODEX_FIVE_HOUR_WINDOW_SECONDS),
67
- weekly: buildCodexUsageWindowFromCache(row.secondaryUsedPercent, row.secondaryResetAt, CODEX_WEEKLY_WINDOW_SECONDS),
70
+ fiveHour: buildCodexUsageWindowFromCache(
71
+ row.primaryUsedPercent,
72
+ row.primaryResetAt,
73
+ CODEX_FIVE_HOUR_WINDOW_SECONDS,
74
+ ),
75
+ weekly: buildCodexUsageWindowFromCache(
76
+ row.secondaryUsedPercent,
77
+ row.secondaryResetAt,
78
+ CODEX_WEEKLY_WINDOW_SECONDS,
79
+ ),
68
80
  usageCheckedAt: row.usageCheckedAt,
69
81
  // P3 rotation cooldown: when set and in the future, this account is cooling-down.
70
82
  exhaustedUntil: row.exhaustedUntil,
@@ -74,20 +86,28 @@ function codexAccountJson(row: CodexAccountStatus) {
74
86
  // The /codex/usage{,/refresh,/:id} wire wrapper: the rich normalized payload
75
87
  // carries its own `status`, surfaced at the top level for back-compat with the
76
88
  // existing CodexUsage = { status; usage } shape.
77
- function codexUsageJson(payload: CodexUsagePayload): { status: CodexUsagePayload["status"]; usage: CodexUsagePayload } {
89
+ function codexUsageJson(payload: CodexUsagePayload): {
90
+ status: CodexUsagePayload["status"];
91
+ usage: CodexUsagePayload;
92
+ } {
78
93
  return { status: payload.status, usage: payload };
79
94
  }
80
95
 
81
- function codexModelsForPicker(slugs: string[]): Array<{ id: string; label: string; provider: string; providerLabel: string; api: "responses" }> {
82
- return slugs
83
- .filter((slug) => (/^gpt-5/.test(slug) || slug.includes("codex")) && slug !== "codex-auto-review")
84
- .map((slug) => ({
85
- id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
86
- label: slug.replace(/^gpt-/, "GPT-"),
87
- provider: CODEX_PROVIDER_ID,
88
- providerLabel: CODEX_PROVIDER_LABEL,
89
- api: "responses" as const,
90
- }));
96
+ export function codexModelsForPicker(
97
+ liveSlugs: readonly string[],
98
+ ): Array<{ id: string; label: string; provider: string; providerLabel: string; api: "responses" }> {
99
+ const available = new Set(liveSlugs);
100
+ const missing = CODEX_FALLBACK_MODEL_SLUGS.filter((slug) => !available.has(slug));
101
+ if (missing.length > 0) {
102
+ throw new Error(`Codex catalog is missing required models: ${missing.join(", ")}`);
103
+ }
104
+ return CODEX_FALLBACK_MODEL_SLUGS.map((slug) => ({
105
+ id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
106
+ label: slug.replace(/^gpt-/, "GPT-"),
107
+ provider: CODEX_PROVIDER_ID,
108
+ providerLabel: CODEX_PROVIDER_LABEL,
109
+ api: "responses" as const,
110
+ }));
91
111
  }
92
112
  import { createSignedState, readSignedState } from "@opengeni/github";
93
113
  import type { ApiRouteDeps } from "@opengeni/core";
@@ -95,7 +115,46 @@ import type { Hono } from "hono";
95
115
  import { HTTPException } from "hono/http-exception";
96
116
  import { requireAccessGrant } from "@opengeni/core";
97
117
 
98
- type CodexConnectState = { workspaceId?: string; deviceAuthId?: string; userCode?: string; iat?: number };
118
+ type CodexConnectState = {
119
+ workspaceId?: string;
120
+ deviceAuthId?: string;
121
+ userCode?: string;
122
+ iat?: number;
123
+ };
124
+
125
+ async function signalCodexCapacityTargets(
126
+ deps: ApiRouteDeps,
127
+ targets: CodexCapacityWakeTarget[],
128
+ ): Promise<void> {
129
+ await Promise.allSettled(
130
+ targets.map((target) =>
131
+ deps.workflowClient.signalCodexCapacity
132
+ ? deps.workflowClient.signalCodexCapacity({
133
+ accountId: target.accountId,
134
+ workspaceId: target.workspaceId,
135
+ sessionId: target.sessionId,
136
+ workflowId: target.workflowId,
137
+ wakeRevision: target.wakeRevision,
138
+ workflowWakeRevision: target.workflowWakeRevision,
139
+ })
140
+ : deps.workflowClient.wakeSessionWorkflow({
141
+ accountId: target.accountId,
142
+ workspaceId: target.workspaceId,
143
+ sessionId: target.sessionId,
144
+ workflowId: target.workflowId,
145
+ wakeRevision: target.workflowWakeRevision,
146
+ }),
147
+ ),
148
+ );
149
+ }
150
+
151
+ async function signalPendingCodexCapacityTargets(
152
+ deps: ApiRouteDeps,
153
+ workspaceId: string,
154
+ ): Promise<void> {
155
+ const targets = await listPendingCodexCapacityWakeTargets(deps.db, workspaceId).catch(() => []);
156
+ await signalCodexCapacityTargets(deps, targets);
157
+ }
99
158
 
100
159
  const CODEX_DEVICE_EXPIRY_SECONDS = 15 * 60; // the device code expires 15 min after start (spec §1.1)
101
160
 
@@ -111,10 +170,22 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
111
170
  try {
112
171
  start = await startDeviceCode();
113
172
  } catch (error) {
114
- throw new HTTPException(502, { message: error instanceof CodexDeviceError ? error.message : "failed to start Codex device login" });
173
+ throw new HTTPException(502, {
174
+ message:
175
+ error instanceof CodexDeviceError ? error.message : "failed to start Codex device login",
176
+ });
115
177
  }
116
- const state = createSignedState(githubStateSecret, { workspaceId, deviceAuthId: start.deviceAuthId, userCode: start.userCode });
117
- return c.json({ userCode: start.userCode, verificationUri: start.verificationUri, intervalSeconds: start.intervalSeconds, state });
178
+ const state = createSignedState(githubStateSecret, {
179
+ workspaceId,
180
+ deviceAuthId: start.deviceAuthId,
181
+ userCode: start.userCode,
182
+ });
183
+ return c.json({
184
+ userCode: start.userCode,
185
+ verificationUri: start.verificationUri,
186
+ intervalSeconds: start.intervalSeconds,
187
+ state,
188
+ });
118
189
  });
119
190
 
120
191
  // Poll for authorization: pending | expired | connected (persists on success).
@@ -122,21 +193,36 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
122
193
  const workspaceId = c.req.param("workspaceId");
123
194
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
124
195
  const { state } = (await c.req.json()) as { state?: string };
125
- const payload = (state ? readSignedState(state, githubStateSecret) : null) as unknown as CodexConnectState | null;
126
- if (!payload || payload.workspaceId !== workspaceId || !payload.deviceAuthId || !payload.userCode) {
196
+ const payload = (state
197
+ ? readSignedState(state, githubStateSecret)
198
+ : null) as unknown as CodexConnectState | null;
199
+ if (
200
+ !payload ||
201
+ payload.workspaceId !== workspaceId ||
202
+ !payload.deviceAuthId ||
203
+ !payload.userCode
204
+ ) {
127
205
  throw new HTTPException(400, { message: "codex connect state is invalid or expired" });
128
206
  }
129
207
  // The device code itself expires 15 minutes after start; surface that to the
130
208
  // client (the 1-hour signed-state TTL is longer than the device window).
131
- if (typeof payload.iat === "number" && Date.now() / 1000 - payload.iat > CODEX_DEVICE_EXPIRY_SECONDS) {
209
+ if (
210
+ typeof payload.iat === "number" &&
211
+ Date.now() / 1000 - payload.iat > CODEX_DEVICE_EXPIRY_SECONDS
212
+ ) {
132
213
  return c.json({ status: "expired" });
133
214
  }
134
215
 
135
216
  let poll: Awaited<ReturnType<typeof pollDeviceCode>>;
136
217
  try {
137
- poll = await pollDeviceCode({ deviceAuthId: payload.deviceAuthId, userCode: payload.userCode });
218
+ poll = await pollDeviceCode({
219
+ deviceAuthId: payload.deviceAuthId,
220
+ userCode: payload.userCode,
221
+ });
138
222
  } catch (error) {
139
- throw new HTTPException(502, { message: error instanceof CodexDeviceError ? error.message : "codex device poll failed" });
223
+ throw new HTTPException(502, {
224
+ message: error instanceof CodexDeviceError ? error.message : "codex device poll failed",
225
+ });
140
226
  }
141
227
  if (poll.status === "pending") {
142
228
  return c.json({ status: "pending" });
@@ -147,42 +233,65 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
147
233
 
148
234
  let tokens: Awaited<ReturnType<typeof exchangeDeviceCode>>;
149
235
  try {
150
- tokens = await exchangeDeviceCode({ authorizationCode: poll.authorizationCode, codeVerifier: poll.codeVerifier });
236
+ tokens = await exchangeDeviceCode({
237
+ authorizationCode: poll.authorizationCode,
238
+ codeVerifier: poll.codeVerifier,
239
+ });
151
240
  } catch (error) {
152
- throw new HTTPException(502, { message: error instanceof CodexDeviceError ? error.message : "codex token exchange failed" });
241
+ throw new HTTPException(502, {
242
+ message: error instanceof CodexDeviceError ? error.message : "codex token exchange failed",
243
+ });
153
244
  }
154
245
  const id = parseIdToken(tokens.idToken);
155
246
  const key = environmentsEncryptionKeyBytes(settings);
156
247
  if (!key) {
157
- throw new HTTPException(500, { message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured" });
248
+ throw new HTTPException(500, {
249
+ message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured",
250
+ });
158
251
  }
159
- const upserted = await upsertCodexSubscriptionCredential(db, {
160
- accountId: grant.accountId,
161
- workspaceId,
162
- credentialEncrypted: encryptEnvironmentValue(
163
- key,
164
- JSON.stringify({ access_token: tokens.accessToken, refresh_token: tokens.refreshToken, id_token: tokens.idToken }),
165
- ),
166
- chatgptAccountId: id.chatgptAccountId,
167
- scopes: null, // device grant scopes are discovered at runtime, not asserted here
168
- planType: id.planType,
169
- isFedramp: id.isFedramp,
170
- expiresAt: accessTokenExpiry(tokens.accessToken),
171
- lastRefreshAt: new Date(),
172
- accountEmail: id.email ?? null,
173
- label: id.email ?? id.chatgptAccountId ?? null,
174
- });
252
+ await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
253
+ const mutation = await withCodexCapacityMutation(
254
+ db,
255
+ { workspaceId, reason: "codex_credential_connected" },
256
+ async (tx) => {
257
+ const upserted = await upsertCodexSubscriptionCredential(tx, {
258
+ accountId: grant.accountId,
259
+ workspaceId,
260
+ credentialEncrypted: encryptEnvironmentValue(
261
+ key,
262
+ JSON.stringify({
263
+ access_token: tokens.accessToken,
264
+ refresh_token: tokens.refreshToken,
265
+ id_token: tokens.idToken,
266
+ }),
267
+ ),
268
+ chatgptAccountId: id.chatgptAccountId,
269
+ scopes: null, // device grant scopes are discovered at runtime, not asserted here
270
+ planType: id.planType,
271
+ isFedramp: id.isFedramp,
272
+ expiresAt: accessTokenExpiry(tokens.accessToken),
273
+ lastRefreshAt: new Date(),
274
+ accountEmail: id.email ?? null,
275
+ label: id.email ?? id.chatgptAccountId ?? null,
276
+ });
277
+ return { result: upserted, changed: true };
278
+ },
279
+ );
280
+ const upserted = mutation.result;
175
281
  // Ensure the per-workspace rotation-settings row exists, then auto-activate
176
282
  // the FIRST account only. Additional new accounts do NOT auto-activate — a
177
283
  // manual switch is required (no auto-rotation in P1). A re-connect of the
178
284
  // already-active account is a no-op for the pointer.
179
- await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
285
+ // Keep both rotation bits false on first connect. The deployment flag makes
286
+ // the compatible allocator available, but the workspace-local cutover bit
287
+ // is enabled only by an explicit settings write after every worker replica
288
+ // understands leasing.
180
289
  const rotation = await getCodexRotationSettings(db, workspaceId);
181
290
  let isActive = rotation?.activeCredentialId === upserted.id;
182
291
  if (!isActive && rotation?.activeCredentialId == null) {
183
- await setActiveCodexCredential(db, workspaceId, upserted.id);
184
- isActive = true;
292
+ isActive = await setInitialActiveCodexCredential(db, workspaceId, upserted.id);
185
293
  }
294
+ await signalCodexCapacityTargets(deps, mutation.wakeTargets);
186
295
  return c.json({ status: "connected", plan: id.planType, accountId: upserted.id, isActive });
187
296
  });
188
297
 
@@ -198,12 +307,23 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
198
307
  const accounts = await listCodexAccountStatuses(db, workspaceId);
199
308
  const activeRow = accounts.find((account) => account.id === status.credentialId) ?? null;
200
309
  const activeAccount = activeRow
201
- ? { id: activeRow.id, label: activeRow.label ?? activeRow.accountEmail ?? activeRow.planType ?? activeRow.chatgptAccountId, chatgptAccountId: activeRow.chatgptAccountId }
310
+ ? {
311
+ id: activeRow.id,
312
+ label:
313
+ activeRow.label ??
314
+ activeRow.accountEmail ??
315
+ activeRow.planType ??
316
+ activeRow.chatgptAccountId,
317
+ chatgptAccountId: activeRow.chatgptAccountId,
318
+ }
202
319
  : null;
203
320
  let valid = false;
204
- let models = codexModelsForPicker([...CODEX_FALLBACK_MODEL_SLUGS]); // offline fallback list
321
+ let models: ReturnType<typeof codexModelsForPicker> = [];
322
+ let catalogError: string | null = null;
205
323
  try {
206
- const cred = status.credentialId ? await loadCodexCredentialForRun(db, settings, workspaceId, status.credentialId) : null;
324
+ const cred = status.credentialId
325
+ ? await loadCodexCredentialForRun(db, settings, workspaceId, status.credentialId)
326
+ : null;
207
327
  if (cred) {
208
328
  const live = await fetchCodexModels({
209
329
  accessToken: cred.tokens.accessToken,
@@ -211,20 +331,23 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
211
331
  isFedramp: cred.isFedramp,
212
332
  clientVersion: CODEX_CLIENT_VERSION,
213
333
  });
214
- valid = live.ok;
215
- if (live.ok && live.slugs.length > 0) {
216
- models = codexModelsForPicker(live.slugs); // prefer the live catalog
334
+ if (live.ok) {
335
+ models = codexModelsForPicker(live.slugs);
336
+ valid = true;
337
+ } else {
338
+ catalogError = `Codex models request failed with status ${live.status}`;
217
339
  }
218
340
  }
219
- } catch {
341
+ } catch (error) {
220
342
  valid = false;
343
+ catalogError = error instanceof Error ? error.message : String(error);
221
344
  }
222
345
  return c.json({
223
346
  connected: status.connected,
224
347
  plan: status.planType,
225
348
  valid,
226
349
  expiresAt: status.expiresAt,
227
- lastError: status.lastError,
350
+ lastError: catalogError ?? status.lastError,
228
351
  models, // ClientModel[] the picker surfaces under the "no credits" group
229
352
  activeAccount, // the account a session runs on when unpinned (label for the indicator)
230
353
  accountCount: accounts.length,
@@ -258,10 +381,19 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
258
381
  const workspaceId = c.req.param("workspaceId");
259
382
  await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
260
383
  const accountId = c.req.param("accountId");
261
- const activated = await setActiveCodexCredential(db, workspaceId, accountId);
384
+ const mutation = await withCodexCapacityMutation(
385
+ db,
386
+ { workspaceId, reason: "codex_active_credential_changed" },
387
+ async (tx) => {
388
+ const activated = await setActiveCodexCredential(tx, workspaceId, accountId);
389
+ return { result: activated, changed: activated };
390
+ },
391
+ );
392
+ const activated = mutation.result;
262
393
  if (!activated) {
263
394
  throw new HTTPException(404, { message: "codex account not found" });
264
395
  }
396
+ await signalCodexCapacityTargets(deps, mutation.wakeTargets);
265
397
  return c.json({ activated: true, accountId });
266
398
  });
267
399
 
@@ -270,7 +402,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
270
402
  app.patch("/v1/workspaces/:workspaceId/codex/settings", async (c) => {
271
403
  const workspaceId = c.req.param("workspaceId");
272
404
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
273
- const body = (await c.req.json().catch(() => ({}))) as { rotationEnabled?: unknown; rotationStrategy?: unknown };
405
+ const body = (await c.req.json().catch(() => ({}))) as {
406
+ rotationEnabled?: unknown;
407
+ rotationStrategy?: unknown;
408
+ };
274
409
  const patch: { rotationEnabled?: boolean; rotationStrategy?: CodexRotationStrategy } = {};
275
410
  if (typeof body.rotationEnabled === "boolean") {
276
411
  patch.rotationEnabled = body.rotationEnabled;
@@ -285,10 +420,19 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
285
420
  throw new HTTPException(400, { message: "no settings to update" });
286
421
  }
287
422
  await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
288
- const updated = await updateCodexRotationSettings(db, workspaceId, patch);
423
+ const mutation = await withCodexCapacityMutation(
424
+ db,
425
+ { workspaceId, reason: "codex_rotation_settings_changed" },
426
+ async (tx) => {
427
+ const updated = await updateCodexRotationSettings(tx, workspaceId, patch);
428
+ return { result: updated, changed: updated !== null };
429
+ },
430
+ );
431
+ const updated = mutation.result;
289
432
  if (!updated) {
290
433
  throw new HTTPException(404, { message: "codex rotation settings not found" });
291
434
  }
435
+ await signalCodexCapacityTargets(deps, mutation.wakeTargets);
292
436
  return c.json({
293
437
  rotationEnabled: updated.rotationEnabled,
294
438
  rotationStrategy: updated.rotationStrategy,
@@ -321,7 +465,16 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
321
465
  const workspaceId = c.req.param("workspaceId");
322
466
  await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
323
467
  const accountId = c.req.param("accountId");
324
- const result = await disconnectCodexAccount(db, workspaceId, accountId);
468
+ const mutation = await withCodexCapacityMutation(
469
+ db,
470
+ { workspaceId, reason: "codex_credential_disconnected" },
471
+ async (tx) => {
472
+ const result = await disconnectCodexAccount(tx, workspaceId, accountId);
473
+ return { result, changed: result.removed };
474
+ },
475
+ );
476
+ const result = mutation.result;
477
+ await signalCodexCapacityTargets(deps, mutation.wakeTargets);
325
478
  return c.json({ disconnected: result.removed, newActiveId: result.newActiveCredentialId });
326
479
  });
327
480
 
@@ -330,7 +483,16 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
330
483
  app.delete("/v1/workspaces/:workspaceId/codex", async (c) => {
331
484
  const workspaceId = c.req.param("workspaceId");
332
485
  await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
333
- const removed = await disconnectAllCodexAccounts(db, workspaceId);
486
+ const mutation = await withCodexCapacityMutation(
487
+ db,
488
+ { workspaceId, reason: "codex_credentials_disconnected" },
489
+ async (tx) => {
490
+ const removed = await disconnectAllCodexAccounts(tx, workspaceId);
491
+ return { result: removed, changed: removed > 0 };
492
+ },
493
+ );
494
+ const removed = mutation.result;
495
+ await signalCodexCapacityTargets(deps, mutation.wakeTargets);
334
496
  return c.json({ disconnected: removed > 0 });
335
497
  });
336
498
 
@@ -345,6 +507,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
345
507
  throw new HTTPException(404, { message: "codex subscription is not connected" });
346
508
  }
347
509
  const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, status.credentialId);
510
+ await signalPendingCodexCapacityTargets(deps, workspaceId);
348
511
  return c.json(codexUsageJson(payload));
349
512
  });
350
513
 
@@ -361,6 +524,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
361
524
  throw new HTTPException(404, { message: "codex account not found" });
362
525
  }
363
526
  const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, accountId);
527
+ await signalPendingCodexCapacityTargets(deps, workspaceId);
364
528
  return c.json(codexUsageJson(payload));
365
529
  });
366
530
 
@@ -373,21 +537,38 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
373
537
  const workspaceId = c.req.param("workspaceId");
374
538
  await requireAccessGrant(c, deps, workspaceId, "workspace:read");
375
539
  const accounts = await listCodexAccountStatuses(db, workspaceId);
376
- const usage: Record<string, { status: CodexUsagePayload["status"]; usage: CodexUsagePayload }> = {};
540
+ const usage: Record<string, { status: CodexUsagePayload["status"]; usage: CodexUsagePayload }> =
541
+ {};
377
542
  const queue = [...accounts];
378
543
  const CONCURRENCY = 4;
379
544
  const worker = async (): Promise<void> => {
380
545
  for (;;) {
381
546
  const account = queue.shift();
382
547
  if (!account) return;
383
- const settled = await Promise.allSettled([fetchCodexUsageForAccount(db, settings, workspaceId, account.id)]);
548
+ const settled = await Promise.allSettled([
549
+ fetchCodexUsageForAccount(db, settings, workspaceId, account.id),
550
+ ]);
384
551
  const result = settled[0];
385
- usage[account.id] = result.status === "fulfilled"
386
- ? codexUsageJson(result.value)
387
- : { status: "error", usage: { status: "error", planType: null, fiveHour: null, weekly: null, limitReached: false, fetchedAt: new Date().toISOString() } };
552
+ usage[account.id] =
553
+ result.status === "fulfilled"
554
+ ? codexUsageJson(result.value)
555
+ : {
556
+ status: "error",
557
+ usage: {
558
+ status: "error",
559
+ planType: null,
560
+ fiveHour: null,
561
+ weekly: null,
562
+ limitReached: false,
563
+ fetchedAt: new Date().toISOString(),
564
+ },
565
+ };
388
566
  }
389
567
  };
390
- await Promise.all(Array.from({ length: Math.min(CONCURRENCY, Math.max(1, accounts.length)) }, () => worker()));
568
+ await Promise.all(
569
+ Array.from({ length: Math.min(CONCURRENCY, Math.max(1, accounts.length)) }, () => worker()),
570
+ );
571
+ await signalPendingCodexCapacityTargets(deps, workspaceId);
391
572
  return c.json({ usage });
392
573
  });
393
574
  }
@@ -38,9 +38,11 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
38
38
  app.get("/v1/workspaces/:workspaceId/connections", async (c) => {
39
39
  const workspaceId = c.req.param("workspaceId");
40
40
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
41
- return c.json(ListConnectionsResponse.parse({
42
- connections: await listConnectionsMetadata(db, workspaceId, grant.subjectId),
43
- }));
41
+ return c.json(
42
+ ListConnectionsResponse.parse({
43
+ connections: await listConnectionsMetadata(db, workspaceId, grant.subjectId),
44
+ }),
45
+ );
44
46
  });
45
47
 
46
48
  app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
@@ -67,7 +69,12 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
67
69
  app.get("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
68
70
  const workspaceId = c.req.param("workspaceId");
69
71
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
70
- const connection = await getConnectionMetadata(db, workspaceId, c.req.param("connectionId"), grant.subjectId);
72
+ const connection = await getConnectionMetadata(
73
+ db,
74
+ workspaceId,
75
+ c.req.param("connectionId"),
76
+ grant.subjectId,
77
+ );
71
78
  if (!connection) {
72
79
  throw new HTTPException(404, { message: "connection not found" });
73
80
  }
@@ -84,26 +91,39 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
84
91
  // could clear the broker's re-auth signal while stale tokens stay in place.
85
92
  if (payload.status !== undefined) {
86
93
  if (payload.status !== "active") {
87
- throw new HTTPException(400, { message: "status can only be set to \"active\"; use DELETE to revoke" });
94
+ throw new HTTPException(400, {
95
+ message: 'status can only be set to "active"; use DELETE to revoke',
96
+ });
88
97
  }
89
98
  if (payload.credential === undefined) {
90
- throw new HTTPException(400, { message: "reactivating a connection requires a new credential" });
99
+ throw new HTTPException(400, {
100
+ message: "reactivating a connection requires a new credential",
101
+ });
91
102
  }
92
103
  }
93
104
  const key = payload.credential === undefined ? null : requireEnvironmentEncryption(settings);
94
- const subjectId = payload.subjectId === undefined ? undefined : writableSubjectId(payload.subjectId, grant.subjectId);
105
+ const subjectId =
106
+ payload.subjectId === undefined
107
+ ? undefined
108
+ : writableSubjectId(payload.subjectId, grant.subjectId);
95
109
  const connection = await updateConnection(db, {
96
110
  workspaceId,
97
111
  connectionId: c.req.param("connectionId"),
98
112
  visibleToSubjectId: grant.subjectId,
99
113
  updatedBySubjectId: grant.subjectId,
100
- ...(payload.providerDomain !== undefined ? { providerDomain: canonicalProviderDomain(payload.providerDomain) } : {}),
114
+ ...(payload.providerDomain !== undefined
115
+ ? { providerDomain: canonicalProviderDomain(payload.providerDomain) }
116
+ : {}),
101
117
  ...(subjectId !== undefined ? { subjectId } : {}),
102
118
  ...(payload.kind !== undefined ? { kind: payload.kind } : {}),
103
119
  ...(payload.status !== undefined ? { status: payload.status } : {}),
104
- ...(payload.credential !== undefined && key ? { credentialEncrypted: encryptCredentialBundle(key, payload.credential) } : {}),
120
+ ...(payload.credential !== undefined && key
121
+ ? { credentialEncrypted: encryptCredentialBundle(key, payload.credential) }
122
+ : {}),
105
123
  ...(payload.grantedScopes !== undefined ? { grantedScopes: payload.grantedScopes } : {}),
106
- ...(payload.expiresAt !== undefined ? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null } : {}),
124
+ ...(payload.expiresAt !== undefined
125
+ ? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null }
126
+ : {}),
107
127
  ...(payload.metadata !== undefined ? { metadata: payload.metadata } : {}),
108
128
  });
109
129
  if (!connection) {
@@ -115,7 +135,12 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
115
135
  app.delete("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
116
136
  const workspaceId = c.req.param("workspaceId");
117
137
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
118
- const connection = await revokeConnection(db, workspaceId, c.req.param("connectionId"), grant.subjectId);
138
+ const connection = await revokeConnection(
139
+ db,
140
+ workspaceId,
141
+ c.req.param("connectionId"),
142
+ grant.subjectId,
143
+ );
119
144
  if (!connection) {
120
145
  throw new HTTPException(404, { message: "connection not found" });
121
146
  }
@@ -128,44 +153,57 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
128
153
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
129
154
  const parsed = OAuthStartRequest.safeParse(await c.req.json());
130
155
  if (!parsed.success) {
131
- throw new HTTPException(400, { message: parsed.error.issues[0]?.message ?? "invalid OAuth start request" });
156
+ throw new HTTPException(400, {
157
+ message: parsed.error.issues[0]?.message ?? "invalid OAuth start request",
158
+ });
132
159
  }
133
160
  const payload = parsed.data;
134
- const result = await startMcpOAuth({ db, settings, observability }, {
135
- accountId: grant.accountId,
136
- workspaceId,
137
- subjectId: grant.subjectId,
138
- requestUrl: c.req.url,
139
- payload,
140
- });
161
+ const result = await startMcpOAuth(
162
+ { db, settings, observability },
163
+ {
164
+ accountId: grant.accountId,
165
+ workspaceId,
166
+ subjectId: grant.subjectId,
167
+ requestUrl: c.req.url,
168
+ payload,
169
+ },
170
+ );
141
171
  return c.json(OAuthStartResponse.parse(result));
142
172
  });
143
173
 
144
174
  app.get("/v1/integrations/oauth/callback", async (c) => {
145
175
  assertIntegrationsEnabled();
146
- const result = await completeMcpOAuthCallback({ db, settings, observability }, {
147
- code: c.req.query("code"),
148
- state: c.req.query("state"),
149
- requestUrl: c.req.url,
150
- });
176
+ const result = await completeMcpOAuthCallback(
177
+ { db, settings, observability },
178
+ {
179
+ code: c.req.query("code"),
180
+ state: c.req.query("state"),
181
+ requestUrl: c.req.url,
182
+ },
183
+ );
151
184
  return c.redirect(result.redirectTo, 302);
152
185
  });
153
186
 
154
187
  app.get("/v1/integrations/oauth/client-metadata.json", (c) => {
155
188
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
156
189
  const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
157
- return c.json(IntegrationClientMetadata.parse({
158
- client_id: metadataUrl,
159
- client_name: "OpenGeni",
160
- redirect_uris: [`${baseUrl}/v1/integrations/oauth/callback`],
161
- token_endpoint_auth_method: "none",
162
- grant_types: ["authorization_code", "refresh_token"],
163
- response_types: ["code"],
164
- }));
190
+ return c.json(
191
+ IntegrationClientMetadata.parse({
192
+ client_id: metadataUrl,
193
+ client_name: "OpenGeni",
194
+ redirect_uris: [`${baseUrl}/v1/integrations/oauth/callback`],
195
+ token_endpoint_auth_method: "none",
196
+ grant_types: ["authorization_code", "refresh_token"],
197
+ response_types: ["code"],
198
+ }),
199
+ );
165
200
  });
166
201
  }
167
202
 
168
- function writableSubjectId(requested: string | null | undefined, grantSubjectId: string): string | null {
203
+ function writableSubjectId(
204
+ requested: string | null | undefined,
205
+ grantSubjectId: string,
206
+ ): string | null {
169
207
  if (requested == null) {
170
208
  return null;
171
209
  }