@opengeni/api-router 0.30.3 → 2.2.0-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.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-WCXHI3FF.js.map +0 -1
|
@@ -50,7 +50,11 @@ import {
|
|
|
50
50
|
} from "@opengeni/db";
|
|
51
51
|
import type { Context, Hono } from "hono";
|
|
52
52
|
import { HTTPException } from "hono/http-exception";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
requireAccessGrant,
|
|
55
|
+
requireAccessGrantAuthorization,
|
|
56
|
+
requirePermission,
|
|
57
|
+
} from "@opengeni/core";
|
|
54
58
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
55
59
|
import {
|
|
56
60
|
approveDeviceEnrollment,
|
|
@@ -88,7 +92,10 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
88
92
|
const lookupLimiter = new TokenBucket({ capacity: 30, refillPerSecond: 1 });
|
|
89
93
|
// The headless token exchange (UNAUTHENTICATED — the token is the auth). Bounded
|
|
90
94
|
// against an enroll-token brute force; the `oget_` HMAC is the real boundary.
|
|
91
|
-
const exchangeLimiter = new TokenBucket({
|
|
95
|
+
const exchangeLimiter = new TokenBucket({
|
|
96
|
+
capacity: 20,
|
|
97
|
+
refillPerSecond: 0.5,
|
|
98
|
+
});
|
|
92
99
|
|
|
93
100
|
function rateLimit(c: Context, limiter: TokenBucket): void {
|
|
94
101
|
const ip = clientIp(c);
|
|
@@ -123,8 +130,10 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
123
130
|
machineName: body.machineName ?? null,
|
|
124
131
|
canOfferDisplay: body.canOfferDisplay,
|
|
125
132
|
requestsScreenControl: body.requestsScreenControl,
|
|
126
|
-
//
|
|
127
|
-
|
|
133
|
+
// Prefer the externally configured deployment origin. Behind a TLS-
|
|
134
|
+
// terminating proxy c.req.url can carry the internal http scheme, which
|
|
135
|
+
// must never leak into the device-flow command shown to the operator.
|
|
136
|
+
verificationOrigin: settings.publicBaseUrl ?? new URL(c.req.url).origin,
|
|
128
137
|
},
|
|
129
138
|
);
|
|
130
139
|
return c.json(result, 201);
|
|
@@ -156,7 +165,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
156
165
|
rateLimit(c, lookupLimiter);
|
|
157
166
|
const parsed = DeviceEnrollmentLookupRequest.safeParse(await c.req.json().catch(() => null));
|
|
158
167
|
if (!parsed.success) {
|
|
159
|
-
throw new HTTPException(400, {
|
|
168
|
+
throw new HTTPException(400, {
|
|
169
|
+
message: "invalid device-lookup request",
|
|
170
|
+
});
|
|
160
171
|
}
|
|
161
172
|
const record = await lookupDeviceEnrollment(
|
|
162
173
|
{ db, settings },
|
|
@@ -165,7 +176,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
165
176
|
if (!record) {
|
|
166
177
|
// Unknown / terminal / expired code → 404 (indistinguishable from an
|
|
167
178
|
// unauthorized one below, by design).
|
|
168
|
-
throw new HTTPException(404, {
|
|
179
|
+
throw new HTTPException(404, {
|
|
180
|
+
message: "no pending enrollment for that code",
|
|
181
|
+
});
|
|
169
182
|
}
|
|
170
183
|
// Authorize the caller against the RESOLVED workspace. A missing grant throws
|
|
171
184
|
// 403/404 from requireAccessGrant; we normalize that to 404 so a caller cannot
|
|
@@ -173,7 +186,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
173
186
|
try {
|
|
174
187
|
await requireAccessGrant(c, deps, record.workspaceId, "enrollments:read");
|
|
175
188
|
} catch {
|
|
176
|
-
throw new HTTPException(404, {
|
|
189
|
+
throw new HTTPException(404, {
|
|
190
|
+
message: "no pending enrollment for that code",
|
|
191
|
+
});
|
|
177
192
|
}
|
|
178
193
|
return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(record)), 200);
|
|
179
194
|
});
|
|
@@ -189,7 +204,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
189
204
|
rateLimit(c, exchangeLimiter);
|
|
190
205
|
const parsed = EnrollTokenExchangeRequest.safeParse(await c.req.json().catch(() => null));
|
|
191
206
|
if (!parsed.success) {
|
|
192
|
-
throw new HTTPException(400, {
|
|
207
|
+
throw new HTTPException(400, {
|
|
208
|
+
message: "invalid enroll-token-exchange request",
|
|
209
|
+
});
|
|
193
210
|
}
|
|
194
211
|
const body = parsed.data;
|
|
195
212
|
const result = await exchangeEnrollToken(
|
|
@@ -206,10 +223,14 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
206
223
|
if (!result.ok) {
|
|
207
224
|
if (result.reason === "disabled") {
|
|
208
225
|
// The credential plane is off for this deployment (no signing secret).
|
|
209
|
-
throw new HTTPException(503, {
|
|
226
|
+
throw new HTTPException(503, {
|
|
227
|
+
message: "enrollment credential plane is not configured",
|
|
228
|
+
});
|
|
210
229
|
}
|
|
211
230
|
// An invalid / expired / wrong-typ token — the token is the auth, so 401.
|
|
212
|
-
throw new HTTPException(401, {
|
|
231
|
+
throw new HTTPException(401, {
|
|
232
|
+
message: "invalid or expired enroll token",
|
|
233
|
+
});
|
|
213
234
|
}
|
|
214
235
|
return c.json(EnrollTokenExchangeResponse.parse({ credentials: result.credentials }), 201);
|
|
215
236
|
});
|
|
@@ -218,18 +239,48 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
218
239
|
// The LOUD CONSENT step. requireAccessGrant BEFORE the Zod parse.
|
|
219
240
|
app.post("/v1/workspaces/:workspaceId/enrollments/device/approve", async (c) => {
|
|
220
241
|
const workspaceId = c.req.param("workspaceId");
|
|
221
|
-
const
|
|
242
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
243
|
+
const grant = authorization.grant;
|
|
244
|
+
requirePermission(grant, "enrollments:manage");
|
|
222
245
|
assertSelfhostedEnabled();
|
|
223
|
-
const
|
|
246
|
+
const rawBody = await c.req.json().catch(() => null);
|
|
247
|
+
const parsed = DeviceEnrollmentApproveRequest.safeParse(rawBody);
|
|
224
248
|
if (!parsed.success) {
|
|
225
|
-
throw new HTTPException(400, {
|
|
249
|
+
throw new HTTPException(400, {
|
|
250
|
+
message: "invalid device-approve request",
|
|
251
|
+
});
|
|
226
252
|
}
|
|
227
253
|
const body = parsed.data;
|
|
254
|
+
const scopeWasExplicit =
|
|
255
|
+
typeof rawBody === "object" &&
|
|
256
|
+
rawBody !== null &&
|
|
257
|
+
Object.prototype.hasOwnProperty.call(rawBody, "scope");
|
|
258
|
+
// Managed browser humans get the personal default. Legacy/delegated
|
|
259
|
+
// principals predate organization-user ownership and retain the compatible
|
|
260
|
+
// workspace default when the caller omitted scope; an explicit scope is
|
|
261
|
+
// never silently rewritten.
|
|
262
|
+
const effectiveScope =
|
|
263
|
+
scopeWasExplicit ||
|
|
264
|
+
(grant.principalKind === "human_session" &&
|
|
265
|
+
authorization.contextIntegrity &&
|
|
266
|
+
grant.metadata?.delegated !== true)
|
|
267
|
+
? body.scope
|
|
268
|
+
: undefined;
|
|
269
|
+
const allowOrganization =
|
|
270
|
+
effectiveScope === "organization" &&
|
|
271
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
272
|
+
if (effectiveScope === "organization" && !allowOrganization) {
|
|
273
|
+
throw new HTTPException(403, {
|
|
274
|
+
message: "missing permission: account:admin",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
228
277
|
const approved = await approveDeviceEnrollment(
|
|
229
278
|
{ db, settings },
|
|
230
279
|
{
|
|
231
280
|
accountId: grant.accountId,
|
|
232
281
|
workspaceId,
|
|
282
|
+
...(effectiveScope ? { scope: effectiveScope } : {}),
|
|
283
|
+
allowOrganization,
|
|
233
284
|
userCode: body.userCode,
|
|
234
285
|
allowScreenControl: body.allowScreenControl,
|
|
235
286
|
// The LOUD consent record: WHO consented (the authenticated subject + label).
|
|
@@ -239,7 +290,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
239
290
|
);
|
|
240
291
|
if (!approved) {
|
|
241
292
|
// An unknown / expired / already-terminal user_code in this workspace.
|
|
242
|
-
throw new HTTPException(404, {
|
|
293
|
+
throw new HTTPException(404, {
|
|
294
|
+
message: "no pending enrollment for that code",
|
|
295
|
+
});
|
|
243
296
|
}
|
|
244
297
|
return c.json(
|
|
245
298
|
DeviceEnrollmentApproveResponse.parse({
|
|
@@ -286,7 +339,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
286
339
|
// allowScreenControl=false). Coalesce a missing/empty body to {} before parse.
|
|
287
340
|
const parsed = MintEnrollTokenRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
288
341
|
if (!parsed.success) {
|
|
289
|
-
throw new HTTPException(400, {
|
|
342
|
+
throw new HTTPException(400, {
|
|
343
|
+
message: "invalid mint-enroll-token request",
|
|
344
|
+
});
|
|
290
345
|
}
|
|
291
346
|
const minted = await mintEnrollToken(
|
|
292
347
|
{ db, settings },
|
|
@@ -298,7 +353,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
298
353
|
);
|
|
299
354
|
if (!minted) {
|
|
300
355
|
// The credential plane is off (no signing secret) — mirror poll's disabled path.
|
|
301
|
-
throw new HTTPException(503, {
|
|
356
|
+
throw new HTTPException(503, {
|
|
357
|
+
message: "enrollment credential plane is not configured",
|
|
358
|
+
});
|
|
302
359
|
}
|
|
303
360
|
return c.json(MintEnrollTokenResponse.parse(minted), 201);
|
|
304
361
|
});
|
|
@@ -306,13 +363,15 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
306
363
|
// ── GET /workspaces/:workspaceId/enrollments (user-authed) ──────────────────
|
|
307
364
|
app.get("/v1/workspaces/:workspaceId/enrollments", async (c) => {
|
|
308
365
|
const workspaceId = c.req.param("workspaceId");
|
|
309
|
-
await requireAccessGrant(c, deps, workspaceId, "enrollments:read");
|
|
366
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "enrollments:read");
|
|
310
367
|
assertSelfhostedEnabled();
|
|
311
368
|
const statusFilter = c.req.query("status");
|
|
312
369
|
if (statusFilter !== undefined && statusFilter !== "active" && statusFilter !== "revoked") {
|
|
313
|
-
throw new HTTPException(400, {
|
|
370
|
+
throw new HTTPException(400, {
|
|
371
|
+
message: "status must be active or revoked",
|
|
372
|
+
});
|
|
314
373
|
}
|
|
315
|
-
const rows = await listEnrollments(db,
|
|
374
|
+
const rows = await listEnrollments(db, grant, {
|
|
316
375
|
status: statusFilter === "revoked" ? "revoked" : "active",
|
|
317
376
|
});
|
|
318
377
|
return c.json(
|
|
@@ -320,6 +379,8 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
320
379
|
enrollments: rows.map((row) =>
|
|
321
380
|
EnrollmentSummary.parse({
|
|
322
381
|
id: row.id,
|
|
382
|
+
scope: row.scope,
|
|
383
|
+
generation: row.generation,
|
|
323
384
|
pubkey: row.pubkey,
|
|
324
385
|
exposure: row.exposure,
|
|
325
386
|
hasDisplay: row.hasDisplay,
|
|
@@ -340,19 +401,40 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
340
401
|
// ── POST /workspaces/:workspaceId/enrollments/:id/revoke (user-authed) ──────
|
|
341
402
|
app.post("/v1/workspaces/:workspaceId/enrollments/:enrollmentId/revoke", async (c) => {
|
|
342
403
|
const workspaceId = c.req.param("workspaceId");
|
|
343
|
-
const
|
|
404
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
405
|
+
const grant = authorization.grant;
|
|
406
|
+
requirePermission(grant, "enrollments:manage");
|
|
344
407
|
assertSelfhostedEnabled();
|
|
345
408
|
const parsed = RemoveEnrollmentRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
346
409
|
if (!parsed.success) {
|
|
347
|
-
throw new HTTPException(400, {
|
|
410
|
+
throw new HTTPException(400, {
|
|
411
|
+
message: "invalid machine removal request",
|
|
412
|
+
});
|
|
348
413
|
}
|
|
349
414
|
const idempotencyKey =
|
|
350
415
|
parsed.data.idempotencyKey?.trim() || c.req.header("idempotency-key")?.trim() || randomUUID();
|
|
351
416
|
try {
|
|
417
|
+
const enrollmentId = c.req.param("enrollmentId");
|
|
418
|
+
const enrollment = (await listEnrollments(db, grant, { status: "active" })).find(
|
|
419
|
+
(candidate) => candidate.id === enrollmentId,
|
|
420
|
+
);
|
|
421
|
+
if (!enrollment) {
|
|
422
|
+
throw new HTTPException(404, {
|
|
423
|
+
message: "machine enrollment not found",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
if (
|
|
427
|
+
enrollment.scope === "organization" &&
|
|
428
|
+
authorization.accountGrant?.permissions.includes("account:admin") !== true
|
|
429
|
+
) {
|
|
430
|
+
throw new HTTPException(403, {
|
|
431
|
+
message: "missing permission: account:admin",
|
|
432
|
+
});
|
|
433
|
+
}
|
|
352
434
|
const result = await removeEnrollment(db, {
|
|
353
435
|
accountId: grant.accountId,
|
|
354
|
-
workspaceId,
|
|
355
|
-
enrollmentId
|
|
436
|
+
workspaceId: enrollment.workspaceId,
|
|
437
|
+
enrollmentId,
|
|
356
438
|
operationKey: idempotencyKey,
|
|
357
439
|
...(parsed.data.expectedUpdatedAt
|
|
358
440
|
? { expectedUpdatedAt: parsed.data.expectedUpdatedAt }
|
|
@@ -360,7 +442,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
360
442
|
subjectId: grant.subjectId,
|
|
361
443
|
});
|
|
362
444
|
if (!result) {
|
|
363
|
-
throw new HTTPException(404, {
|
|
445
|
+
throw new HTTPException(404, {
|
|
446
|
+
message: "machine enrollment not found",
|
|
447
|
+
});
|
|
364
448
|
}
|
|
365
449
|
return c.json(
|
|
366
450
|
RevokeEnrollmentResponse.parse({
|
|
@@ -376,7 +460,10 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
376
460
|
if (error instanceof MachineRemovalRevisionConflictError) {
|
|
377
461
|
throw new HTTPException(409, {
|
|
378
462
|
message: error.message,
|
|
379
|
-
cause: {
|
|
463
|
+
cause: {
|
|
464
|
+
code: "stale_revision",
|
|
465
|
+
currentUpdatedAt: error.currentUpdatedAt,
|
|
466
|
+
},
|
|
380
467
|
});
|
|
381
468
|
}
|
|
382
469
|
throw error;
|
|
@@ -410,7 +497,10 @@ class TokenBucket {
|
|
|
410
497
|
}
|
|
411
498
|
|
|
412
499
|
take(key: string, now = Date.now()): boolean {
|
|
413
|
-
const bucket = this.buckets.get(key) ?? {
|
|
500
|
+
const bucket = this.buckets.get(key) ?? {
|
|
501
|
+
tokens: this.capacity,
|
|
502
|
+
updatedAt: now,
|
|
503
|
+
};
|
|
414
504
|
const elapsedSeconds = Math.max(0, (now - bucket.updatedAt) / 1000);
|
|
415
505
|
bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsedSeconds * this.refillPerSecond);
|
|
416
506
|
bucket.updatedAt = now;
|
|
@@ -5,8 +5,6 @@ import {
|
|
|
5
5
|
VariableSetVariableName,
|
|
6
6
|
} from "@opengeni/contracts";
|
|
7
7
|
import {
|
|
8
|
-
countActiveSessionsUsingVariableSet,
|
|
9
|
-
countScheduledTasksUsingVariableSet,
|
|
10
8
|
countVariableSets,
|
|
11
9
|
createVariableSet,
|
|
12
10
|
decryptVariableSetValue,
|
|
@@ -18,10 +16,16 @@ import {
|
|
|
18
16
|
readVariableSetSecretAtomically,
|
|
19
17
|
setVariableSetVariable,
|
|
20
18
|
updateVariableSet,
|
|
19
|
+
VariableSetAttachedError,
|
|
21
20
|
} from "@opengeni/db";
|
|
22
21
|
import type { Hono } from "hono";
|
|
23
22
|
import { HTTPException } from "hono/http-exception";
|
|
24
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
requireAccessGrant,
|
|
25
|
+
requireAccessGrantAuthorization,
|
|
26
|
+
requireLiteralPermission,
|
|
27
|
+
requirePermission,
|
|
28
|
+
} from "@opengeni/core";
|
|
25
29
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
26
30
|
import {
|
|
27
31
|
assertAllowedVariableSetVariableName,
|
|
@@ -45,15 +49,35 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
45
49
|
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
46
50
|
requirePermission(grant, "variable-sets:list");
|
|
47
51
|
requirePermission(grant, "secrets:list");
|
|
48
|
-
return c.json(
|
|
52
|
+
return c.json(
|
|
53
|
+
await listVariableSets(db, {
|
|
54
|
+
accountId: grant.accountId,
|
|
55
|
+
workspaceId,
|
|
56
|
+
subjectId: grant.subjectId,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
49
59
|
});
|
|
50
60
|
|
|
51
61
|
app.post(`${prefix}`, async (c) => {
|
|
52
62
|
const workspaceId = c.req.param("workspaceId")!;
|
|
53
|
-
const
|
|
63
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
64
|
+
const grant = authorization.grant;
|
|
54
65
|
requirePermission(grant, "variable-sets:write");
|
|
55
66
|
const key = requireVariableSetEncryption(settings);
|
|
56
67
|
const payload = CreateVariableSetRequest.parse(await c.req.json());
|
|
68
|
+
if (prefix.endsWith("/environments") && payload.scope !== "workspace") {
|
|
69
|
+
throw new HTTPException(422, {
|
|
70
|
+
message: "the legacy environments route only creates workspace-scoped variable sets",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
payload.scope === "organization" &&
|
|
75
|
+
authorization.accountGrant?.permissions.includes("account:admin") !== true
|
|
76
|
+
) {
|
|
77
|
+
throw new HTTPException(403, {
|
|
78
|
+
message: "missing permission: account:admin",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
57
81
|
if (payload.variables.length > 0) {
|
|
58
82
|
requirePermission(grant, "secrets:write");
|
|
59
83
|
}
|
|
@@ -73,12 +97,17 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
73
97
|
}
|
|
74
98
|
variableNames.add(variable.name);
|
|
75
99
|
}
|
|
76
|
-
|
|
100
|
+
const access = {
|
|
101
|
+
accountId: grant.accountId,
|
|
102
|
+
workspaceId,
|
|
103
|
+
subjectId: grant.subjectId,
|
|
104
|
+
};
|
|
105
|
+
if ((await countVariableSets(db, access, payload.scope)) >= MAX_ENVIRONMENTS_PER_WORKSPACE) {
|
|
77
106
|
throw new HTTPException(422, {
|
|
78
107
|
message: `a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE} variable sets`,
|
|
79
108
|
});
|
|
80
109
|
}
|
|
81
|
-
if (await getVariableSetByName(db,
|
|
110
|
+
if (await getVariableSetByName(db, access, name, payload.scope)) {
|
|
82
111
|
throw new HTTPException(409, {
|
|
83
112
|
message: `variable set name is already in use: ${name}`,
|
|
84
113
|
});
|
|
@@ -88,6 +117,9 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
88
117
|
const created = await createVariableSet(db, {
|
|
89
118
|
accountId: grant.accountId,
|
|
90
119
|
workspaceId,
|
|
120
|
+
scope: payload.scope,
|
|
121
|
+
subjectId: grant.subjectId,
|
|
122
|
+
allowOrganization: payload.scope === "organization",
|
|
91
123
|
name,
|
|
92
124
|
description: payload.description ?? null,
|
|
93
125
|
variables: payload.variables.map((variable) => ({
|
|
@@ -108,7 +140,7 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
108
140
|
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
109
141
|
requirePermission(grant, "variable-sets:read");
|
|
110
142
|
requirePermission(grant, "secrets:list");
|
|
111
|
-
return c.json(await requireVariableSetForApi(db,
|
|
143
|
+
return c.json(await requireVariableSetForApi(db, grant, c.req.param("variableSetId")!));
|
|
112
144
|
});
|
|
113
145
|
|
|
114
146
|
if (prefix.endsWith("/variable-sets")) {
|
|
@@ -139,27 +171,47 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
139
171
|
|
|
140
172
|
app.patch(`${prefix}/:variableSetId`, async (c) => {
|
|
141
173
|
const workspaceId = c.req.param("workspaceId")!;
|
|
142
|
-
const
|
|
174
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
175
|
+
const grant = authorization.grant;
|
|
143
176
|
requirePermission(grant, "variable-sets:write");
|
|
144
|
-
const variableSet = await requireVariableSetForApi(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
177
|
+
const variableSet = await requireVariableSetForApi(db, grant, c.req.param("variableSetId")!);
|
|
178
|
+
const allowOrganization =
|
|
179
|
+
variableSet.scope === "organization" &&
|
|
180
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
181
|
+
if (variableSet.scope === "organization" && !allowOrganization) {
|
|
182
|
+
throw new HTTPException(403, {
|
|
183
|
+
message: "missing permission: account:admin",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
149
186
|
const payload = UpdateVariableSetRequest.parse(await c.req.json());
|
|
150
187
|
const name = payload.name !== undefined ? trimmedVariableSetName(payload.name) : undefined;
|
|
151
188
|
if (name !== undefined && name !== variableSet.name) {
|
|
152
|
-
const existing = await getVariableSetByName(
|
|
189
|
+
const existing = await getVariableSetByName(
|
|
190
|
+
db,
|
|
191
|
+
{
|
|
192
|
+
accountId: grant.accountId,
|
|
193
|
+
workspaceId,
|
|
194
|
+
subjectId: grant.subjectId,
|
|
195
|
+
},
|
|
196
|
+
name,
|
|
197
|
+
variableSet.scope,
|
|
198
|
+
);
|
|
153
199
|
if (existing && existing.id !== variableSet.id) {
|
|
154
200
|
throw new HTTPException(409, {
|
|
155
201
|
message: `variable set name is already in use: ${name}`,
|
|
156
202
|
});
|
|
157
203
|
}
|
|
158
204
|
}
|
|
159
|
-
const updated = await updateVariableSet(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
205
|
+
const updated = await updateVariableSet(
|
|
206
|
+
db,
|
|
207
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
208
|
+
variableSet.id,
|
|
209
|
+
{
|
|
210
|
+
...(name !== undefined ? { name } : {}),
|
|
211
|
+
...(payload.description !== undefined ? { description: payload.description } : {}),
|
|
212
|
+
allowOrganization,
|
|
213
|
+
},
|
|
214
|
+
);
|
|
163
215
|
await recordVariableSetAuditEvent(db, {
|
|
164
216
|
grant,
|
|
165
217
|
action: "variable_set.updated",
|
|
@@ -170,35 +222,36 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
170
222
|
|
|
171
223
|
app.delete(`${prefix}/:variableSetId`, async (c) => {
|
|
172
224
|
const workspaceId = c.req.param("workspaceId")!;
|
|
173
|
-
const
|
|
225
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
226
|
+
const grant = authorization.grant;
|
|
174
227
|
requirePermission(grant, "variable-sets:write");
|
|
175
228
|
requirePermission(grant, "secrets:write");
|
|
176
|
-
const variableSet = await requireVariableSetForApi(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
workspaceId,
|
|
184
|
-
variableSet.id,
|
|
185
|
-
);
|
|
186
|
-
if (attachedTasks > 0) {
|
|
187
|
-
throw new HTTPException(409, {
|
|
188
|
-
message: `variable set is attached to ${attachedTasks} scheduled task(s); detach first`,
|
|
229
|
+
const variableSet = await requireVariableSetForApi(db, grant, c.req.param("variableSetId")!);
|
|
230
|
+
const allowOrganization =
|
|
231
|
+
variableSet.scope === "organization" &&
|
|
232
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
233
|
+
if (variableSet.scope === "organization" && !allowOrganization) {
|
|
234
|
+
throw new HTTPException(403, {
|
|
235
|
+
message: "missing permission: account:admin",
|
|
189
236
|
});
|
|
190
237
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
238
|
+
try {
|
|
239
|
+
await deleteVariableSet(
|
|
240
|
+
db,
|
|
241
|
+
{
|
|
242
|
+
accountId: grant.accountId,
|
|
243
|
+
workspaceId,
|
|
244
|
+
subjectId: grant.subjectId,
|
|
245
|
+
},
|
|
246
|
+
variableSet.id,
|
|
247
|
+
{ allowOrganization },
|
|
248
|
+
);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof VariableSetAttachedError) {
|
|
251
|
+
throw new HTTPException(409, { message: error.message });
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
200
254
|
}
|
|
201
|
-
await deleteVariableSet(db, workspaceId, variableSet.id);
|
|
202
255
|
await recordVariableSetAuditEvent(db, {
|
|
203
256
|
grant,
|
|
204
257
|
action: "variable_set.deleted",
|
|
@@ -209,16 +262,21 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
209
262
|
|
|
210
263
|
app.put(`${prefix}/:variableSetId/variables/:name`, async (c) => {
|
|
211
264
|
const workspaceId = c.req.param("workspaceId")!;
|
|
212
|
-
const
|
|
265
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
266
|
+
const grant = authorization.grant;
|
|
213
267
|
requirePermission(grant, "variable-sets:write");
|
|
214
268
|
requirePermission(grant, "secrets:write");
|
|
215
269
|
const key = requireVariableSetEncryption(settings);
|
|
216
270
|
const name = parseVariableName(c.req.param("name")!);
|
|
217
|
-
const variableSet = await requireVariableSetForApi(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
271
|
+
const variableSet = await requireVariableSetForApi(db, grant, c.req.param("variableSetId")!);
|
|
272
|
+
const allowOrganization =
|
|
273
|
+
variableSet.scope === "organization" &&
|
|
274
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
275
|
+
if (variableSet.scope === "organization" && !allowOrganization) {
|
|
276
|
+
throw new HTTPException(403, {
|
|
277
|
+
message: "missing permission: account:admin",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
222
280
|
const payload = SetVariableSetVariableRequest.parse(await c.req.json());
|
|
223
281
|
const exists = variableSet.variables.some((variable) => variable.name === name);
|
|
224
282
|
if (!exists && variableSet.variables.length >= MAX_VARIABLES_PER_ENVIRONMENT) {
|
|
@@ -229,9 +287,11 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
229
287
|
const metadata = await setVariableSetVariable(db, {
|
|
230
288
|
accountId: grant.accountId,
|
|
231
289
|
workspaceId,
|
|
290
|
+
subjectId: grant.subjectId,
|
|
232
291
|
variableSetId: variableSet.id,
|
|
233
292
|
name,
|
|
234
293
|
valueEncrypted: encryptVariableSetValue(key, payload.value),
|
|
294
|
+
allowOrganization,
|
|
235
295
|
});
|
|
236
296
|
await recordVariableSetAuditEvent(db, {
|
|
237
297
|
grant,
|
|
@@ -244,16 +304,28 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
244
304
|
|
|
245
305
|
app.delete(`${prefix}/:variableSetId/variables/:name`, async (c) => {
|
|
246
306
|
const workspaceId = c.req.param("workspaceId")!;
|
|
247
|
-
const
|
|
307
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
308
|
+
const grant = authorization.grant;
|
|
248
309
|
requirePermission(grant, "variable-sets:write");
|
|
249
310
|
requirePermission(grant, "secrets:write");
|
|
250
311
|
const name = parseVariableName(c.req.param("name")!);
|
|
251
|
-
const variableSet = await requireVariableSetForApi(
|
|
252
|
-
|
|
312
|
+
const variableSet = await requireVariableSetForApi(db, grant, c.req.param("variableSetId")!);
|
|
313
|
+
const allowOrganization =
|
|
314
|
+
variableSet.scope === "organization" &&
|
|
315
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
316
|
+
if (variableSet.scope === "organization" && !allowOrganization) {
|
|
317
|
+
throw new HTTPException(403, {
|
|
318
|
+
message: "missing permission: account:admin",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const deleted = await deleteVariableSetVariable(db, {
|
|
322
|
+
accountId: grant.accountId,
|
|
253
323
|
workspaceId,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
324
|
+
subjectId: grant.subjectId,
|
|
325
|
+
variableSetId: variableSet.id,
|
|
326
|
+
name,
|
|
327
|
+
allowOrganization,
|
|
328
|
+
});
|
|
257
329
|
if (!deleted) {
|
|
258
330
|
throw new HTTPException(404, {
|
|
259
331
|
message: "variable set variable not found",
|