@opengeni/api-router 2.5.0 → 2.6.4-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 (70) hide show
  1. package/dist/app.d.ts +2 -1
  2. package/dist/app.js +3 -1
  3. package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
  4. package/dist/auth/managed-auth.d.ts +24 -1
  5. package/dist/{chunk-QESX7HDK.js → chunk-UOI7KAP3.js} +3240 -791
  6. package/dist/chunk-UOI7KAP3.js.map +1 -0
  7. package/dist/http/sse.d.ts +9 -0
  8. package/dist/index.js +117 -5
  9. package/dist/index.js.map +1 -1
  10. package/dist/interaction-metrics.d.ts +2 -0
  11. package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
  12. package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
  13. package/dist/mcp/remember.d.ts +2 -2
  14. package/dist/mcp/scheduled-task-view.d.ts +3 -3
  15. package/dist/mcp/server.d.ts +1 -0
  16. package/dist/mcp/session-view.d.ts +1 -0
  17. package/dist/mcp/session-wait.d.ts +19 -0
  18. package/dist/model-catalog.d.ts +5 -31
  19. package/dist/routes/api-keys.d.ts +2 -0
  20. package/dist/routes/browser-sessions.d.ts +1 -0
  21. package/dist/routes/computer-sessions.d.ts +12 -0
  22. package/dist/routes/managed-auth-session-sets.d.ts +7 -0
  23. package/dist/routes/workspaces.d.ts +1 -1
  24. package/dist/sandbox/metrics-ingestion.d.ts +16 -0
  25. package/dist/workspace-delete-observability.d.ts +10 -0
  26. package/package.json +19 -19
  27. package/src/app.ts +206 -29
  28. package/src/auth/managed-auth-attempt-context.ts +40 -3
  29. package/src/auth/managed-auth-session-adapter.ts +1 -0
  30. package/src/auth/managed-auth.ts +164 -4
  31. package/src/http/sse.ts +279 -45
  32. package/src/index.ts +13 -1
  33. package/src/integrations/oauth-client.ts +8 -1
  34. package/src/integrations/provider-oauth.ts +12 -2
  35. package/src/integrations/slack-interactions.ts +77 -37
  36. package/src/interaction-metrics.ts +30 -0
  37. package/src/mcp/company-brain-governed-writes.ts +38 -23
  38. package/src/mcp/company-profile-agent-admin.ts +7 -7
  39. package/src/mcp/remember.ts +19 -8
  40. package/src/mcp/server.ts +328 -27
  41. package/src/mcp/session-wait.ts +56 -8
  42. package/src/model-catalog.ts +45 -337
  43. package/src/routes/api-integrations.ts +2 -2
  44. package/src/routes/api-keys.ts +149 -7
  45. package/src/routes/automations.ts +186 -19
  46. package/src/routes/browser-sessions.ts +10 -2
  47. package/src/routes/capabilities.ts +3 -3
  48. package/src/routes/codex.ts +483 -74
  49. package/src/routes/company-profile.ts +64 -0
  50. package/src/routes/computer-sessions.ts +103 -1
  51. package/src/routes/connections.ts +271 -16
  52. package/src/routes/github.ts +0 -8
  53. package/src/routes/integration-facets.ts +8 -5
  54. package/src/routes/interaction-resources.ts +7 -1
  55. package/src/routes/managed-auth-session-sets.ts +199 -2
  56. package/src/routes/organization-memberships.ts +28 -8
  57. package/src/routes/packs.ts +5 -5
  58. package/src/routes/plugins.ts +2 -2
  59. package/src/routes/pr-review.ts +80 -4
  60. package/src/routes/scheduled-tasks.ts +47 -11
  61. package/src/routes/sessions.ts +75 -53
  62. package/src/routes/skills.ts +3 -3
  63. package/src/routes/supergrok.ts +3 -2
  64. package/src/routes/workspaces.ts +665 -56
  65. package/src/sandbox/channel-a.ts +10 -4
  66. package/src/sandbox/machines.ts +13 -6
  67. package/src/sandbox/metrics-ingestion.ts +157 -3
  68. package/src/sandbox/viewer.ts +20 -1
  69. package/src/workspace-delete-observability.ts +75 -0
  70. package/dist/chunk-QESX7HDK.js.map +0 -1
@@ -1,73 +1,119 @@
1
+ import { createHash } from "node:crypto";
1
2
  import {
2
3
  AddWorkspaceMemberRequest,
3
4
  CreateWorkspaceRequest,
5
+ EnsureWorkspaceRequest,
6
+ EnsureWorkspaceResponse,
7
+ ListWorkspaceMemberCandidatesResponse,
4
8
  ListWorkspaceMembersResponse,
5
9
  SetWorkspaceDefaultRigRequest,
6
10
  UpdateWorkspaceMemberRequest,
11
+ CreateWorkspaceGatewayCustomModelRequest,
12
+ CreateWorkspaceOpenRouterCustomModelRequest,
13
+ DeleteWorkspaceGatewayCustomModelRequest,
14
+ DeleteWorkspaceOpenRouterCustomModelRequest,
7
15
  UpdateWorkspaceModelPolicyRequest,
8
16
  UpdateWorkspaceRequest,
9
17
  UpdateWorkspaceSettingsRequest,
10
18
  WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
11
19
  WorkspaceModelCatalogResponse,
20
+ WorkspaceGatewayCustomModel,
21
+ WorkspaceGatewayCustomModelsResponse,
22
+ WorkspaceOpenRouterCustomModel,
23
+ WorkspaceOpenRouterCustomModelsResponse,
12
24
  WorkspaceRealtimeModelCatalogResponse,
13
25
  WorkspaceInferenceControlRequest,
14
26
  Workspace,
15
27
  WorkspaceMember,
16
28
  workspaceControlUtf8Bytes,
29
+ stableJson,
17
30
  type AccessContext,
18
31
  type Permission,
32
+ type WorkspaceMemberCandidate,
19
33
  type WorkspaceMember as WorkspaceMemberValue,
20
34
  } from "@opengeni/contracts";
