@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
@@ -1,9 +1,39 @@
1
1
  import { type PersonalGitHubRepository as PersonalGitHubRepositoryContract, type PersonalGitHubRepositorySelectionInput } from "@opengeni/contracts/personal-github";
2
+ import { type GitHubRepositoryBranchesResponse as GitHubRepositoryBranchesResponseContract, type ListGitHubRepositoryBranchesQuery } from "@opengeni/contracts/github-repository-contracts";
2
3
  import type { ApiRouteDeps } from "@opengeni/core";
3
- import { getConnectionMetadata } from "@opengeni/db";
4
+ import { getConnectionMetadata, type PersonalGitHubRepositorySelectionState as DbPersonalGitHubRepositorySelectionState } from "@opengeni/db";
4
5
  import { HTTPException } from "hono/http-exception";
5
6
  export type PersonalGitHubRepositoryConnection = NonNullable<Awaited<ReturnType<typeof getConnectionMetadata>>>;
6
- export type PersonalGitHubRepositoryProviderErrorCode = "connection_changed" | "connection_inactive" | "invalid_provider_response" | "provider_denied" | "provider_rate_limited" | "provider_unavailable" | "repository_archived" | "repository_not_found" | "repository_read_denied" | "repository_write_denied";
7
+ type PersonalGitHubRepositoryProviderOperation = "repositories_list" | "repository_branches_list" | "repository_verify";
8
+ type ExpectedPersonalGitHubRepositoryAuthority = {
9
+ selectionGeneration: number;
10
+ repositoryId: string;
11
+ fullName: string;
12
+ };
13
+ type PersonalGitHubProviderJsonOptions = {
14
+ operation: PersonalGitHubRepositoryProviderOperation;
15
+ expectedRepositoryAuthority?: ExpectedPersonalGitHubRepositoryAuthority;
16
+ responseLabel?: string;
17
+ responseMaxBytes?: number;
18
+ };
19
+ export type PersonalGitHubRepositoryBranchServices = {
20
+ requireConnection: typeof requirePersonalGitHubRepositoryConnection;
21
+ getSelectionState: (deps: ApiRouteDeps, input: {
22
+ accountId: string;
23
+ originWorkspaceId: string;
24
+ subjectId: string;
25
+ connectionId: string;
26
+ }) => Promise<DbPersonalGitHubRepositorySelectionState | null>;
27
+ providerJson: (input: {
28
+ deps: ApiRouteDeps;
29
+ connection: PersonalGitHubRepositoryConnection;
30
+ subjectId: string;
31
+ url: URL;
32
+ expectedConnectionAuthorityGeneration: number;
33
+ options: PersonalGitHubProviderJsonOptions;
34
+ }) => Promise<unknown>;
35
+ };
36
+ export type PersonalGitHubRepositoryProviderErrorCode = "connection_changed" | "connection_inactive" | "invalid_provider_response" | "provider_denied" | "provider_rate_limited" | "provider_unavailable" | "repository_archived" | "repository_not_found" | "repository_not_selected" | "repository_read_denied" | "repository_selection_changed" | "repository_write_denied";
7
37
  export declare class PersonalGitHubRepositoryProviderError extends Error {
8
38
  readonly code: PersonalGitHubRepositoryProviderErrorCode;
9
39
  constructor(code: PersonalGitHubRepositoryProviderErrorCode);
@@ -26,6 +56,14 @@ export declare function listLivePersonalGitHubRepositories(deps: ApiRouteDeps, i
26
56
  repositories: PersonalGitHubRepositoryContract[];
27
57
  nextPage: number | null;
28
58
  }>;
59
+ export declare function listLivePersonalGitHubRepositoryBranches(deps: ApiRouteDeps, input: {
60
+ workspaceId: string;
61
+ accountId: string;
62
+ subjectId: string;
63
+ connectionId: string;
64
+ repositoryId: string;
65
+ query: ListGitHubRepositoryBranchesQuery;
66
+ }, services?: PersonalGitHubRepositoryBranchServices): Promise<GitHubRepositoryBranchesResponseContract>;
29
67
  /** Serial by design: one bounded provider request and one authority check at a time. */
30
68
  export declare function verifyLivePersonalGitHubRepositories(deps: ApiRouteDeps, input: {
31
69
  workspaceId: string;
@@ -43,3 +81,4 @@ export declare function parsePersonalGitHubProviderJson(value: string): unknown;
43
81
  export declare function personalGitHubRepositoryCanRead(permissions: PersonalGitHubRepositoryContract["permissions"]): boolean;
44
82
  export declare function personalGitHubRepositoryCanWrite(permissions: PersonalGitHubRepositoryContract["permissions"]): boolean;
45
83
  export declare function parsePersonalGitHubRepository(value: unknown): PersonalGitHubRepositoryContract;
84
+ export {};
@@ -82,8 +82,8 @@ export declare function scheduledTaskMcpSummary(task: ScheduledTask): {
82
82
  runMode: "existing_session" | "new_session_per_run" | "reusable_session";
83
83
  overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
84
84
  action: {
85
+ readonly sourceId?: never;
85
86
  kind: "agent_turn";
86
- sourceId?: never;
87
87
  sourceGeneration?: never;
88
88
  sourceLifecycleGeneration?: never;
89
89
  destination?: never;
@@ -205,8 +205,8 @@ export declare function boundScheduledTaskDetailMcp(task: ScheduledTask): {
205
205
  runMode: "existing_session" | "new_session_per_run" | "reusable_session";
206
206
  overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
207
207
  action: {
208
+ readonly sourceId?: never;
208
209
  kind: "agent_turn";
209
- sourceId?: never;
210
210
  sourceGeneration?: never;
211
211
  sourceLifecycleGeneration?: never;
212
212
  destination?: never;
@@ -389,8 +389,8 @@ export declare function boundScheduledTaskMcpPage(input: {
389
389
  runMode: "existing_session" | "new_session_per_run" | "reusable_session";
390
390
  overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
391
391
  action: {
392
+ readonly sourceId?: never;
392
393
  kind: "agent_turn";
393
- sourceId?: never;
394
394
  sourceGeneration?: never;
395
395
  sourceLifecycleGeneration?: never;
396
396
  destination?: never;
@@ -1,23 +1,7 @@
1
- import { type ConfiguredModel, type Settings } from "@opengeni/config";
2
- import { ClientModel, type WorkspaceModelCatalogResponse as WorkspaceModelCatalogResponseType, type WorkspaceModelPolicyContract } from "@opengeni/contracts";
3
- export type ModelAvailabilityObservation = {
4
- status: "available" | "degraded" | "unavailable";
5
- reason: "not_entitled" | "provider_unhealthy" | null;
6
- checkedAt: string;
7
- };
8
- export type ModelCredentialReadinessObservation = {
9
- status: "ready";
10
- checkedAt: string;
11
- } | {
12
- status: "not_ready";
13
- reason: "prerequisites_missing" | "needs_reauth";
14
- checkedAt: string;
15
- } | {
16
- status: "error";
17
- reason: "resolver_error";
18
- checkedAt: string;
19
- };
20
- export declare const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS: number;
1
+ import { type ConfiguredModel } from "@opengeni/config";
2
+ import { ClientModel, type WorkspaceModelCatalogResponse as WorkspaceModelCatalogResponseType } from "@opengeni/contracts";
3
+ import { type WorkspaceModelSelectionInput } from "@opengeni/core";
4
+ export { MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS, type ModelAvailabilityObservation, type ModelCredentialReadinessObservation, } from "@opengeni/core";
21
5
  /** Static, client-safe definition projection. No provider secret is reachable. */
22
6
  export declare function projectClientModel(model: ConfiguredModel): ClientModel;
23
7
  /**
@@ -28,14 +12,4 @@ export declare function projectClientModel(model: ConfiguredModel): ClientModel;
28
12
  * successful typed resolver observation; credential health and provider health
29
13
  * are separate inputs and neither is fabricated.
30
14
  */
31
- export declare function buildWorkspaceModelCatalog(input: {
32
- settings: Settings;
33
- policy: WorkspaceModelPolicyContract | null;
34
- codexSubscriptionActive: boolean;
35
- xaiSubscriptionActive?: boolean;
36
- workspaceGatewayConnectionActive?: boolean;
37
- credentialReadinessObservations?: Readonly<Record<string, ModelCredentialReadinessObservation>> | undefined;
38
- observations?: Readonly<Record<string, ModelAvailabilityObservation>> | undefined;
39
- now?: Date | undefined;
40
- credentialReadinessMaxAgeMs?: number | undefined;
41
- }): WorkspaceModelCatalogResponseType;
15
+ export declare function buildWorkspaceModelCatalog(input: WorkspaceModelSelectionInput): WorkspaceModelCatalogResponseType;
@@ -6,5 +6,12 @@ export declare function codexModelsForPicker(liveSlugs: readonly string[]): Arra
6
6
  api: "responses";
7
7
  }>;
8
8
  import { type ApiRouteDeps } from "@opengeni/core";
9
- import type { Hono } from "hono";
9
+ import type { Context, Hono } from "hono";
10
+ type ManagedCookieHuman = {
11
+ subjectId: string;
12
+ browserSessionHash: string;
13
+ };
14
+ export declare function requireOrganizationCodexHuman(c: Context, deps: ApiRouteDeps, organizationId: string): Promise<ManagedCookieHuman>;
15
+ export declare function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void;
10
16
  export declare function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void;
17
+ export {};
@@ -1,3 +1,5 @@
1
+ import { type AccessGrant } from "@opengeni/contracts";
1
2
  import type { Hono } from "hono";
2
3
  import type { ApiRouteDeps } from "@opengeni/core";
3
4
  export declare function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void;
5
+ export declare function requirePublicGitHubRepositoryVerificationPermission(grant: Pick<AccessGrant, "permissions">): void;
@@ -0,0 +1,3 @@
1
+ import { type ApiRouteDeps } from "@opengeni/core";
2
+ import type { Hono } from "hono";
3
+ export declare function registerOrganizationModelProviderRoutes(app: Hono, deps: ApiRouteDeps): void;
@@ -0,0 +1,6 @@
1
+ import type { ApiRouteDeps } from "@opengeni/core";
2
+ export declare function deleteWorkspaceForRequest(deps: ApiRouteDeps, input: {
3
+ accountId: string;
4
+ workspaceId: string;
5
+ organizationAdministratorSubjectId?: string;
6
+ }): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/api-router",
3
- "version": "2.6.4",
3
+ "version": "2.8.1-canary.0",
4
4
  "description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -43,23 +43,23 @@
43
43
  "@hono/zod-validator": "^0.7.6",
44
44
  "@llamaindex/liteparse": "2.14.2",
45
45
  "@modelcontextprotocol/sdk": "^1.29.0",
46
- "@opengeni/agent-proto": "^0.5.1",
47
- "@opengeni/artifact-tool": "^0.3.14",
48
- "@opengeni/capabilities": "^0.3.1",
49
- "@opengeni/codemode": "^0.4.22",
50
- "@opengeni/codex": "^0.2.20",
51
- "@opengeni/config": "^0.22.5",
52
- "@opengeni/contracts": "^2.9.2",
53
- "@opengeni/core": "^2.6.4",
54
- "@opengeni/db": "^3.7.4",
55
- "@opengeni/documents": "^0.8.14",
56
- "@opengeni/events": "^0.4.12",
57
- "@opengeni/github": "^0.6.7",
58
- "@opengeni/network": "^0.2.3",
59
- "@opengeni/observability": "^0.8.14",
60
- "@opengeni/runtime": "^2.0.1",
61
- "@opengeni/storage": "^0.2.115",
62
- "@opengeni/xai-subscription": "^0.1.2",
46
+ "@opengeni/agent-proto": "^0.5.1-canary.8",
47
+ "@opengeni/artifact-tool": "^0.3.17-canary.0",
48
+ "@opengeni/capabilities": "^0.3.1-canary.7",
49
+ "@opengeni/codemode": "^0.4.25-canary.0",
50
+ "@opengeni/codex": "^0.2.20-canary.2",
51
+ "@opengeni/config": "^0.23.2-canary.0",
52
+ "@opengeni/contracts": "^2.11.1-canary.0",
53
+ "@opengeni/core": "^2.7.2-canary.0",
54
+ "@opengeni/db": "^3.8.2-canary.0",
55
+ "@opengeni/documents": "^0.8.17-canary.0",
56
+ "@opengeni/events": "^0.4.15-canary.0",
57
+ "@opengeni/github": "^0.7.1-canary.0",
58
+ "@opengeni/network": "^0.2.3-canary.7",
59
+ "@opengeni/observability": "^0.8.17-canary.0",
60
+ "@opengeni/runtime": "^2.1.2-canary.0",
61
+ "@opengeni/storage": "^0.2.118-canary.0",
62
+ "@opengeni/xai-subscription": "^0.1.2-canary.7",
63
63
  "@temporalio/client": "^1.17.0",
64
64
  "better-auth": "^1.6.14",
65
65
  "hono": "^4.12.18",
package/src/app.ts CHANGED
@@ -66,6 +66,8 @@ import {
66
66
  requireLiveAgentAttemptAuthorization,
67
67
  requirePermission,
68
68
  releaseManagedAuthRequestActorLease,
69
+ resolveCatalogSettings,
70
+ resolveWorkspaceCatalogSettings,
69
71
  validateManagedAuthRequestActorLease,
70
72
  requireSessionAuthorization,
71
73
  SessionAuthorizationDeniedError,
@@ -116,6 +118,7 @@ import { allowedCorsOrigin } from "./http/cors";
116
118
  import { registerCapabilityRoutes } from "./routes/capabilities";
117
119
  import { registerCatalogAssetRoutes } from "./routes/catalog-assets";
118
120
  import { registerCodexRoutes } from "./routes/codex";
121
+ import { registerOrganizationModelProviderRoutes } from "./routes/organization-model-providers";
119
122
  import { registerSuperGrokRoutes } from "./routes/supergrok";
120
123
  import { registerConnectionRoutes } from "./routes/connections";
121
124
  import { registerDocumentRoutes } from "./routes/documents";
@@ -222,6 +225,22 @@ export function createApp(deps: AppDependencies): Hono {
222
225
  return createAppComposition(deps).app;
223
226
  }
224
227
 
228
+ export async function resolveWorkspaceMcpRouteDeps(
229
+ routeDeps: ApiRouteDeps,
230
+ grant: AccessGrant,
231
+ ): Promise<ApiRouteDeps> {
232
+ const catalogSourceSettings = routeDeps.catalogSourceSettings ?? routeDeps.settings;
233
+ const resolvedCatalog = await resolveWorkspaceCatalogSettings(
234
+ routeDeps.db,
235
+ catalogSourceSettings,
236
+ {
237
+ accountId: grant.accountId,
238
+ workspaceId: grant.workspaceId,
239
+ },
240
+ );
241
+ return { ...routeDeps, catalogSourceSettings, settings: resolvedCatalog.settings };
242
+ }
243
+
225
244
  export function createAppComposition(deps: AppDependencies): {
226
245
  app: Hono;
227
246
  routeDeps: ApiRouteDeps;
@@ -358,6 +377,7 @@ export function createAppComposition(deps: AppDependencies): {
358
377
  : deps.transcriptionSegmenter;
359
378
  const routeDeps: ApiRouteDeps = {
360
379
  ...deps,
380
+ resolveCatalogSettings: () => resolveCatalogSettings(deps.db, deps.settings),
361
381
  observability,
362
382
  githubStateSecret:
363
383
  deps.githubStateSecret ?? deps.settings.githubAppManifestStateSecret ?? crypto.randomUUID(),
@@ -403,6 +423,21 @@ export function createAppComposition(deps: AppDependencies): {
403
423
  "X-OpenGeni-Actor-Epoch",
404
424
  "X-OpenGeni-Actor-State",
405
425
  "X-OpenGeni-Correlation-Id",
426
+ "X-OpenGeni-Covered-First",
427
+ "X-OpenGeni-Covered-Last",
428
+ "X-OpenGeni-Event-Direction",
429
+ "X-OpenGeni-Event-Mode",
430
+ "X-OpenGeni-Event-Result",
431
+ "X-OpenGeni-Event-Result-Mode",
432
+ "X-OpenGeni-Forensic-Exact",
433
+ "X-OpenGeni-Has-More",
434
+ "X-OpenGeni-Next-After",
435
+ "X-OpenGeni-Next-Before",
436
+ "X-OpenGeni-Page-Bytes",
437
+ "X-OpenGeni-Page-Max-Bytes",
438
+ "X-OpenGeni-Page-Truncated",
439
+ "X-OpenGeni-Payload-Mode",
440
+ "X-OpenGeni-Truncated-By",
406
441
  ],
407
442
  };
408
443
  const publicApiCors = cors({
@@ -766,10 +801,12 @@ export function createAppComposition(deps: AppDependencies): {
766
801
 
767
802
  app.get("/v1/config/client", async (c) => {
768
803
  c.header("cache-control", "no-store");
769
- const codexCatalogSettings = deps.settings.codexSubscriptionEnabled
770
- ? withCodexCatalogProvider(deps.settings)
771
- : deps.settings;
772
- const catalogSettings = deps.settings.supergrokSubscriptionEnabled
804
+ const resolvedCatalog = await resolveCatalogSettings(deps.db, deps.settings);
805
+ const baseCatalogSettings = resolvedCatalog.settings;
806
+ const codexCatalogSettings = baseCatalogSettings.codexSubscriptionEnabled
807
+ ? withCodexCatalogProvider(baseCatalogSettings)
808
+ : baseCatalogSettings;
809
+ const catalogSettings = baseCatalogSettings.supergrokSubscriptionEnabled
773
810
  ? withXaiSubscriptionCatalogProvider(codexCatalogSettings)
774
811
  : codexCatalogSettings;
775
812
  return c.json(
@@ -889,7 +926,8 @@ export function createAppComposition(deps: AppDependencies): {
889
926
  const transport = new WebStandardStreamableHTTPServerTransport({
890
927
  enableJsonResponse: true,
891
928
  });
892
- const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
929
+ const mcpDeps = await resolveWorkspaceMcpRouteDeps(routeDeps, grant);
930
+ const mcp = buildOpenGeniMcpServer(mcpDeps, grant, {
893
931
  requestOrigin: new URL(c.req.url).origin,
894
932
  workspaceMemoryEnabled,
895
933
  workspaceMemoryPromptMode,
@@ -996,6 +1034,7 @@ export function createAppComposition(deps: AppDependencies): {
996
1034
  registerSessionRoutes(app, routeDeps);
997
1035
  registerScheduledTaskRoutes(app, routeDeps);
998
1036
  registerCodexRoutes(app, routeDeps);
1037
+ registerOrganizationModelProviderRoutes(app, routeDeps);
999
1038
  registerSuperGrokRoutes(app, routeDeps);
1000
1039
  registerTranscriptionRoutes(app, routeDeps);
1001
1040
  registerEditableArtifactRoutes(app, routeDeps);
@@ -1093,6 +1132,10 @@ const publicWorkspaceBrowserRoutePatterns = [
1093
1132
  ] as const;
1094
1133
 
1095
1134
  export function workspaceActorContextExempt(method: string, pathname: string): boolean {
1135
+ // The account-scoped provisioning route shares the workspace collection prefix.
1136
+ // Hono's trailing wildcard also matches it, but "external" is not a workspace UUID;
1137
+ // the route performs its own organization API-key authorization.
1138
+ if (method === "PUT" && pathname === "/v1/workspaces/external") return true;
1096
1139
  if (/^\/v1\/workspaces\/[^/]+\/mcp$/.test(pathname)) return true;
1097
1140
  if (
1098
1141
  method === "GET" &&
@@ -1379,11 +1422,14 @@ type ReadinessCheckResult = { ok: boolean; error?: string };
1379
1422
  function readinessChecks(deps: AppDependencies): ReadinessChecks {
1380
1423
  const configuredNatsCheck = deps.readinessChecks?.nats;
1381
1424
  return {
1382
- db:
1383
- deps.readinessChecks?.db ??
1384
- (async () => {
1425
+ db: async () => {
1426
+ if (deps.readinessChecks?.db) {
1427
+ await deps.readinessChecks.db();
1428
+ } else {
1385
1429
  await deps.db.execute(dbSql`select 1`);
1386
- }),
1430
+ }
1431
+ await resolveCatalogSettings(deps.db, deps.settings);
1432
+ },
1387
1433
  nats: async () => {
1388
1434
  requireSessionEventDurableFanoutCapability(deps.bus);
1389
1435
  if (configuredNatsCheck) {
@@ -107,9 +107,9 @@ export function renderOrganizationUserSetupEmail(input: OrganizationUserSetupEma
107
107
  return {
108
108
  from: input.senderEmail,
109
109
  to: input.recipientEmail,
110
- subject: `Join ${input.organizationName} on OpenGeni`,
111
- text: `${greeting}\n\nYou have been invited to ${input.organizationName} as ${role}.\n\n${workspaceSummary}\n\nThis invitation grants only the organization role and shared workspace access listed above. It never shares anyone's Personal workspace.\n\nSet up your account: ${input.setupUrl}\n\nIf you already have an OpenGeni account, sign in and accept the invitation instead.`,
112
- html: `<p>${escapeHtml(greeting)}</p><p>You have been invited to <strong>${escapeHtml(input.organizationName)}</strong> as ${escapeHtml(role)}.</p>${workspaceHtml}<p>This invitation grants only the organization role and shared workspace access listed above. It never shares anyone's Personal workspace.</p><p><a href="${escapeHtml(input.setupUrl)}">Set up your account</a></p><p>If you already have an OpenGeni account, sign in and accept the invitation instead.</p>`,
110
+ subject: `You're invited to join ${input.organizationName} on OpenGeni`,
111
+ text: `${greeting}\n\nYou've been invited to join ${input.organizationName} on OpenGeni as ${role}.\n\n${workspaceSummary}\n\nThis invitation grants only the organization role and shared workspace access listed above. It never shares anyone's Personal workspace.\n\nAccept invitation to ${input.organizationName}: ${input.setupUrl}\n\nYou'll sign in or create an account before joining. Already use OpenGeni? Open this invitation and choose Sign in as ${input.recipientEmail}. OpenGeni will show the invitation immediately after you sign in.`,
112
+ html: `<p>${escapeHtml(greeting)}</p><p>You've been invited to join <strong>${escapeHtml(input.organizationName)}</strong> on OpenGeni as ${escapeHtml(role)}.</p>${workspaceHtml}<p>This invitation grants only the organization role and shared workspace access listed above. It never shares anyone's Personal workspace.</p><p><a href="${escapeHtml(input.setupUrl)}">Accept invitation to ${escapeHtml(input.organizationName)}</a></p><p>You'll sign in or create an account before joining. Already use OpenGeni? Open this invitation and choose <strong>Sign in as ${escapeHtml(input.recipientEmail)}</strong>. OpenGeni will show the invitation immediately after you sign in.</p>`,
113
113
  };
114
114
  }
115
115
 
@@ -1,15 +1,61 @@
1
1
  import type {
2
+ GitHubAppRepositoryBranchPage,
2
3
  GitHubBindingStatus,
3
4
  GitHubInstallationBinding,
4
5
  GitHubRepository,
5
6
  } from "@opengeni/contracts";
6
7
  import {
8
+ GitHubRepositoryBranchesResponse as GitHubRepositoryBranchesResponseSchema,
9
+ type GitHubRepositoryBranchesResponse,
10
+ type ListGitHubRepositoryBranchesQuery,
11
+ } from "@opengeni/contracts/github-repository-contracts";
12
+ import {
13
+ areGitHubRepositoriesAllowedForWorkspace,
7
14
  hasAuditableGitHubInstallationAuthority,
8
15
  listGitHubInstallationAccessForWorkspace,
9
16
  } from "@opengeni/db";
10
- import { listGitHubAppInstallationSummaries, listGitHubAppRepositories } from "@opengeni/github";
17
+ import {
18
+ GitHubAppApiError,
19
+ listGitHubAppInstallationSummaries,
20
+ listGitHubAppRepositories,
21
+ listGitHubAppRepositoryBranches,
22
+ } from "@opengeni/github";
11
23
  import type { ApiRouteDeps } from "@opengeni/core";
12
24
 
25
+ export type GitHubRepositoryBranchAuthorityErrorCode = "changed" | "not_authorized";
26
+
27
+ export class GitHubRepositoryBranchAuthorityError extends Error {
28
+ constructor(readonly code: GitHubRepositoryBranchAuthorityErrorCode) {
29
+ super(code);
30
+ this.name = "GitHubRepositoryBranchAuthorityError";
31
+ }
32
+ }
33
+
34
+ export type WorkspaceGitHubRepositoryBranchServices = {
35
+ listInstallationAccess: typeof listGitHubInstallationAccessForWorkspace;
36
+ areRepositoriesAllowed: typeof areGitHubRepositoriesAllowedForWorkspace;
37
+ listProviderBranches: (
38
+ deps: ApiRouteDeps,
39
+ input: {
40
+ installationId: number;
41
+ repositoryId: number;
42
+ page: number;
43
+ limit: number;
44
+ },
45
+ ) => Promise<GitHubAppRepositoryBranchPage | null>;
46
+ };
47
+
48
+ const workspaceGitHubRepositoryBranchServices: WorkspaceGitHubRepositoryBranchServices = {
49
+ listInstallationAccess: listGitHubInstallationAccessForWorkspace,
50
+ areRepositoriesAllowed: areGitHubRepositoriesAllowedForWorkspace,
51
+ listProviderBranches: async (deps, input) =>
52
+ deps.githubAppApi?.listRepositoryBranches
53
+ ? await deps.githubAppApi.listRepositoryBranches(input)
54
+ : deps.githubAppApi
55
+ ? null
56
+ : await listGitHubAppRepositoryBranches(deps.settings, input),
57
+ };
58
+
13
59
  export async function listWorkspaceGitHubInstallationBindings(
14
60
  deps: ApiRouteDeps,
15
61
  workspaceId: string,
@@ -153,3 +199,73 @@ export async function listWorkspaceGitHubRepositories(
153
199
  return installation.repositoryIds.includes(repository.id);
154
200
  });
155
201
  }
202
+
203
+ export async function listWorkspaceGitHubRepositoryBranches(
204
+ deps: ApiRouteDeps,
205
+ input: {
206
+ accountId: string;
207
+ workspaceId: string;
208
+ installationId: number;
209
+ repositoryId: number;
210
+ query: ListGitHubRepositoryBranchesQuery;
211
+ },
212
+ services: WorkspaceGitHubRepositoryBranchServices = workspaceGitHubRepositoryBranchServices,
213
+ ): Promise<GitHubRepositoryBranchesResponse> {
214
+ const installations = await services.listInstallationAccess(deps.db, input.workspaceId);
215
+ const installation = installations.find(
216
+ (candidate) => candidate.installationId === input.installationId,
217
+ );
218
+ if (
219
+ !installation ||
220
+ installation.accountId !== input.accountId ||
221
+ !hasAuditableGitHubInstallationAuthority(installation) ||
222
+ !installation.repositoryIds.includes(input.repositoryId)
223
+ ) {
224
+ throw new GitHubRepositoryBranchAuthorityError("not_authorized");
225
+ }
226
+ if (
227
+ !(await services.areRepositoriesAllowed(deps.db, input.workspaceId, input.installationId, [
228
+ input.repositoryId,
229
+ ]))
230
+ ) {
231
+ throw new GitHubRepositoryBranchAuthorityError("changed");
232
+ }
233
+ const page = await services.listProviderBranches(deps, {
234
+ installationId: input.installationId,
235
+ repositoryId: input.repositoryId,
236
+ page: input.query.cursor,
237
+ limit: input.query.limit,
238
+ });
239
+ if (!page) {
240
+ throw new GitHubAppApiError(
241
+ "The configured GitHub provider cannot list exact repository branches",
242
+ );
243
+ }
244
+ if (
245
+ page.installationId !== input.installationId ||
246
+ page.repositoryId !== input.repositoryId ||
247
+ page.branches.length > input.query.limit ||
248
+ (page.nextPage !== null && page.nextPage !== input.query.cursor + 1) ||
249
+ (page.branches.length < input.query.limit && page.nextPage !== null)
250
+ ) {
251
+ throw new GitHubAppApiError("GitHub returned an invalid repository branches page");
252
+ }
253
+ const response = GitHubRepositoryBranchesResponseSchema.safeParse({
254
+ branches: page.branches.map((name) => ({
255
+ name,
256
+ isDefault: name === page.defaultBranch,
257
+ })),
258
+ nextCursor: page.nextPage,
259
+ });
260
+ if (!response.success) {
261
+ throw new GitHubAppApiError("GitHub returned an invalid repository branches page");
262
+ }
263
+ if (
264
+ !(await services.areRepositoriesAllowed(deps.db, input.workspaceId, input.installationId, [
265
+ input.repositoryId,
266
+ ]))
267
+ ) {
268
+ throw new GitHubRepositoryBranchAuthorityError("changed");
269
+ }
270
+ return response.data;
271
+ }
package/src/http/sse.ts CHANGED
@@ -10,12 +10,11 @@ import {
10
10
  type Database,
11
11
  } from "@opengeni/db";
12
12
  import {
13
- coalesceSessionEventDeltas,
13
+ coalesceSessionEventDeltasWithCoverage,
14
14
  formatSessionEventSse,
15
15
  formatWorkspaceControlEventSse,
16
16
  requireSessionEventDurableFanoutCapability,
17
17
  SESSION_EVENT_SSE_FRAME_MAX_BYTES,
18
- sessionEventResumeSequence,
19
18
  type EventBus,
20
19
  } from "@opengeni/events";
21
20
  import type { Observability } from "@opengeni/observability";
@@ -295,8 +294,14 @@ export async function sseSessionStream(
295
294
  limit: SESSION_REPLAY_PAGE_SIZE,
296
295
  });
297
296
  await options.reauthorize?.();
297
+ const compactProjection = coalesceSessionEventDeltasWithCoverage(events);
298
298
  return finiteSseBatchResponse(
299
- coalesceSessionEventDeltas(events).map(formatSessionEventSse),
299
+ compactProjection.events.map((event) =>
300
+ formatSessionEventSse(
301
+ event,
302
+ compactProjection.coveredThroughBySequence.get(event.sequence) ?? event.sequence,
303
+ ),
304
+ ),
300
305
  options,
301
306
  );
302
307
  }
@@ -378,9 +383,12 @@ export async function sseSessionStream(
378
383
  // adjacent text deltas into bounded frames carrying `coalescedUntil`, so
379
384
  // a long answer cannot create thousands of React renders and starve
380
385
  // command acknowledgements behind its own token stream.
381
- for (const projected of coalesceSessionEventDeltas(eligible)) {
382
- await writeFrame(formatSessionEventSse(projected));
383
- lastSent = sessionEventResumeSequence(projected);
386
+ const compactProjection = coalesceSessionEventDeltasWithCoverage(eligible);
387
+ for (const projected of compactProjection.events) {
388
+ const coveredThrough =
389
+ compactProjection.coveredThroughBySequence.get(projected.sequence) ?? projected.sequence;
390
+ await writeFrame(formatSessionEventSse(projected, coveredThrough));
391
+ lastSent = coveredThrough;
384
392
  }
385
393
  if (lastSent <= previousLastSent) {
386
394
  throw new Error(`Session event replay made no progress after sequence ${lastSent}`);
@@ -432,7 +440,7 @@ export async function sseSessionStream(
432
440
  drainReconnectReconciliation();
433
441
  };
434
442
  const send = async (event: SessionEvent) => {
435
- const targetSequence = sessionEventResumeSequence(event);
443
+ const targetSequence = event.sequence;
436
444
  if (targetSequence <= lastSent) return;
437
445
  await reconcileDurableThrough(targetSequence);
438
446
  };
package/src/index.ts CHANGED
@@ -28,7 +28,10 @@ import {
28
28
  } from "@opengeni/observability";
29
29
  import { createObjectStorage } from "@opengeni/storage";
30
30
  import { isArtifactRuntimeConfigured } from "@opengeni/artifact-tool/runtime/development";
31
- import { SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID } from "@opengeni/core";
31
+ import {
32
+ resolveCatalogSettings,
33
+ SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID,
34
+ } from "@opengeni/core";
32
35
  import {
33
36
  Connection,
34
37
  Client as TemporalClient,
@@ -357,6 +360,15 @@ export async function startApi(
357
360
  () => assertRuntimeDatabasePosture(dbClient.db, databasePosture),
358
361
  { ...retryOptions, onRetry },
359
362
  );
363
+ const resolvedCatalog = await retryStartupDependency(
364
+ "model catalog",
365
+ () => resolveCatalogSettings(dbClient.db, settings),
366
+ { ...retryOptions, onRetry },
367
+ );
368
+ observability.info("OpenGeni model catalog resolved", {
369
+ catalogSource: resolvedCatalog.source,
370
+ catalogVersion: resolvedCatalog.version,
371
+ });
360
372
  bus = await retryStartupDependency(
361
373
  "NATS",
362
374
  () =>