@opengeni/api-router 1.0.2 → 1.1.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.
@@ -43,7 +43,7 @@ export declare function slackBotFileContentResult(result: Awaited<ReturnType<Ope
43
43
  mimeType?: never;
44
44
  data?: never;
45
45
  } | {
46
- readonly text?: never;
46
+ text?: never;
47
47
  type: "image";
48
48
  mimeType: "image/jpeg" | "image/png" | "image/webp";
49
49
  data: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/api-router",
3
- "version": "1.0.2",
3
+ "version": "1.1.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,25 +43,25 @@
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": "1.0.2",
47
- "@opengeni/artifact-tool": "1.0.2",
48
- "@opengeni/capabilities": "1.0.2",
49
- "@opengeni/codemode": "1.0.2",
50
- "@opengeni/codex": "1.0.2",
51
- "@opengeni/config": "1.0.2",
52
- "@opengeni/contracts": "1.0.2",
53
- "@opengeni/core": "1.0.2",
54
- "@opengeni/db": "1.0.2",
55
- "@opengeni/documents": "1.0.2",
56
- "@opengeni/events": "1.0.2",
57
- "@opengeni/github": "1.0.2",
58
- "@opengeni/interaction": "1.0.2",
59
- "@opengeni/network": "1.0.2",
60
- "@opengeni/observability": "1.0.2",
61
- "@opengeni/runtime": "1.0.2",
62
- "@opengeni/storage": "1.0.2",
63
- "@opengeni/tool-gateway": "1.0.2",
64
- "@opengeni/xai-subscription": "1.0.2",
46
+ "@opengeni/agent-proto": "1.1.0",
47
+ "@opengeni/artifact-tool": "1.1.0",
48
+ "@opengeni/capabilities": "1.1.0",
49
+ "@opengeni/codemode": "1.1.0",
50
+ "@opengeni/codex": "1.1.0",
51
+ "@opengeni/config": "1.1.0",
52
+ "@opengeni/contracts": "1.1.0",
53
+ "@opengeni/core": "1.1.0",
54
+ "@opengeni/db": "1.1.0",
55
+ "@opengeni/documents": "1.1.0",
56
+ "@opengeni/events": "1.1.0",
57
+ "@opengeni/github": "1.1.0",
58
+ "@opengeni/interaction": "1.1.0",
59
+ "@opengeni/network": "1.1.0",
60
+ "@opengeni/observability": "1.1.0",
61
+ "@opengeni/runtime": "1.1.0",
62
+ "@opengeni/storage": "1.1.0",
63
+ "@opengeni/tool-gateway": "1.1.0",
64
+ "@opengeni/xai-subscription": "1.1.0",
65
65
  "@temporalio/client": "^1.17.0",
66
66
  "better-auth": "^1.6.14",
67
67
  "hono": "^4.12.18",
package/src/app.ts CHANGED
@@ -1418,7 +1418,13 @@ export function createAppComposition(deps: AppDependencies): {
1418
1418
  await next();
1419
1419
  return;
1420
1420
  }
1421
- const grant = await requireAccessGrant(c, routeDeps, workspaceId);
1421
+ // Match Hono's decoded routing path before first-use membership and RLS.
1422
+ const permission =
1423
+ c.req.method === "PUT" &&
1424
+ /^\/v1\/workspaces\/[^/]+\/artifact-catalog\/[^/]+\/[^/]+\/pin$/.test(c.req.path)
1425
+ ? "artifacts:publish"
1426
+ : undefined;
1427
+ const grant = await requireAccessGrant(c, routeDeps, workspaceId, permission);
1422
1428
  await withAccessGrantSessionRlsContext(routeDeps, grant, next);
1423
1429
  });
1424
1430
 
@@ -2480,7 +2486,7 @@ const routeLabelPatterns: Array<{
2480
2486
  label: string | ((match: RegExpMatchArray) => string);
2481
2487
  }> = [
2482
2488
  {
2483
- pattern: /^\/\.well-known\/oauth-authorization-server$/,
2489
+ pattern: /^\/\.well-known\/oauth-authorization-server(?:\/.*)?$/,
2484
2490
  label: "/.well-known/oauth-authorization-server",
2485
2491
  },
2486
2492
  {
@@ -13,6 +13,7 @@ const Cursor = z
13
13
  key: z.string().max(4096),
14
14
  kind: ArtifactCatalogKind,
15
15
  id: z.string().min(1).max(128),
16
+ pinned: z.boolean().optional(),
16
17
  })