21
35
  import {
22
36
  allWorkspacePermissions,
23
37
  createWorkspace,
24
38
  deleteWorkspaceIfQuiescent,
25
- getManagedUserByEmail,
39
+ ensureWorkspaceByExternalIdentity,
40
+ findWorkspaceByExternalIdentity,
41
+ getManagedUserProfilesByIds,
26
42
  getWorkspaceModelPolicy,
27
43
  grantWorkspaceAccess,
28
44
  listWorkspaceMembers,
45
+ listWorkspaceMemberManagementCandidates,
29
46
  normalizeWorkspaceMembershipPermissions,
30
47
  listWorkspaceControlEvents,
48
+ listSharedWorkspacesForAccount,
49
+ listWorkspaceGatewayCustomModels,
50
+ listWorkspaceOpenRouterCustomModels,
51
+ createWorkspaceGatewayCustomModel,
52
+ createWorkspaceOpenRouterCustomModel,
53
+ deleteWorkspaceGatewayCustomModel,
54
+ deleteWorkspaceOpenRouterCustomModel,
55
+ replayWorkspaceGatewayCustomModelCreate,
56
+ replayWorkspaceOpenRouterCustomModelCreate,
31
57
  listWorkspacesForSubject,
58
+ nestedPostgresSqlState,
32
59
  removeWorkspaceMember,
33
60
  requireWorkspace,
34
61
  getRig,
35
62
  setWorkspaceDefaultRig,
36
63
  updateWorkspace,
37
64
  updateWorkspaceSettings,
65
+ upsertWorkspaceMemberAsWorkspaceManager,
38
66
  upsertWorkspaceModelPolicy,
39
67
  workspaceCodexSubscriptionActive,
40
68
  workspaceControlRequestLockTimeoutMs,
41
69
  workspaceXaiSubscriptionActive,
42
70
  workspaceVercelAiGatewayConnectionActive,
71
+ workspaceOpenRouterConnectionActive,
72
+ WorkspaceGatewayCustomModelHistoryLimitError,
73
+ WorkspaceOpenRouterCustomModelHistoryLimitError,
74
+ WorkspaceExternalIdentityConflictError,
75
+ WorkspaceGatewayCustomModelLimitError,
76
+ WorkspaceOpenRouterCustomModelLimitError,
77
+ WorkspaceLimitExceededError,
43
78
  } from "@opengeni/db";
44
79
  import { boundWorkspaceControlHttpPage } from "@opengeni/events";
45
80
  import type { Hono } from "hono";
46
81
  import { HTTPException } from "hono/http-exception";
47
82
  import {
48
83
  getManagedAuthRequestActorEpoch,
84
+ accountScopedApiKeyWorkspaceAuthority,
49
85
  hasPermission,
50
86
  requireAccessContext,
51
87
  requireAccessGrant,
52
88
  requireFreshAccessGrant,
89
+ resolveWorkspaceCatalogSettings,
53
90
  } from "@opengeni/core";
54
91
  import { requireLimit } from "@opengeni/core";
55
92
  import type { ApiRouteDeps } from "@opengeni/core";
56
93
  import {
57
94
  assertWorkspaceMemberRemovable,
95
+ assertWorkspaceMemberUpdateAllowed,
58
96
  controlHumanWorkspace,
59
- resolveMemberSubjectId,
60
97
  } from "@opengeni/core";
61
98
  import { boundedLimit } from "../http/common";
62
99
  import { ApiHttpError } from "../http/api-error";
63
- import { sseWorkspaceControlStream } from "../http/sse";
100
+ import { browserSseDeliveryOptions, sseWorkspaceControlStream } from "../http/sse";
64
101
  import { buildWorkspaceModelCatalog } from "../model-catalog";
65
102
  import { processTemporalScheduleCleanupClaims } from "../temporal-schedule-cleanup";
103
+ import { workspaceDeleteObserver } from "../workspace-delete-observability";
66
104
  import {
67
105
  AI_GATEWAY_REALTIME_MODELS,
68
106
  CODEX_REALTIME_MODEL_ID,
69
107
  SUPERGROK_REALTIME_MODEL_ID,
70
108
  canonicalizeConfiguredModelId,
109
+ configuredStaticUsageLimits,
110
+ configuredGatewayUpstreamModelIds,
111
+ configuredGatewayWorkspaceProductModelIds,
112
+ configuredModelInputIdentities,
113
+ configuredOpenRouterUpstreamModelIds,
114
+ configuredOpenRouterWorkspaceProductModelIds,
115
+ WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
116
+ WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
71
117
  type Settings,
72
118
  } from "@opengeni/config";
73
119
 
@@ -81,6 +127,46 @@ export function canonicalWorkspacePolicyModelIds(
81
127
  return [...new Set(modelIds.map((modelId) => canonicalizeConfiguredModelId(settings, modelId)))];
82
128
  }
83
129
 
130
+ function projectWorkspaceGatewayCustomModel(model: {
131
+ id: string;
132
+ upstreamModelId: string;
133
+ label: string | null;
134
+ version: number;
135
+ createdAt: Date;
136
+ updatedAt: Date;
137
+ }) {
138
+ return WorkspaceGatewayCustomModel.parse({
139
+ id: model.id,
140
+ upstreamModelId: model.upstreamModelId,
141
+ label: model.label,
142
+ version: model.version,
143
+ createdAt: model.createdAt.toISOString(),
144
+ updatedAt: model.updatedAt.toISOString(),
145
+ });
146
+ }
147
+
148
+ function projectWorkspaceOpenRouterCustomModel(model: {
149
+ id: string;
150
+ upstreamModelId: string;
151
+ label: string | null;
152
+ version: number;
153
+ createdAt: Date;
154
+ updatedAt: Date;
155
+ }) {
156
+ return WorkspaceOpenRouterCustomModel.parse({
157
+ id: model.id,
158
+ upstreamModelId: model.upstreamModelId,
159
+ label: model.label,
160
+ version: model.version,
161
+ createdAt: model.createdAt.toISOString(),
162
+ updatedAt: model.updatedAt.toISOString(),
163
+ });
164
+ }
165
+
166
+ function workspaceCustomModelRequestHash(value: unknown): string {
167
+ return createHash("sha256").update(stableJson(value), "utf8").digest("hex");
168
+ }
169
+
84
170
  type WorkspaceMemberProjectionInput = Omit<WorkspaceMemberValue, "permissions"> & {
85
171
  permissions: unknown;
86
172
  };
