@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.
- package/dist/app.js +1 -1
- package/dist/artifact-catalog-cursor.d.ts +1 -0
- package/dist/{chunk-6TZ5LG3O.js → chunk-A4J3LBH6.js} +169 -69
- package/dist/{chunk-6TZ5LG3O.js.map → chunk-A4J3LBH6.js.map} +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/package.json +20 -20
- package/src/app.ts +8 -2
- package/src/artifact-catalog-cursor.ts +1 -0
- package/src/index.ts +0 -2
- package/src/mcp/action-catalog.gen.ts +11 -0
- package/src/mcp/server.ts +3 -0
- package/src/mcp-oauth.ts +8 -2
- package/src/routes/artifact-catalog.ts +92 -0
- package/src/routes/organization-memberships.ts +0 -5
- package/src/routes/sessions.ts +11 -1
package/dist/mcp/server.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export declare function slackBotFileContentResult(result: Awaited<ReturnType<Ope
|
|
|
43
43
|
mimeType?: never;
|
|
44
44
|
data?: never;
|
|
45
45
|
} | {
|
|
46
|
-
|
|
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
|
|
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
|
|
47
|
-
"@opengeni/artifact-tool": "1.0
|
|
48
|
-
"@opengeni/capabilities": "1.0
|
|
49
|
-
"@opengeni/codemode": "1.0
|
|
50
|
-
"@opengeni/codex": "1.0
|
|
51
|
-
"@opengeni/config": "1.0
|
|
52
|
-
"@opengeni/contracts": "1.0
|
|
53
|
-
"@opengeni/core": "1.0
|
|
54
|
-
"@opengeni/db": "1.0
|
|
55
|
-
"@opengeni/documents": "1.0
|
|
56
|
-
"@opengeni/events": "1.0
|
|
57
|
-
"@opengeni/github": "1.0
|
|
58
|
-
"@opengeni/interaction": "1.0
|
|
59
|
-
"@opengeni/network": "1.0
|
|
60
|
-
"@opengeni/observability": "1.0
|
|
61
|
-
"@opengeni/runtime": "1.0
|
|
62
|
-
"@opengeni/storage": "1.0
|
|
63
|
-
"@opengeni/tool-gateway": "1.0
|
|
64
|
-
"@opengeni/xai-subscription": "1.0
|
|
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
|
-
|
|
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
|
{
|
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
|
-
|
|
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
|
});
|
package/src/routes/sessions.ts
CHANGED
|
@@ -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: "
|
|
5165
|
+
message: "Only-me chats are not enabled for this organization.",
|
|
5156
5166
|
retryable: false,
|
|
5157
5167
|
details: { reason: "not_activated" },
|
|
5158
5168
|
});
|