17
18
  .strict(),
18
19
  })
package/src/index.ts CHANGED
@@ -342,8 +342,6 @@ export async function startApi(
342
342
  rlsStrategy: settings.rlsStrategy,
343
343
  expectedRole: settings.runtimeDatabaseRole,
344
344
  targetSchema: settings.dbSchema.trim() || "public",
345
- organizationTenancyCanonicalActivationEnabled:
346
- settings.organizationTenancyCanonicalActivationEnabled,
347
345
  } as const;
348
346
  // The PRIVILEGED control-plane NATS login (M-AUTH): when the server runs with
349
347
  // auth_callout, api/worker authenticate as a static account user permitted to
@@ -6106,6 +6106,17 @@ export const ACTION_CATALOG: readonly ActionCatalogEntry[] = [
6106
6106
  "PrReviewAppRegistration"
6107
6107
  ]
6108
6108
  },
6109
+ {
6110
+ "id": "updateArtifactPin",
6111
+ "method": "PUT",
6112
+ "path": "/v1/workspaces/:workspaceId/artifact-catalog/:kind/:artifactId/pin",
6113
+ "request": [
6114
+ "ArtifactCatalogKind"
6115
+ ],
6116
+ "response": [
6117
+ "ArtifactPinResponse"
6118
+ ]
6119
+ },
6109
6120
  {
6110
6121
  "id": "updateBrowserIdentity",
6111
6122
  "method": "PATCH",
package/src/mcp/server.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  setSessionChannel,
10
10
  } from "@opengeni/db";
11
11
  import { createHash, randomUUID } from "node:crypto";
12
+ import { assertGoalResumeAllowed } from "@opengeni/core";
12
13
  import { mintEnrollToken } from "../sandbox/enrollment";
13
14
  import { capabilityAccountReadiness } from "./capability-account-readiness";
14
15
  import {
@@ -3076,6 +3077,8 @@ function registerGoalTools(
3076
3077
  sessionId,
3077
3078
  {
3078
3079
  status: "active",
3080
+ beforeResume: (tx, session, causalTurn) =>
3081
+ assertGoalResumeAllowed({ ...deps, db: tx }, session, causalTurn),
3079
3082
  event: { type: "goal.resumed", actor: "agent" },
3080
3083
  },
3081
3084
  );
package/src/mcp-oauth.ts CHANGED
@@ -79,7 +79,10 @@ export type McpOAuthRouteAccess = {
79
79
  };
80
80
 
81
81
  export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
82
- app.get("/.well-known/oauth-authorization-server", (c) => {
82
+ // RFC 8414 path-aware discovery: MCP clients (Claude among them) ask for the
83
+ // metadata at /.well-known/oauth-authorization-server/<resource path> first
84
+ // and give up on anything but JSON. Serve the same document at both.
85
+ const authorizationServerMetadata = (c: Context) => {
83
86
  requireMcpOAuthEnabled(deps);
84
87
  const issuer = mcpOAuthIssuer(deps);
85
88
  c.header("cache-control", "public, max-age=300");
@@ -97,7 +100,9 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
97
100
  authorization_response_iss_parameter_supported: true,
98
101
  }),
99
102
  );
100
- });
103
+ };
104
+ app.get("/.well-known/oauth-authorization-server", authorizationServerMetadata);
105
+ app.get("/.well-known/oauth-authorization-server/*", authorizationServerMetadata);
101
106
 