@@ -114,6 +200,17 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
114
200
 
115
201
  app.get("/v1/workspaces", async (c) => {
116
202
  const context = await requireAccessContext(c, deps);
203
+ const accountScopedAuthority = accountScopedApiKeyWorkspaceAuthority(context);
204
+ if (
205
+ accountScopedAuthority &&
206
+ hasPermission(accountScopedAuthority.permissions, "workspace:read")
207
+ ) {
208
+ return c.json(
209
+ (await listSharedWorkspacesForAccount(deps.db, accountScopedAuthority.accountId)).map(
210
+ (workspace) => Workspace.parse(workspace),
211
+ ),
212
+ );
213
+ }
117
214
  const readableWorkspaceIds = [
118
215
  ...new Set(
119
216
  context.workspaceGrants
@@ -134,34 +231,103 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
134
231
  );
135
232
  });
136
233
 
234
+ app.put("/v1/workspaces/external", async (c) => {
235
+ const context = await requireAccessContext(c, deps);
236
+ const payload = EnsureWorkspaceRequest.parse(await c.req.json());
237
+ requireAccountPermission(context, payload.accountId, "workspace:create");
238
+ try {
239
+ const existing = await findWorkspaceByExternalIdentity(deps.db, {
240
+ externalSource: payload.externalSource,
241
+ externalId: payload.externalId,
242
+ });
243
+ if (existing) {
244
+ if (existing.accountId !== payload.accountId || existing.kind !== "shared") {
245
+ throw new WorkspaceExternalIdentityConflictError();
246
+ }
247
+ return c.json(
248
+ EnsureWorkspaceResponse.parse({
249
+ workspace: existing,
250
+ created: false,
251
+ }),
252
+ );
253
+ }
254
+ await requireLimit(deps, {
255
+ accountId: payload.accountId,
256
+ action: "workspace:create",
257
+ quantity: 1,
258
+ });
259
+ const result = await ensureWorkspaceByExternalIdentity(deps.db, {
260
+ accountId: payload.accountId,
261
+ externalSource: payload.externalSource,
262
+ externalId: payload.externalId,
263
+ name: payload.name,
264
+ slug: payload.slug ?? null,
265
+ ...(payload.agentInstructions !== undefined
266
+ ? {
267
+ agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
268
+ }
269
+ : {}),
270
+ maxWorkspacesPerAccount: workspaceLimit(deps),
271
+ });
272
+ const response = EnsureWorkspaceResponse.parse(result);
273
+ return result.created ? c.json(response, 201) : c.json(response);
274
+ } catch (error) {
275
+ if (error instanceof WorkspaceExternalIdentityConflictError) {
276
+ throw new HTTPException(409, {
277
+ message: "external workspace identity is already in use",
278
+ });
279
+ }
280
+ if (error instanceof WorkspaceLimitExceededError) {
281
+ throw new HTTPException(429, { message: error.message });
282
+ }
283
+ throw error;
284
+ }
285
+ });
286
+
137
287
  app.post("/v1/workspaces", async (c) => {
138
288
  const context = await requireAccessContext(c, deps);
139
289
  const payload = CreateWorkspaceRequest.parse(await c.req.json());
140
290
  const accountId = payload.accountId ?? context.defaultAccountId;
141
291
  if (!accountId) {
142
- throw new HTTPException(409, { message: "account selection is required" });
292
+ throw new HTTPException(409, {
293
+ message: "account selection is required",
294
+ });
143
295
  }
144
296
  requireAccountPermission(context, accountId, "workspace:create");
145
- await requireLimit(deps, { accountId, action: "workspace:create", quantity: 1 });
146
- const workspace = await createWorkspace(deps.db, {
147
- accountId,
148
- name: payload.name.trim(),
149
- slug: payload.slug?.trim() || null,
150
- externalSource: payload.externalSource ?? null,
151
- externalId: payload.externalId ?? null,
152
- ...(payload.agentInstructions !== undefined
153
- ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions) }
154
- : {}),
155
- });
156
- await grantWorkspaceAccess(deps.db, {
297
+ await requireLimit(deps, {
157
298
  accountId,
158
- workspaceId: workspace.id,
159
- subjectId: context.subjectId,
160
- role: "owner",
161
- permissions: allWorkspacePermissions,
162
- ...(context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}),
299
+ action: "workspace:create",
300
+ quantity: 1,
163
301
  });
164
- return c.json(Workspace.parse(workspace), 201);
302
+ try {
303
+ const workspace = await createWorkspace(deps.db, {
304
+ accountId,
305
+ name: payload.name.trim(),
306
+ slug: payload.slug?.trim() || null,
307
+ externalSource: payload.externalSource ?? null,
308
+ externalId: payload.externalId ?? null,
309
+ ...(payload.agentInstructions !== undefined
310
+ ? {
311
+ agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
312
+ }
313
+ : {}),
314
+ maxWorkspacesPerAccount: workspaceLimit(deps),
315
+ });
316
+ await grantWorkspaceAccess(deps.db, {
317
+ accountId,
318
+ workspaceId: workspace.id,
319
+ subjectId: context.subjectId,
320
+ role: "owner",
321
+ permissions: allWorkspacePermissions,
322
+ ...(context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}),
323
+ });
324
+ return c.json(Workspace.parse(workspace), 201);
325
+ } catch (error) {
326
+ if (error instanceof WorkspaceLimitExceededError) {
327
+ throw new HTTPException(429, { message: error.message });
328
+ }
329
+ throw error;
330
+ }
165
331
  });
