@opengeni/api-router 2.5.0 → 2.6.4
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/auth/managed-auth-attempt-context.d.ts +5 -1
- package/dist/auth/managed-auth.d.ts +24 -1
- package/dist/{chunk-QESX7HDK.js → chunk-XTLI3CBH.js} +2236 -387
- package/dist/chunk-XTLI3CBH.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +104 -4
- package/dist/index.js.map +1 -1
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
- package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
- package/dist/mcp/remember.d.ts +2 -2
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/mcp/session-wait.d.ts +19 -0
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/browser-sessions.d.ts +1 -0
- package/dist/routes/computer-sessions.d.ts +12 -0
- package/dist/routes/managed-auth-session-sets.d.ts +7 -0
- package/dist/routes/workspaces.d.ts +1 -1
- package/dist/sandbox/metrics-ingestion.d.ts +16 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/package.json +15 -15
- package/src/app.ts +172 -20
- package/src/auth/managed-auth-attempt-context.ts +40 -3
- package/src/auth/managed-auth-session-adapter.ts +1 -0
- package/src/auth/managed-auth.ts +164 -4
- package/src/http/sse.ts +279 -45
- package/src/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/company-brain-governed-writes.ts +38 -23
- package/src/mcp/company-profile-agent-admin.ts +7 -7
- package/src/mcp/remember.ts +19 -8
- package/src/mcp/server.ts +318 -26
- package/src/mcp/session-wait.ts +56 -8
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- package/src/routes/browser-sessions.ts +10 -2
- package/src/routes/capabilities.ts +3 -3
- package/src/routes/codex.ts +483 -74
- package/src/routes/company-profile.ts +64 -0
- package/src/routes/computer-sessions.ts +103 -1
- package/src/routes/integration-facets.ts +8 -5
- package/src/routes/interaction-resources.ts +7 -1
- package/src/routes/managed-auth-session-sets.ts +199 -2
- package/src/routes/organization-memberships.ts +28 -8
- package/src/routes/packs.ts +5 -5
- package/src/routes/plugins.ts +2 -2
- package/src/routes/scheduled-tasks.ts +9 -0
- package/src/routes/sessions.ts +3 -6
- package/src/routes/skills.ts +3 -3
- package/src/routes/workspaces.ts +293 -54
- package/src/sandbox/channel-a.ts +10 -4
- package/src/sandbox/machines.ts +13 -6
- package/src/sandbox/metrics-ingestion.ts +157 -3
- package/src/sandbox/viewer.ts +20 -1
- package/src/workspace-delete-observability.ts +75 -0
- package/dist/chunk-QESX7HDK.js.map +0 -1
package/src/routes/api-keys.ts
CHANGED
|
@@ -1,12 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
CreateApiKeyRequest,
|
|
3
|
+
CreateApiKeyResponse,
|
|
4
|
+
CreateOrganizationApiKeyRequest,
|
|
5
|
+
Permission,
|
|
6
|
+
type AccessContext,
|
|
7
|
+
} from "@opengeni/contracts";
|
|
8
|
+
import {
|
|
9
|
+
createApiKey,
|
|
10
|
+
createOrganizationApiKey as createOrganizationApiKeyRecord,
|
|
11
|
+
listApiKeys,
|
|
12
|
+
listOrganizationApiKeys,
|
|
13
|
+
OrganizationApiKeyLimitExceededError,
|
|
14
|
+
revokeApiKey,
|
|
15
|
+
revokeOrganizationApiKey,
|
|
16
|
+
} from "@opengeni/db";
|
|
17
|
+
import { configuredStaticUsageLimits } from "@opengeni/config";
|
|
3
18
|
import { zValidator } from "@hono/zod-validator";
|
|
4
19
|
import type { Hono } from "hono";
|
|
5
20
|
import { HTTPException } from "hono/http-exception";
|
|
6
21
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
7
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
accountScopedApiKeyWorkspaceAuthority,
|
|
24
|
+
requireAccessContext,
|
|
25
|
+
requireAccessGrant,
|
|
26
|
+
requireAccessGrantAuthorization,
|
|
27
|
+
type AccessGrantAuthorization,
|
|
28
|
+
} from "@opengeni/core";
|
|
8
29
|
import { requireLimit } from "@opengeni/core";
|
|
9
30
|
|
|
31
|
+
export const organizationApiKeyPermissions: Permission[] = [
|
|
32
|
+
"account:read",
|
|
33
|
+
"workspace:create",
|
|
34
|
+
"workspace:read",
|
|
35
|
+
"workspace:admin",
|
|
36
|
+
"api_keys:manage",
|
|
37
|
+
];
|
|
38
|
+
|
|
10
39
|
export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
11
40
|
app.get("/v1/workspaces/:workspaceId/api-keys", async (c) => {
|
|
12
41
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -19,11 +48,17 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
19
48
|
zValidator("json", CreateApiKeyRequest.omit({ workspaceId: true })),
|
|
20
49
|
async (c) => {
|
|
21
50
|
const workspaceId = c.req.param("workspaceId");
|
|
22
|
-
const
|
|
51
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
52
|
+
c,
|
|
53
|
+
deps,
|
|
54
|
+
workspaceId,
|
|
55
|
+
"api_keys:manage",
|
|
56
|
+
);
|
|
57
|
+
const grant = authorization.grant;
|
|
23
58
|
const body = c.req.valid("json");
|
|
24
59
|
const permissions: Permission[] =
|
|
25
60
|
body.permissions.length > 0 ? (body.permissions as Permission[]) : ["workspace:read"];
|
|
26
|
-
ensureDelegablePermissions(
|
|
61
|
+
ensureDelegablePermissions(authorization, permissions);
|
|
27
62
|
await requireLimit(deps, {
|
|
28
63
|
accountId: grant.accountId,
|
|
29
64
|
workspaceId,
|
|
@@ -51,12 +86,90 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
51
86
|
await requireAccessGrant(c, deps, workspaceId, "api_keys:manage");
|
|
52
87
|
return c.json(await revokeApiKey(deps.db, workspaceId, c.req.param("apiKeyId")));
|
|
53
88
|
});
|
|
89
|
+
|
|
90
|
+
app.get("/v1/organizations/:organizationId/api-keys", async (c) => {
|
|
91
|
+
const organizationId = c.req.param("organizationId");
|
|
92
|
+
const context = await requireAccessContext(c, deps);
|
|
93
|
+
requireOrganizationApiKeyControlPermission(context, organizationId);
|
|
94
|
+
return c.json({ apiKeys: await listOrganizationApiKeys(deps.db, organizationId) });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
app.post(
|
|
98
|
+
"/v1/organizations/:organizationId/api-keys",
|
|
99
|
+
zValidator("json", CreateOrganizationApiKeyRequest),
|
|
100
|
+
async (c) => {
|
|
101
|
+
const organizationId = c.req.param("organizationId");
|
|
102
|
+
const context = await requireAccessContext(c, deps);
|
|
103
|
+
requireOrganizationApiKeyControlPermission(context, organizationId);
|
|
104
|
+
const body = c.req.valid("json");
|
|
105
|
+
const token = generateApiKeyToken();
|
|
106
|
+
try {
|
|
107
|
+
const apiKey = await createOrganizationApiKeyRecord(deps.db, {
|
|
108
|
+
accountId: organizationId,
|
|
109
|
+
name: body.name,
|
|
110
|
+
description: body.description ?? null,
|
|
111
|
+
prefix: token.slice(0, 14),
|
|
112
|
+
keyHash: await sha256Hex(token),
|
|
113
|
+
permissions: organizationApiKeyPermissions,
|
|
114
|
+
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
|
|
115
|
+
maxActiveKeys: organizationApiKeyLimit(deps),
|
|
116
|
+
rotationSourceApiKeyId: authenticatedApiKeyId(context),
|
|
117
|
+
});
|
|
118
|
+
return c.json(CreateApiKeyResponse.parse({ apiKey, token }), 201);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof OrganizationApiKeyLimitExceededError) {
|
|
121
|
+
throw new HTTPException(429, { message: error.message });
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
app.delete("/v1/organizations/:organizationId/api-keys/:apiKeyId", async (c) => {
|
|
129
|
+
const organizationId = c.req.param("organizationId");
|
|
130
|
+
const context = await requireAccessContext(c, deps);
|
|
131
|
+
requireOrganizationApiKeyControlPermission(context, organizationId);
|
|
132
|
+
const apiKey = await revokeOrganizationApiKey(deps.db, organizationId, c.req.param("apiKeyId"));
|
|
133
|
+
if (!apiKey) {
|
|
134
|
+
throw new HTTPException(404, { message: "API key not found" });
|
|
135
|
+
}
|
|
136
|
+
return c.json(apiKey);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function requireAccountPermission(
|
|
141
|
+
context: AccessContext,
|
|
142
|
+
accountId: string,
|
|
143
|
+
permission: Permission,
|
|
144
|
+
): void {
|
|
145
|
+
const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
|
|
146
|
+
if (
|
|
147
|
+
!grant ||
|
|
148
|
+
(!grant.permissions.includes(permission) && !grant.permissions.includes("account:admin"))
|
|
149
|
+
) {
|
|
150
|
+
throw new HTTPException(403, { message: `missing permission: ${permission}` });
|
|
151
|
+
}
|
|
54
152
|
}
|
|
55
153
|
|
|
56
|
-
function ensureDelegablePermissions(
|
|
154
|
+
function ensureDelegablePermissions(
|
|
155
|
+
authorization: AccessGrantAuthorization,
|
|
156
|
+
requested: Permission[],
|
|
157
|
+
): void {
|
|
158
|
+
const grantPermissions = authorization.grant.permissions;
|
|
57
159
|
if (grantPermissions.includes("workspace:admin")) {
|
|
160
|
+
const accountLiteralPermissions = new Set<Permission>([
|
|
161
|
+
"account:read",
|
|
162
|
+
"account:admin",
|
|
163
|
+
"workspace:create",
|
|
164
|
+
"billing:read",
|
|
165
|
+
"billing:manage",
|
|
166
|
+
]);
|
|
167
|
+
const workspaceLiteralPermissions = new Set<Permission>(["members:manage", "secrets:read"]);
|
|
58
168
|
const highTrustMissing = requested.filter(
|
|
59
|
-
(permission) =>
|
|
169
|
+
(permission) =>
|
|
170
|
+
(accountLiteralPermissions.has(permission) &&
|
|
171
|
+
!authorization.accountGrant?.permissions.includes(permission)) ||
|
|
172
|
+
(workspaceLiteralPermissions.has(permission) && !grantPermissions.includes(permission)),
|
|
60
173
|
);
|
|
61
174
|
if (highTrustMissing.length === 0) return;
|
|
62
175
|
throw new HTTPException(403, {
|
|
@@ -71,6 +184,35 @@ function ensureDelegablePermissions(grantPermissions: Permission[], requested: P
|
|
|
71
184
|
}
|
|
72
185
|
}
|
|
73
186
|
|
|
187
|
+
function requireOrganizationApiKeyControlPermission(
|
|
188
|
+
context: AccessContext,
|
|
189
|
+
organizationId: string,
|
|
190
|
+
): void {
|
|
191
|
+
requireAccountPermission(context, organizationId, "api_keys:manage");
|
|
192
|
+
if (!context.subjectId.startsWith("api_key:")) return;
|
|
193
|
+
const authority = accountScopedApiKeyWorkspaceAuthority(context);
|
|
194
|
+
if (
|
|
195
|
+
!authority ||
|
|
196
|
+
authority.accountId !== organizationId ||
|
|
197
|
+
!authority.permissions.includes("api_keys:manage")
|
|
198
|
+
) {
|
|
199
|
+
throw new HTTPException(403, { message: "organization API key authority required" });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function authenticatedApiKeyId(context: AccessContext): string | null {
|
|
204
|
+
return context.subjectId.startsWith("api_key:")
|
|
205
|
+
? context.subjectId.slice("api_key:".length)
|
|
206
|
+
: null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function organizationApiKeyLimit(deps: ApiRouteDeps): number | null {
|
|
210
|
+
if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return configuredStaticUsageLimits(deps.settings).maxApiKeysPerWorkspace ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
74
216
|
function generateApiKeyToken(): string {
|
|
75
217
|
const bytes = new Uint8Array(32);
|
|
76
218
|
crypto.getRandomValues(bytes);
|
|
@@ -3435,7 +3435,14 @@ async function ensureHeadedBrowserDisplayStack(
|
|
|
3435
3435
|
headless: boolean,
|
|
3436
3436
|
linkedComputer: boolean,
|
|
3437
3437
|
): Promise<void> {
|
|
3438
|
-
if (
|
|
3438
|
+
if (
|
|
3439
|
+
!browserNeedsStandaloneDisplayStack({
|
|
3440
|
+
headless,
|
|
3441
|
+
linkedComputer,
|
|
3442
|
+
nativeBrowserControl: typeof session.ensureBrowserControl === "function",
|
|
3443
|
+
})
|
|
3444
|
+
)
|
|
3445
|
+
return;
|
|
3439
3446
|
if (typeof session.exec !== "function" && typeof session.execCommand !== "function") return;
|
|
3440
3447
|
await ensureDisplayStack(session, {
|
|
3441
3448
|
telemetryContext: { callerKind: "viewer" },
|
|
@@ -3445,8 +3452,9 @@ async function ensureHeadedBrowserDisplayStack(
|
|
|
3445
3452
|
export function browserNeedsStandaloneDisplayStack(input: {
|
|
3446
3453
|
headless: boolean;
|
|
3447
3454
|
linkedComputer: boolean;
|
|
3455
|
+
nativeBrowserControl: boolean;
|
|
3448
3456
|
}): boolean {
|
|
3449
|
-
return !input.headless && !input.linkedComputer;
|
|
3457
|
+
return !input.headless && !input.linkedComputer && !input.nativeBrowserControl;
|
|
3450
3458
|
}
|
|
3451
3459
|
|
|
3452
3460
|
async function ensureLinkedComputerController(
|
|
@@ -32,7 +32,7 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
32
32
|
|
|
33
33
|
app.post("/v1/workspaces/:workspaceId/capabilities", async (c) => {
|
|
34
34
|
const workspaceId = c.req.param("workspaceId");
|
|
35
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
35
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
36
36
|
const payload = CreateCapabilityCatalogItemRequest.parse(await c.req.json());
|
|
37
37
|
return c.json(
|
|
38
38
|
await createCatalogItem({ db, accountId: grant.accountId, workspaceId, payload }),
|
|
@@ -62,7 +62,7 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
62
62
|
|
|
63
63
|
app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/enable", async (c) => {
|
|
64
64
|
const workspaceId = c.req.param("workspaceId");
|
|
65
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
65
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
66
66
|
const payload = EnableCapabilityRequest.parse(await c.req.json());
|
|
67
67
|
const installation = await enableCapability({
|
|
68
68
|
db,
|
|
@@ -78,7 +78,7 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
78
78
|
|
|
79
79
|
app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/disable", async (c) => {
|
|
80
80
|
const workspaceId = c.req.param("workspaceId");
|
|
81
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
81
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
82
82
|
const installation = await disableCapability({
|
|
83
83
|
db,
|
|
84
84
|
accountId: grant.accountId,
|