102
107
  app.get("/.well-known/oauth-protected-resource/*", (c) => {
103
108
  requireMcpOAuthEnabled(deps);
@@ -563,6 +568,7 @@ export function mcpOAuthConsentToolIdentities(catalog: ToolGatewayCatalog): Tool
563
568
  export function isMcpOAuthPublicProtocolPath(pathname: string): boolean {
564
569
  return (
565
570
  pathname === "/.well-known/oauth-authorization-server" ||
571
+ pathname.startsWith("/.well-known/oauth-authorization-server/") ||
566
572
  pathname.startsWith("/.well-known/oauth-protected-resource/") ||
567
573
  pathname === "/oauth/register" ||
568
574
  pathname === "/oauth/authorize" ||
@@ -4,6 +4,8 @@ import {
4
4
  ArtifactCatalogKind,
5
5
  ArtifactCatalogListQuery,
6
6
  ArtifactCatalogListResponse,
7
+ ArtifactPinResponse,
8
+ UpdateArtifactPinRequest,
7
9
  retainedArtifactReferenceFromFile,
8
10
  retainedGeneratedImageReferenceFromFile,
9
11
  type AccessGrant,
@@ -26,6 +28,8 @@ import {
26
28
  getGeneratedImageArtifact,
27
29
  getSessionAuthorityProjection,
28
30
  listArtifactCatalogCandidates,
31
+ nestedPostgresSqlState,
32
+ updateArtifactPin,
29
33
  withSessionRlsActorContext,
30
34
  type ArtifactCatalogCandidate,
31
35
  type ArtifactCatalogPosition,
@@ -33,6 +37,7 @@ import {
33
37
  } from "@opengeni/db";
34
38
  import type { Hono } from "hono";
35
39
  import { HTTPException } from "hono/http-exception";
40
+ import { z } from "zod";
36
41
  import { artifactCatalogCursorCodec } from "../artifact-catalog-cursor";
37
42
  import {
38
43
  editableArtifactActorForGrant,
@@ -51,9 +56,95 @@ const position = (row: ArtifactCatalogCandidate): ArtifactCatalogPosition => ({
51
56
  key: row.sort_key,
52
57
  kind: row.kind,
53
58
  id: row.id,
59
+ pinned: row.pinned ?? false,
54
60
  });
55
61
 
56
62
  export function registerArtifactCatalogRoutes(app: Hono, deps: Dependencies): void {
63
+ app.put("/v1/workspaces/:workspaceId/artifact-catalog/:kind/:artifactId/pin", async (context) => {
64
+ const workspaceId = context.req.param("workspaceId");
65
+ const authorization = await requireAccessGrantAuthorization(
66
+ context,
67
+ deps,
68
+ workspaceId,
69
+ "artifacts:publish",
70
+ );
71
+ const grant = authorization.grant;
72
+ if (!hasPermission(grant.permissions, "artifacts:publish"))
73
+ throw new HTTPException(403, { message: "Artifact publish access required" });
74
+ const kind = ArtifactCatalogKind.safeParse(context.req.param("kind"));
75
+ const body = UpdateArtifactPinRequest.safeParse(await context.req.json().catch(() => null));
76
+ const requestedId = context.req.param("artifactId");
77
+ if (
78
+ !kind.success ||
79
+ !body.success ||
80
+ !(editableKinds.has(kind.data)
81
+ ? /^[0-9a-f]{32}$/.test(requestedId)
82
+ : z.string().uuid().safeParse(requestedId).success)
83
+ )
84
+ throw new HTTPException(422, { message: "Invalid artifact pin request" });
85
+ const artifactId = editableKinds.has(kind.data) ? requestedId : requestedId.toLowerCase();
86
+ const fileKind = kind.data === "file" || kind.data === "image";
87
+ if (!hasPermission(grant.permissions, fileKind ? "files:read" : "artifacts:read"))
88
+ throw new HTTPException(403, { message: "Artifact read access required" });
89
+ const actor = fileKind
90
+ ? await fileOwnerContextForAccess(deps, authorization, "files:read")
91
+ : { subjectId: grant.subjectId, privateFileOwnerSubjectId: null };
92
+ context.header("cache-control", "private, no-store");
93
+ return withSessionRlsActorContext(actor, async () => {
94
+ if (editableKinds.has(kind.data)) {
95
+ if (!deps.editableArtifacts)
96
+ throw new HTTPException(503, { message: "Editable artifacts are unavailable" });
97
+ try {
98
+ const scope = editableArtifactScope({ accountId: grant.accountId, workspaceId });
99
+ const artifact = await deps.editableArtifacts.readArtifact({
100
+ scope,
101
+ actor: editableArtifactActorForGrant(grant, "0000000000000001"),
102
+ artifactId: editableArtifactId(artifactId),
103
+ });
104
+ if (
105
+ artifact.scope.accountId !== scope.accountId ||
106
+ artifact.scope.workspaceId !== scope.workspaceId ||
107
+ artifact.id !== artifactId ||
108
+ artifact.modality !== kind.data
109
+ )
110
+ throw new HTTPException(404, { message: "Artifact not found" });
111
+ } catch (error) {
112
+ if (isInvisibleListCandidate(error))
113
+ throw new HTTPException(404, { message: "Artifact not found" });
114
+ throw editableArtifactHttpError(error);
115
+ }
116
+ }
117
+ if (fileKind) {
118
+ const subjectId = await fileAuthoritySubjectIdForGrant(deps, grant);
119
+ const [file] = await getFilesForSubject(deps.db, {
120
+ accountId: grant.accountId,
121
+ workspaceId,
122
+ subjectId,
123
+ fileIds: [artifactId],
124
+ });
125
+ if (!file || file.status !== "ready")
126
+ throw new HTTPException(404, { message: "Artifact not found" });
127
+ }
128
+ try {
129
+ await updateArtifactPin(
130
+ deps.db,
131
+ { accountId: grant.accountId, workspaceId },
132
+ {
133
+ kind: kind.data,
134
+ artifactId,
135
+ pinned: body.data.pinned,
136
+ },
137
+ );
138
+ } catch (error) {
139
+ if (nestedPostgresSqlState(error) === "42501")
140
+ throw new HTTPException(404, { message: "Artifact not found" });
141
+ throw error;
142
+ }
143
+ return context.json(
144
+ ArtifactPinResponse.parse({ kind: kind.data, artifactId, pinned: body.data.pinned }),
145
+ );
146
+ });
147
+ });
57
148
  app.get("/v1/workspaces/:workspaceId/artifact-catalog", async (context) => {
58
149
  const workspaceId = context.req.param("workspaceId");
59
150
  const authorization = await requireAccessGrantAuthorization(context, deps, workspaceId);
@@ -210,6 +301,7 @@ async function projectCandidate(
210
301
  status: row.status,
211
302
  createdAt: iso(row.created_at),
212
303
  updatedAt: iso(row.updated_at),
304
+ ...(row.pinned !== undefined ? { pinned: row.pinned } : {}),
213
305
  };
214
306
  if (row.version_id) item.versionId = row.version_id;
215
307
  if (editableKinds.has(row.kind)) {
@@ -691,11 +691,6 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
691
691
  ),
692
692
  );
693
693
  } catch (error) {
694
- if (nestedPostgresSqlState(error) === "55000") {
695
- throw new HTTPException(409, {
696
- message: "private-session readiness is not activated for this organization",
697
- });
698
- }
699
694
  rethrowMembershipError(error);
700
695
  }
701
696
  });
@@ -1,4 +1,5 @@
1
1
  import { getRetainedProviderCommand } from "@opengeni/db/retained-provider-commands";
2
+ import { assertGoalResumeAllowed, GoalResumeBlockedError } from "@opengeni/core";
2
3
  import { UnsupportedLatencyModeError } from "@opengeni/config";
3
4
  import { searchSessionMessagesForSubject, SessionMessageSearchCursorError } from "@opengeni/db";
4
5
  import { listSessionEventSlices } from "@opengeni/db/session-event-slices";
@@ -2748,6 +2749,15 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2748
2749
  sessionId,
2749
2750
  {
2750
2751
  status: "active",
2752
+ beforeResume: (tx, session, causalTurn) =>
2753
+ assertGoalResumeAllowed({ ...deps, db: tx }, session, causalTurn).catch(
2754
+ (error: unknown) => {
2755
+ if (error instanceof GoalResumeBlockedError) {
2756
+ throw new HTTPException(422, { message: error.message, cause: error });
2757
+ }
2758
+ throw error;
2759
+ },
2760
+ ),
2751
2761
  event: { type: "goal.resumed", actor: "api" },
2752
2762
  },
2753
2763
  );
@@ -5152,7 +5162,7 @@ export function sessionTenancyHttpError(error: unknown): Error {
5152
5162
  if (error instanceof SessionTenancyNotActivatedError) {
5153
5163
  return new ApiHttpError(409, {
5154
5164
  code: "conflict",
5155
- message: "Session tenancy is not activated for this organization.",
5165
+ message: "Only-me chats are not enabled for this organization.",
5156
5166
  retryable: false,
5157
5167
  details: { reason: "not_activated" },
5158
5168
  });