166
332
 
167
333
  app.get("/v1/workspaces/:workspaceId", async (c) => {
@@ -189,7 +355,9 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
189
355
  ...(payload.name !== undefined ? { name: payload.name.trim() } : {}),
190
356
  ...(payload.slug !== undefined ? { slug: payload.slug?.trim() || null } : {}),
191
357
  ...(payload.agentInstructions !== undefined
192
- ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions) }
358
+ ? {
359
+ agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
360
+ }
193
361
  : {}),
194
362
  });
195
363
  return c.json(Workspace.parse(workspace));
@@ -202,7 +370,9 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
202
370
  await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
203
371
  const parsed = UpdateWorkspaceSettingsRequest.safeParse(await c.req.json());
204
372
  if (!parsed.success) {
205
- throw new HTTPException(400, { message: "invalid workspace settings patch" });
373
+ throw new HTTPException(400, {
374
+ message: "invalid workspace settings patch",
375
+ });
206
376
  }
207
377
  // Request-scoped: bound the exclusive control-prefix wait so a busy
208
378
  // workspace yields the retryable 503 instead of parking this request.
@@ -220,30 +390,323 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
220
390
  const workspaceId = c.req.param("workspaceId");
221
391
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
222
392
  const [
393
+ resolvedCatalog,
223
394
  policy,
224
395
  codexSubscriptionActive,
225
396
  xaiSubscriptionActive,
226
397
  workspaceGatewayConnectionActive,
398
+ workspaceGatewayCustomModels,
399
+ openRouterConnectionActive,
400
+ workspaceOpenRouterCustomModels,
227
401
  ] = await Promise.all([
402
+ deps.resolveCatalogSettings(),
228
403
  getWorkspaceModelPolicy(deps.db, workspaceId),
229
404
  workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
230
405
  workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
231
406
  workspaceVercelAiGatewayConnectionActive(deps.db, workspaceId),
407
+ listWorkspaceGatewayCustomModels(deps.db, {
408
+ accountId: grant.accountId,
409
+ workspaceId,
410
+ }),
411
+ workspaceOpenRouterConnectionActive(deps.db, workspaceId),
412
+ listWorkspaceOpenRouterCustomModels(deps.db, {
413
+ accountId: grant.accountId,
414
+ workspaceId,
415
+ }),
232
416
  ]);
233
417
  c.header("cache-control", "private, no-store");
234
418
  return c.json(
235
419
  WorkspaceModelCatalogResponse.parse(
236
420
  buildWorkspaceModelCatalog({
237
- settings: deps.settings,
421
+ settings: resolvedCatalog.settings,
238
422
  policy,
239
423
  codexSubscriptionActive,
240
424
  xaiSubscriptionActive,
241
425
  workspaceGatewayConnectionActive,
426
+ workspaceGatewayCustomModels,
427
+ workspaceOpenRouterConnectionActive: openRouterConnectionActive,
428
+ workspaceOpenRouterCustomModels,
242
429
  }),
243
430
  ),
244
431
  );
245
432
  });
246
433
 
434
+ app.get("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
435
+ const workspaceId = c.req.param("workspaceId");
436
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
437
+ const models = await listWorkspaceGatewayCustomModels(deps.db, {
438
+ accountId: grant.accountId,
439
+ workspaceId,
440
+ });
441
+ c.header("cache-control", "private, no-store");
442
+ return c.json(
443
+ WorkspaceGatewayCustomModelsResponse.parse({
444
+ models: models.map(projectWorkspaceGatewayCustomModel),
445
+ }),
446
+ );
447
+ });
448
+
449
+ app.post("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
450
+ const workspaceId = c.req.param("workspaceId");
451
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
452
+ const parsed = CreateWorkspaceGatewayCustomModelRequest.safeParse(
453
+ await c.req.json().catch(() => null),
454
+ );
455
+ if (!parsed.success) {
456
+ throw new HTTPException(422, { message: "invalid Gateway custom model" });
457
+ }
458
+ const requestHash = workspaceCustomModelRequestHash({
459
+ action: "create",
460
+ upstreamModelId: parsed.data.upstreamModelId,
461
+ label: parsed.data.label ?? null,
462
+ });
463
+ const replay = await replayWorkspaceGatewayCustomModelCreate(deps.db, {
464
+ accountId: grant.accountId,
465
+ workspaceId,
466
+ operationId: parsed.data.operationId,
467
+ requestHash,
468
+ });
469
+ if (replay.outcome === "conflict") {
470
+ throw new HTTPException(409, {
471
+ message: "Gateway custom model operation conflicts with current state",
472
+ });
473
+ }
474
+ if (replay.outcome === "success") {
475
+ return c.json(projectWorkspaceGatewayCustomModel(replay.model), 201);
476
+ }
477
+ const catalog = await deps.resolveCatalogSettings();
478
+ if (configuredGatewayUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)) {
479
+ throw new HTTPException(422, {
480
+ message: "Gateway model is already included in the deployment catalog",
481
+ });
482
+ }
483
+ const customProductId = `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`;
484
+ const deploymentProductIds = new Set([
485
+ ...configuredModelInputIdentities(catalog.settings),
486
+ ...configuredGatewayWorkspaceProductModelIds(catalog.settings),
487
+ ]);
488
+ if (deploymentProductIds.has(customProductId)) {
489
+ throw new HTTPException(422, {
490
+ message: "Gateway model product id conflicts with the deployment catalog",
491
+ });
492
+ }
493
+ try {
494
+ const model = await createWorkspaceGatewayCustomModel(deps.db, {
495
+ accountId: grant.accountId,
496
+ workspaceId,
497
+ upstreamModelId: parsed.data.upstreamModelId,
498
+ label: parsed.data.label ?? null,
499
+ operationId: parsed.data.operationId,
500
+ requestHash,
501
+ createdBySubjectId: grant.subjectId,
502
+ });
503
+ if (!model || model.retiredAt) {
504
+ throw new HTTPException(409, {
505
+ message: "Gateway custom model operation conflicts with current state",
506
+ });
507
+ }
508
+ return c.json(projectWorkspaceGatewayCustomModel(model), 201);
509
+ } catch (error) {
510
+ if (
511
+ error instanceof WorkspaceGatewayCustomModelLimitError ||
512
+ error instanceof WorkspaceGatewayCustomModelHistoryLimitError
513
+ ) {
514
+ throw new HTTPException(422, { message: error.message });
515
+ }
516
+ if (nestedPostgresSqlState(error) === "23505") {
517
+ throw new HTTPException(422, {
518
+ message: "Gateway custom model already exists",
519
+ });
520
+ }
521
+ throw error;
522
+ }
523
+ });
524
+
525
+ app.delete("/v1/workspaces/:workspaceId/gateway-custom-models/:customModelId", async (c) => {
526
+ const workspaceId = c.req.param("workspaceId");
527
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
528
+ const customModelId = c.req.param("customModelId");
529
+ if (
530
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
531
+ customModelId,
532
+ )
533
+ ) {
534
+ throw new HTTPException(422, {
535
+ message: "invalid Gateway custom model id",
536
+ });
537
+ }
538
+ const parsed = DeleteWorkspaceGatewayCustomModelRequest.safeParse(
539
+ await c.req.json().catch(() => null),
540
+ );
541
+ if (!parsed.success) {
542
+ throw new HTTPException(422, {
543
+ message: "invalid Gateway custom model deletion",
544
+ });
545
+ }
546
+ const removed = await deleteWorkspaceGatewayCustomModel(deps.db, {
547
+ accountId: grant.accountId,
548
+ workspaceId,
549
+ customModelId,
550
+ expectedVersion: parsed.data.expectedVersion,
551
+ operationId: parsed.data.operationId,
552
+ requestHash: workspaceCustomModelRequestHash({
553
+ action: "delete",
554
+ customModelId,
555
+ expectedVersion: parsed.data.expectedVersion,
556
+ }),
557
+ });
558
+ if (removed.outcome === "not_found") {
559
+ throw new HTTPException(404, {
560
+ message: "Gateway custom model not found",
561
+ });
562
+ }
563
+ if (removed.outcome === "conflict") {
564
+ throw new HTTPException(409, {
565
+ message: "Gateway custom model changed; reload and retry",
566
+ });
567
+ }
568
+ return c.body(null, 204);
569
+ });
570
+
571
+ app.get("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => {
572
+ const workspaceId = c.req.param("workspaceId");
573
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
574
+ const models = await listWorkspaceOpenRouterCustomModels(deps.db, {
575
+ accountId: grant.accountId,
576
+ workspaceId,
577
+ });
578
+ c.header("cache-control", "private, no-store");
579
+ return c.json(
580
+ WorkspaceOpenRouterCustomModelsResponse.parse({
581
+ models: models.map(projectWorkspaceOpenRouterCustomModel),
582
+ }),
583
+ );
584
+ });
585
+
586
+ app.post("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => {
587
+ const workspaceId = c.req.param("workspaceId");
588
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
589
+ const parsed = CreateWorkspaceOpenRouterCustomModelRequest.safeParse(
590
+ await c.req.json().catch(() => null),
591
+ );
592
+ if (!parsed.success) {
593
+ throw new HTTPException(422, { message: "invalid OpenRouter custom model" });
594
+ }
595
+ const requestHash = workspaceCustomModelRequestHash({
596
+ action: "create",
597
+ upstreamModelId: parsed.data.upstreamModelId,
598
+ label: parsed.data.label ?? null,
599
+ });
600
+ const replay = await replayWorkspaceOpenRouterCustomModelCreate(deps.db, {
601
+ accountId: grant.accountId,
602
+ workspaceId,
603
+ operationId: parsed.data.operationId,
604
+ requestHash,
605
+ });
606
+ if (replay.outcome === "conflict") {
607
+ throw new HTTPException(409, {
608
+ message: "OpenRouter custom model operation conflicts with current state",
609
+ });
610
+ }
611
+ if (replay.outcome === "success") {
612
+ return c.json(projectWorkspaceOpenRouterCustomModel(replay.model), 201);
613
+ }
614
+ const catalog = await deps.resolveCatalogSettings();
615
+ if (
616
+ configuredOpenRouterUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)
617
+ ) {
618
+ throw new HTTPException(422, {
619
+ message: "OpenRouter model is already included in the deployment catalog",
620
+ });
621
+ }
622
+ const customProductId = `${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`;
623
+ const deploymentProductIds = new Set([
624
+ ...configuredModelInputIdentities(catalog.settings),
625
+ ...configuredOpenRouterWorkspaceProductModelIds(catalog.settings),
626
+ ]);
627
+ if (deploymentProductIds.has(customProductId)) {
628
+ throw new HTTPException(422, {
629
+ message: "OpenRouter model product id conflicts with the deployment catalog",
630
+ });
631
+ }
632
+ try {
633
+ const model = await createWorkspaceOpenRouterCustomModel(deps.db, {
634
+ accountId: grant.accountId,
635
+ workspaceId,
636
+ upstreamModelId: parsed.data.upstreamModelId,
637
+ label: parsed.data.label ?? null,
638
+ operationId: parsed.data.operationId,
639
+ requestHash,
640
+ createdBySubjectId: grant.subjectId,
641
+ });
642
+ if (!model || model.retiredAt) {
643
+ throw new HTTPException(409, {
644
+ message: "OpenRouter custom model operation conflicts with current state",
645
+ });
646
+ }
647
+ return c.json(projectWorkspaceOpenRouterCustomModel(model), 201);
648
+ } catch (error) {
649
+ if (
650
+ error instanceof WorkspaceOpenRouterCustomModelLimitError ||
651
+ error instanceof WorkspaceOpenRouterCustomModelHistoryLimitError
652
+ ) {
653
+ throw new HTTPException(422, { message: error.message });
654
+ }
655
+ if (nestedPostgresSqlState(error) === "23505") {
656
+ throw new HTTPException(422, {
657
+ message: "OpenRouter custom model already exists",
658
+ });
659
+ }
660
+ throw error;
661
+ }
662
+ });
663
+
664
+ app.delete("/v1/workspaces/:workspaceId/openrouter-custom-models/:customModelId", async (c) => {
665
+ const workspaceId = c.req.param("workspaceId");
666
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
667
+ const customModelId = c.req.param("customModelId");
668
+ if (
669
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
670
+ customModelId,
671
+ )
672
+ ) {
673
+ throw new HTTPException(422, {
674
+ message: "invalid OpenRouter custom model id",
675
+ });
676
+ }
677
+ const parsed = DeleteWorkspaceOpenRouterCustomModelRequest.safeParse(
678
+ await c.req.json().catch(() => null),
679
+ );
680
+ if (!parsed.success) {
681
+ throw new HTTPException(422, {
682
+ message: "invalid OpenRouter custom model deletion",
683
+ });
684
+ }
685
+ const removed = await deleteWorkspaceOpenRouterCustomModel(deps.db, {
686
+ accountId: grant.accountId,
687
+ workspaceId,
688
+ customModelId,
689
+ expectedVersion: parsed.data.expectedVersion,
690
+ operationId: parsed.data.operationId,
691
+ requestHash: workspaceCustomModelRequestHash({
692
+ action: "delete",
693
+ customModelId,
694
+ expectedVersion: parsed.data.expectedVersion,
695
+ }),
696
+ });
697
+ if (removed.outcome === "not_found") {
698
+ throw new HTTPException(404, {
699
+ message: "OpenRouter custom model not found",
700
+ });
701
+ }
702
+ if (removed.outcome === "conflict") {
703
+ throw new HTTPException(409, {
704
+ message: "OpenRouter custom model changed; reload and retry",
705
+ });
706
+ }
707
+ return c.body(null, 204);
708
+ });
709
+
247
710
  app.get("/v1/workspaces/:workspaceId/realtime-model-catalog", async (c) => {
248
711
  const workspaceId = c.req.param("workspaceId");
249
712
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
@@ -320,11 +783,15 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
320
783
  const workspaceId = c.req.param("workspaceId");
321
784
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
322
785
  const payload = UpdateWorkspaceModelPolicyRequest.parse(await c.req.json());
786
+ const catalog = await resolveWorkspaceCatalogSettings(deps.db, deps.settings, {
787
+ accountId: grant.accountId,
788
+ workspaceId,
789
+ });
323
790
  const policy = await upsertWorkspaceModelPolicy(deps.db, {
324
791
  accountId: grant.accountId,
325
792
  workspaceId,
326
793
  allowedProviders: payload.allowedProviders ?? null,
327
- allowedModels: canonicalWorkspacePolicyModelIds(deps.settings, payload.allowedModels),
794
+ allowedModels: canonicalWorkspacePolicyModelIds(catalog.settings, payload.allowedModels),
328
795
  });
329
796
  return c.json(policy);
330
797
  });
@@ -333,11 +800,15 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
333
800
  const workspaceId = c.req.param("workspaceId");
334
801
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
335
802
  if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) {
336
- throw new HTTPException(400, { message: "workspace-control actor is too large" });
803
+ throw new HTTPException(400, {
804
+ message: "workspace-control actor is too large",
805
+ });
337
806
  }
338
807
  const parsed = WorkspaceInferenceControlRequest.safeParse(await c.req.json().catch(() => null));
339
808
  if (!parsed.success) {
340
- throw new HTTPException(400, { message: "invalid workspace inference-control request" });
809
+ throw new HTTPException(400, {
810
+ message: "invalid workspace inference-control request",
811
+ });
341
812
  }
342
813
  return c.json(
343
814
  await controlHumanWorkspace(
@@ -383,6 +854,7 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
383
854
  after,
384
855
  c.req.raw.signal,
385
856
  {
857
+ ...browserSseDeliveryOptions(c.req.query("transport")),
386
858
  observability: deps.observability,
387
859
  actorEpoch: getManagedAuthRequestActorEpoch(c.req.raw) ?? undefined,
388
860
  reauthorize: async () => {
@@ -399,7 +871,9 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
399
871
  if (payload.rigId) {
400
872
  const rig = await getRig(deps.db, workspaceId, payload.rigId);
401
873
  if (!rig) {
402
- throw new HTTPException(422, { message: `unknown rigId: ${payload.rigId}` });
874
+ throw new HTTPException(422, {
875
+ message: `unknown rigId: ${payload.rigId}`,
876
+ });
403
877
  }
404
878
  }
405
879
  const workspace = await setWorkspaceDefaultRig(deps.db, workspaceId, payload.rigId);
@@ -413,15 +887,22 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
413
887
  // session/lease before checking runtime quiescence, then returns the exact
414
888
  // external schedules removed by the cascade. A racing cold->warm transition
415
889
  // can therefore never erase the only provider/capture ownership receipt.
890
+ const deleteObserver = workspaceDeleteObserver(deps.observability, {
891
+ accountId: grant.accountId,
892
+ workspaceId,
893
+ });
416
894
  const deleted = await deleteWorkspaceIfQuiescent(deps.db, {
417
895
  accountId: grant.accountId,
418
896
  workspaceId,
897
+ ...(deleteObserver ? { observer: deleteObserver } : {}),
419
898
  });
420
899
  if (deleted.status === "not_found") {
421
900
  throw new HTTPException(404, { message: "workspace not found" });
422
901
  }
423
902
  if (deleted.status === "only_workspace") {
424
- throw new HTTPException(409, { message: "cannot delete the account's only workspace" });
903
+ throw new HTTPException(409, {
904
+ message: "cannot delete the account's only workspace",
905
+ });
425
906
  }
426
907
  if (deleted.status === "active_sessions") {
427
908
  throw new HTTPException(409, {
@@ -453,7 +934,9 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
453
934
  {
454
935
  db: deps.db,
455
936
  deleteSchedule: async (temporalScheduleId) => {
456
- await deps.workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId });
937
+ await deps.workflowClient.deleteScheduledTaskSchedule({
938
+ temporalScheduleId,
939
+ });
457
940
  },
458
941
  ...(deps.observability ? { observability: deps.observability } : {}),
459
942
  },
@@ -467,28 +950,73 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
467
950
  app.get("/v1/workspaces/:workspaceId/members", async (c) => {
468
951
  const workspaceId = c.req.param("workspaceId");
469
952
  await requireAccessGrant(c, deps, workspaceId, "workspace:read");
470
- const members = await listWorkspaceMembers(deps.db, workspaceId);
953
+ const members = await listWorkspacePeople(deps, workspaceId);
471
954
  return c.json(workspaceMembersResponse(members));
472
955
  });
473
956
 
957
+ app.get("/v1/workspaces/:workspaceId/member-candidates", async (c) => {
958
+ const workspaceId = c.req.param("workspaceId");
959
+ const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage");
960
+ try {
961
+ return c.json(
962
+ ListWorkspaceMemberCandidatesResponse.parse({
963
+ members: await listWorkspaceMemberManagementCandidates(deps.db, {
964
+ accountId: grant.accountId,
965
+ workspaceId,
966
+ actorSubjectId: grant.subjectId,
967
+ }),
968
+ }),
969
+ );
970
+ } catch (error) {
971
+ rethrowWorkspaceMemberCandidateError(error);
972
+ }
973
+ });
974
+
474
975
  app.post("/v1/workspaces/:workspaceId/members", async (c) => {
475
976
  const workspaceId = c.req.param("workspaceId");
476
977
  const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage");
477
978
  const payload = AddWorkspaceMemberRequest.parse(await c.req.json());
478
- const email = payload.email.trim();
479
- // Email invites for not-yet-registered users are deferred: an unknown email
480
- // resolves to null, which resolveMemberSubjectId turns into a 404.
481
- const subjectId = resolveMemberSubjectId(await getManagedUserByEmail(deps.db, email));
482
- await grantWorkspaceAccess(deps.db, {
483
- accountId: grant.accountId,
484
- workspaceId,
485
- subjectId,
486
- subjectLabel: email,
487
- ...(payload.role !== undefined ? { role: payload.role } : {}),
488
- permissions: payload.permissions,
489
- });
490
- const members = await listWorkspaceMembers(deps.db, workspaceId);
491
- const member = members.find((candidate) => candidate.subjectId === subjectId);
979
+ let candidates: WorkspaceMemberCandidate[];
980
+ try {
981
+ candidates = await listWorkspaceMemberManagementCandidates(deps.db, {
982
+ accountId: grant.accountId,
983
+ workspaceId,
984
+ actorSubjectId: grant.subjectId,
985
+ });
986
+ } catch (error) {
987
+ rethrowWorkspaceMemberCandidateError(error);
988
+ }
989
+ const candidate = candidates.find(
990
+ (entry) => entry.organizationMembershipId === payload.organizationMembershipId,
991
+ );
992
+ if (!candidate) {
993
+ throw new HTTPException(404, {
994
+ message: "this organization member is not available to add",
995
+ });
996
+ }
997
+ const subjectId = candidate.subjectId;
998
+ const existing = await listWorkspaceMembers(deps.db, workspaceId);
999
+ if (existing.some((member) => member.subjectId === subjectId)) {
1000
+ throw new HTTPException(409, {
1001
+ message: "this person already has access to the workspace",
1002
+ });
1003
+ }
1004
+ try {
1005
+ await upsertWorkspaceMemberAsWorkspaceManager(deps.db, {
1006
+ accountId: grant.accountId,
1007
+ workspaceId,
1008
+ actorSubjectId: grant.subjectId,
1009
+ targetSubjectId: subjectId,
1010
+ mode: "add",
1011
+ subjectLabel: candidate.name?.trim() || candidate.email || subjectId,
1012
+ role: payload.role ?? "member",
1013
+ permissions: payload.permissions,
1014
+ });
1015
+ } catch (error) {
1016
+ rethrowWorkspaceMemberCandidateError(error);
1017
+ }
1018
+ const members = await listWorkspacePeople(deps, workspaceId);
1019
+ const member = members.find((addedMember) => addedMember.subjectId === subjectId);
492
1020
  if (!member) {
493
1021
  throw new HTTPException(500, { message: "failed to add member" });
494
1022
  }
@@ -500,20 +1028,32 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
500
1028
  const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage");
501
1029
  const subjectId = decodeURIComponent(c.req.param("subjectId"));
502
1030
  const payload = UpdateWorkspaceMemberRequest.parse(await c.req.json());
503
- const existing = await listWorkspaceMembers(deps.db, workspaceId);
1031
+ const existing = await listWorkspacePeople(deps, workspaceId);
504
1032
  const current = existing.find((member) => member.subjectId === subjectId);
505
1033
  if (!current) {
506
1034
  throw new HTTPException(404, { message: "member not found" });
507
1035
  }
508
- await grantWorkspaceAccess(deps.db, {
509
- accountId: grant.accountId,
510
- workspaceId,
1036
+ assertWorkspaceMemberUpdateAllowed({
1037
+ members: existing,
511
1038
  subjectId,
512
- ...(current.subjectLabel ? { subjectLabel: current.subjectLabel } : {}),
513
- role: payload.role ?? current.role,
514
- permissions: payload.permissions,
1039
+ callerSubjectId: grant.subjectId,
1040
+ nextPermissions: payload.permissions,
515
1041
  });
516
- const members = await listWorkspaceMembers(deps.db, workspaceId);
1042
+ try {
1043
+ await upsertWorkspaceMemberAsWorkspaceManager(deps.db, {
1044
+ accountId: grant.accountId,
1045
+ workspaceId,
1046
+ actorSubjectId: grant.subjectId,
1047
+ targetSubjectId: subjectId,
1048
+ mode: "update",
1049
+ ...(current.subjectLabel ? { subjectLabel: current.subjectLabel } : {}),
1050
+ role: payload.role ?? current.role,
1051
+ permissions: payload.permissions,
1052
+ });
1053
+ } catch (error) {
1054
+ rethrowWorkspaceMemberCandidateError(error);
1055
+ }
1056
+ const members = await listWorkspacePeople(deps, workspaceId);
517
1057
  const member = members.find((candidate) => candidate.subjectId === subjectId);
518
1058
  if (!member) {
519
1059
  throw new HTTPException(500, { message: "failed to update member" });
@@ -528,7 +1068,11 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
528
1068
  const members = await listWorkspaceMembers(deps.db, workspaceId);
529
1069
  // Never remove yourself, and never remove the last administering member.
530
1070
  // The fenced removal command re-enforces both guards fail-closed.
531
- assertWorkspaceMemberRemovable({ members, subjectId, callerSubjectId: grant.subjectId });
1071
+ assertWorkspaceMemberRemovable({
1072
+ members,
1073
+ subjectId,
1074
+ callerSubjectId: grant.subjectId,
1075
+ });
532
1076
  await removeWorkspaceMember(deps.db, {
533
1077
  accountId: grant.accountId,
534
1078
  workspaceId,
@@ -539,6 +1083,69 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
539
1083
  });
540
1084
  }
541
1085
 
1086
+ function workspaceLimit(deps: ApiRouteDeps): number | null {
1087
+ if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
1088
+ return null;
1089
+ }
1090
+ return configuredStaticUsageLimits(deps.settings).maxWorkspacesPerAccount ?? null;
1091
+ }
1092
+
1093
+ async function listWorkspacePeople(
1094
+ deps: ApiRouteDeps,
1095
+ workspaceId: string,
1096
+ ): Promise<WorkspaceMemberValue[]> {
1097
+ const members = await listWorkspaceMembers(deps.db, workspaceId);
1098
+ const userIds = members.flatMap((member) =>
1099
+ member.subjectId.startsWith("user:") ? [member.subjectId.slice("user:".length)] : [],
1100
+ );
1101
+ const profiles = await getManagedUserProfilesByIds(deps.db, userIds);
1102
+ const profileBySubject = new Map(
1103
+ profiles.map((profile) => [`user:${profile.id}`, profile.name?.trim() || profile.email]),
1104
+ );
1105
+ return members.map((member) => ({
1106
+ ...member,
1107
+ subjectLabel: profileBySubject.get(member.subjectId) ?? member.subjectLabel,
1108
+ }));
1109
+ }
1110
+
1111
+ function rethrowWorkspaceMemberCandidateError(error: unknown): never {
1112
+ const managementCode =
1113
+ error && typeof error === "object" && "code" in error ? error.code : undefined;
1114
+ if (managementCode === "WORKSPACE_MEMBER_ALREADY_EXISTS") {
1115
+ throw new HTTPException(409, {
1116
+ message: "this person already has access to the workspace",
1117
+ });
1118
+ }
1119
+ if (managementCode === "WORKSPACE_MEMBER_NOT_FOUND") {
1120
+ throw new HTTPException(404, { message: "member not found" });
1121
+ }
1122
+ if (managementCode === "WORKSPACE_MEMBER_SELF_UPDATE") {
1123
+ throw new HTTPException(409, {
1124
+ message: "you cannot change your own workspace access",
1125
+ });
1126
+ }
1127
+ if (managementCode === "WORKSPACE_MEMBER_LAST_ADMIN") {
1128
+ throw new HTTPException(409, {
1129
+ message: "the workspace must keep at least one administrator",
1130
+ });
1131
+ }
1132
+ const sqlState = nestedPostgresSqlState(error);
1133
+ if (sqlState === "P0002") {
1134
+ throw new HTTPException(404, { message: "workspace not found" });
1135
+ }
1136
+ if (sqlState === "42501") {
1137
+ throw new HTTPException(403, {
1138
+ message: "workspace member management is not allowed",
1139
+ });
1140
+ }
1141
+ if (sqlState === "54000") {
1142
+ throw new HTTPException(409, {
1143
+ message: "this organization has too many members to show here",
1144
+ });
1145
+ }
1146
+ throw error;
1147
+ }
1148
+
542
1149
  // A persona override that is null or trims to empty collapses to null (use the
543
1150
  // deployment default). Otherwise the template is stored verbatim so the runtime
544
1151
  // can substitute the non-bypassable CORE at its {{core}} marker.
@@ -560,6 +1167,8 @@ function requireAccountPermission(
560
1167
  !grant ||
561
1168
  (!grant.permissions.includes(permission) && !grant.permissions.includes("account:admin"))
562
1169
  ) {
563
- throw new HTTPException(403, { message: `missing permission: ${permission}` });
1170
+ throw new HTTPException(403, {
1171
+ message: `missing permission: ${permission}`,
1172
+ });
564
1173
  }
565
1174
  }