@opengeni/api-router 2.11.3 → 2.12.3-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/app.js +1 -1
- package/dist/{chunk-5X3XM6CE.js → chunk-4AQMKVQM.js} +17727 -14427
- package/dist/chunk-4AQMKVQM.js.map +1 -0
- package/dist/connection-ownership.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +4 -0
- package/dist/integrations/connect-authority.d.ts +1 -0
- package/dist/integrations/connect-callback-return.d.ts +6 -0
- package/dist/integrations/fiken.d.ts +7 -0
- package/dist/integrations/github-app-connect.d.ts +47 -0
- package/dist/integrations/github-installation-proof.d.ts +3 -0
- package/dist/integrations/github-lens-connect.d.ts +153 -0
- package/dist/integrations/google-drive.d.ts +4 -0
- package/dist/integrations/oauth-client.d.ts +4 -0
- package/dist/integrations/oauth-return-path.d.ts +1 -0
- package/dist/integrations/personal-github.d.ts +2 -0
- package/dist/integrations/provider-oauth.d.ts +11 -1
- package/dist/integrations/slack-install.d.ts +20 -0
- package/dist/integrations/social-oauth.d.ts +2 -0
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/routes/api-integrations.d.ts +14 -0
- package/dist/routes/connect.d.ts +5 -0
- package/dist/routes/external-identity-links.d.ts +6 -0
- package/dist/routes/host-mcp-bindings.d.ts +5 -0
- package/dist/sandbox/rematerialize.d.ts +1 -1
- package/dist/workspace-tool-gateway.d.ts +1 -0
- package/package.json +19 -19
- package/src/app.ts +22 -2
- package/src/connection-ownership.ts +14 -1
- package/src/index.ts +2 -0
- package/src/integrations/atlassian.ts +143 -39
- package/src/integrations/connect-authority.ts +2 -0
- package/src/integrations/connect-callback-return.ts +86 -0
- package/src/integrations/fiken.ts +222 -40
- package/src/integrations/github-app-connect.ts +389 -0
- package/src/integrations/github-installation-proof.ts +60 -0
- package/src/integrations/github-lens-connect.ts +97 -0
- package/src/integrations/google-drive.ts +152 -47
- package/src/integrations/oauth-client.ts +180 -84
- package/src/integrations/oauth-return-path.ts +18 -0
- package/src/integrations/personal-github.ts +146 -52
- package/src/integrations/provider-oauth.ts +132 -21
- package/src/integrations/slack-install.ts +83 -0
- package/src/integrations/social-oauth.ts +142 -1
- package/src/mcp/documents.ts +1 -1
- package/src/mcp/server.ts +64 -17
- package/src/routes/api-integrations.ts +74 -64
- package/src/routes/connect.ts +1869 -0
- package/src/routes/connections.ts +263 -221
- package/src/routes/documents.ts +2 -2
- package/src/routes/external-identity-links.ts +200 -0
- package/src/routes/github.ts +39 -64
- package/src/routes/host-mcp-bindings.ts +122 -0
- package/src/routes/organization-memberships.ts +23 -0
- package/src/routes/organization-sessions.ts +2 -2
- package/src/routes/personal-github.ts +8 -1
- package/src/routes/pr-review-github.ts +30 -3
- package/src/routes/scheduled-tasks.ts +29 -4
- package/src/routes/sessions.ts +61 -32
- package/src/routes/social.ts +5 -1
- package/src/routes/supergrok.ts +90 -3
- package/src/routes/workspaces.ts +16 -0
- package/src/sandbox/rematerialize.ts +33 -1
- package/src/workspace-tool-gateway.ts +136 -24
- package/dist/chunk-5X3XM6CE.js.map +0 -1
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
import { listScheduledTaskRuns, listScheduledTasks } from "@opengeni/db";
|
|
8
8
|
import type { Hono } from "hono";
|
|
9
9
|
import { HTTPException } from "hono/http-exception";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
requireAccessGrant,
|
|
12
|
+
requireAccessGrantAuthorization,
|
|
13
|
+
resolveWorkspaceCatalogSettings,
|
|
14
|
+
} from "@opengeni/core";
|
|
11
15
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
12
16
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
13
17
|
import {
|
|
@@ -35,7 +39,13 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
35
39
|
|
|
36
40
|
app.post("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
|
|
37
41
|
const workspaceId = c.req.param("workspaceId");
|
|
38
|
-
const
|
|
42
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
43
|
+
c,
|
|
44
|
+
deps,
|
|
45
|
+
workspaceId,
|
|
46
|
+
"scheduled_tasks:manage",
|
|
47
|
+
);
|
|
48
|
+
const grant = authorization.grant;
|
|
39
49
|
const rawPayload = await c.req.json();
|
|
40
50
|
const parsedPayload = CreateScheduledTaskRequest.safeParse(rawPayload);
|
|
41
51
|
if (!parsedPayload.success) {
|
|
@@ -61,6 +71,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
61
71
|
db,
|
|
62
72
|
objectStorage,
|
|
63
73
|
grant,
|
|
74
|
+
authorization,
|
|
64
75
|
payload,
|
|
65
76
|
toolsProvided: scheduledTaskToolsProvided(rawPayload),
|
|
66
77
|
sessionAuthorization: deps.sessionAuthorization,
|
|
@@ -103,7 +114,13 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
103
114
|
|
|
104
115
|
app.patch("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
|
|
105
116
|
const workspaceId = c.req.param("workspaceId");
|
|
106
|
-
const
|
|
117
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
118
|
+
c,
|
|
119
|
+
deps,
|
|
120
|
+
workspaceId,
|
|
121
|
+
"scheduled_tasks:manage",
|
|
122
|
+
);
|
|
123
|
+
const grant = authorization.grant;
|
|
107
124
|
const taskId = c.req.param("taskId");
|
|
108
125
|
const existing = await requireScheduledTaskForApi(db, workspaceId, taskId);
|
|
109
126
|
const previous = await captureScheduledTaskRestoreState(db, existing);
|
|
@@ -127,6 +144,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
127
144
|
objectStorage,
|
|
128
145
|
grant,
|
|
129
146
|
existing,
|
|
147
|
+
authorization,
|
|
130
148
|
payload,
|
|
131
149
|
toolsProvided: scheduledTaskToolsProvided(rawPayload),
|
|
132
150
|
sessionAuthorization: deps.sessionAuthorization,
|
|
@@ -151,7 +169,13 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
151
169
|
|
|
152
170
|
app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/resume", async (c) => {
|
|
153
171
|
const workspaceId = c.req.param("workspaceId");
|
|
154
|
-
const
|
|
172
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
173
|
+
c,
|
|
174
|
+
deps,
|
|
175
|
+
workspaceId,
|
|
176
|
+
"scheduled_tasks:manage",
|
|
177
|
+
);
|
|
178
|
+
const grant = authorization.grant;
|
|
155
179
|
const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
156
180
|
const previous = await captureScheduledTaskRestoreState(db, existing);
|
|
157
181
|
const catalogSettings = (
|
|
@@ -167,6 +191,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
167
191
|
grant,
|
|
168
192
|
existing,
|
|
169
193
|
payload: { status: "active" },
|
|
194
|
+
authorization,
|
|
170
195
|
sessionAuthorization: deps.sessionAuthorization,
|
|
171
196
|
authorizationSurface: "http",
|
|
172
197
|
});
|
package/src/routes/sessions.ts
CHANGED
|
@@ -49,9 +49,7 @@ import {
|
|
|
49
49
|
SessionEventReadMode,
|
|
50
50
|
SessionEventLatestClass,
|
|
51
51
|
SessionEventResultMode,
|
|
52
|
-
|
|
53
|
-
SESSION_END_USER_ID_MAX_CHARS,
|
|
54
|
-
SESSION_END_USER_SOURCE_MAX_CHARS,
|
|
52
|
+
SessionScopeSubjectId,
|
|
55
53
|
SessionEventSemanticClass,
|
|
56
54
|
SessionEventType,
|
|
57
55
|
SessionMcpServerId,
|
|
@@ -214,6 +212,10 @@ import {
|
|
|
214
212
|
GatewayRealtimeBrokerError,
|
|
215
213
|
} from "../gateway-realtime";
|
|
216
214
|
import { createXaiRealtimeConnectionSecret, XaiRealtimeBrokerError } from "../xai-realtime";
|
|
215
|
+
import {
|
|
216
|
+
prepareExternalLinkTurnAdmission,
|
|
217
|
+
externalContinuationCommitAuthorizer,
|
|
218
|
+
} from "@opengeni/core";
|
|
217
219
|
import { z, ZodError } from "zod";
|
|
218
220
|
import {
|
|
219
221
|
runConcurrentChannelAReads,
|
|
@@ -240,6 +242,7 @@ import {
|
|
|
240
242
|
requireAccessGrant,
|
|
241
243
|
requireAccessGrantAuthorization,
|
|
242
244
|
requireFreshAccessGrant,
|
|
245
|
+
hasVerifiedOwningUserAuthorization,
|
|
243
246
|
requirePermission,
|
|
244
247
|
requireSessionAuthorization,
|
|
245
248
|
requireSessionAuthorizationListScope,
|
|
@@ -639,6 +642,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
639
642
|
workspaceId,
|
|
640
643
|
payload,
|
|
641
644
|
authorization.canonicalManagedHumanSession,
|
|
645
|
+
authorization,
|
|
642
646
|
),
|
|
643
647
|
);
|
|
644
648
|
} catch (error) {
|
|
@@ -700,12 +704,12 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
700
704
|
...(query.updatedBefore ? { updatedBefore: query.updatedBefore } : {}),
|
|
701
705
|
...(query.createdFrom ? { createdFrom: query.createdFrom } : {}),
|
|
702
706
|
...(query.createdBefore ? { createdBefore: query.createdBefore } : {}),
|
|
703
|
-
...(query.
|
|
707
|
+
...(query.scopeSubjectId ? { scopeSubjectId: query.scopeSubjectId } : {}),
|
|
704
708
|
...(authorizationScope ? { authorizationScope } : {}),
|
|
705
709
|
// A managed human's own personal workspace has no membership row, so
|
|
706
710
|
// the list's removal fence must fall back to the organization-membership
|
|
707
|
-
// pointer
|
|
708
|
-
personalWorkspaceOwnerException: authorization
|
|
711
|
+
// pointer, only with verified native or external owning-user provenance.
|
|
712
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
709
713
|
});
|
|
710
714
|
} catch (error) {
|
|
711
715
|
if (error instanceof SessionListAccessError) {
|
|
@@ -1672,7 +1676,15 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1672
1676
|
const workspaceId = c.req.param("workspaceId");
|
|
1673
1677
|
const sessionId = c.req.param("sessionId");
|
|
1674
1678
|
const realtimeId = c.req.param("realtimeId");
|
|
1675
|
-
const
|
|
1679
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
1680
|
+
c,
|
|
1681
|
+
deps,
|
|
1682
|
+
workspaceId,
|
|
1683
|
+
"sessions:control",
|
|
1684
|
+
);
|
|
1685
|
+
const grant = authorization.grant;
|
|
1686
|
+
const beforeCommit = externalContinuationCommitAuthorizer(authorization);
|
|
1687
|
+
const captureLinked = prepareExternalLinkTurnAdmission(authorization);
|
|
1676
1688
|
if (
|
|
1677
1689
|
!z.string().uuid().safeParse(sessionId).success ||
|
|
1678
1690
|
!z.string().uuid().safeParse(realtimeId).success
|
|
@@ -1688,16 +1700,29 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1688
1700
|
});
|
|
1689
1701
|
}
|
|
1690
1702
|
try {
|
|
1691
|
-
const result = await withWorkspaceSessionActivityRls(db, workspaceId, async (scopedDb) =>
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1703
|
+
const result = await withWorkspaceSessionActivityRls(db, workspaceId, async (scopedDb) => {
|
|
1704
|
+
// Acquire origin authority before inference/session locks. A deferred
|
|
1705
|
+
// realtime session has no initial worker turn; capture its exact
|
|
1706
|
+
// linked actor when the ledger actually admits ordinary agent work.
|
|
1707
|
+
await beforeCommit?.(scopedDb as unknown as Database);
|
|
1708
|
+
return syncSessionRealtimeLedgerInTransaction(
|
|
1709
|
+
scopedDb,
|
|
1710
|
+
{
|
|
1711
|
+
workspaceId,
|
|
1712
|
+
sessionId,
|
|
1713
|
+
realtimeId,
|
|
1714
|
+
ownerSubjectId: grant.subjectId,
|
|
1715
|
+
...parsed.data,
|
|
1716
|
+
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
1717
|
+
},
|
|
1718
|
+
captureLinked
|
|
1719
|
+
? {
|
|
1720
|
+
afterDelegationAdmission: async ({ turnId }) =>
|
|
1721
|
+
captureLinked(scopedDb as unknown as Database, sessionId, turnId),
|
|
1722
|
+
}
|
|
1723
|
+
: {},
|
|
1724
|
+
);
|
|
1725
|
+
});
|
|
1701
1726
|
await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
|
|
1702
1727
|
c.header("cache-control", "private, no-store");
|
|
1703
1728
|
return c.json({ accepted: result.accepted, outbound: result.outbound });
|
|
@@ -1733,7 +1758,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1733
1758
|
subjectId: grant.subjectId,
|
|
1734
1759
|
sessionId,
|
|
1735
1760
|
// Same owner-only personal-workspace fallback as the list above.
|
|
1736
|
-
personalWorkspaceOwnerException: authorization
|
|
1761
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
1737
1762
|
...parsed.data,
|
|
1738
1763
|
});
|
|
1739
1764
|
if (!session) {
|
|
@@ -1790,7 +1815,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1790
1815
|
workspaceId,
|
|
1791
1816
|
subjectId: grant.subjectId,
|
|
1792
1817
|
sessionId,
|
|
1793
|
-
personalWorkspaceOwnerException: authorization
|
|
1818
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
1794
1819
|
...parsed.data,
|
|
1795
1820
|
});
|
|
1796
1821
|
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
@@ -1840,7 +1865,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1840
1865
|
workspaceId,
|
|
1841
1866
|
subjectId: grant.subjectId,
|
|
1842
1867
|
sessionId,
|
|
1843
|
-
personalWorkspaceOwnerException: authorization
|
|
1868
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
1844
1869
|
...parsed.data,
|
|
1845
1870
|
});
|
|
1846
1871
|
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
@@ -3064,6 +3089,9 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3064
3089
|
latencyMode: payload.latencyMode ?? null,
|
|
3065
3090
|
mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
|
|
3066
3091
|
connectionAuthorities: payload.connectionAuthorities,
|
|
3092
|
+
...(payload.selectedHostMcpDelegations
|
|
3093
|
+
? { selectedHostMcpDelegations: payload.selectedHostMcpDelegations }
|
|
3094
|
+
: {}),
|
|
3067
3095
|
...(payload.personalResourceAttachment
|
|
3068
3096
|
? { personalResourceAttachment: payload.personalResourceAttachment }
|
|
3069
3097
|
: {}),
|
|
@@ -3146,6 +3174,9 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3146
3174
|
latencyMode: event.payload.latencyMode ?? null,
|
|
3147
3175
|
mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
|
|
3148
3176
|
connectionAuthorities: event.payload.connectionAuthorities,
|
|
3177
|
+
...(event.payload.selectedHostMcpDelegations
|
|
3178
|
+
? { selectedHostMcpDelegations: event.payload.selectedHostMcpDelegations }
|
|
3179
|
+
: {}),
|
|
3149
3180
|
...(event.payload.personalResourceAttachment
|
|
3150
3181
|
? { personalResourceAttachment: event.payload.personalResourceAttachment }
|
|
3151
3182
|
: {}),
|
|
@@ -4740,7 +4771,7 @@ function sessionListQuery(
|
|
|
4740
4771
|
updatedBefore: Date | undefined;
|
|
4741
4772
|
createdFrom: Date | undefined;
|
|
4742
4773
|
createdBefore: Date | undefined;
|
|
4743
|
-
|
|
4774
|
+
scopeSubjectId: SessionScopeSubjectId | undefined;
|
|
4744
4775
|
hasPageFilters: boolean;
|
|
4745
4776
|
} {
|
|
4746
4777
|
const parentSessionId = query.parentSessionId;
|
|
@@ -4836,22 +4867,20 @@ function sessionListQuery(
|
|
|
4836
4867
|
}
|
|
4837
4868
|
// The opaque end-user label filter is an exact pair: one half alone is a
|
|
4838
4869
|
// client error rather than a silently unfiltered list.
|
|
4839
|
-
|
|
4840
|
-
const endUserId = query.endUserId;
|
|
4841
|
-
if ((endUserSource === undefined) !== (endUserId === undefined)) {
|
|
4870
|
+
if (query.endUserSource !== undefined || query.endUserId !== undefined) {
|
|
4842
4871
|
throw new HTTPException(400, {
|
|
4843
|
-
message: "
|
|
4872
|
+
message: "Use canonical scopeSubjectId, not an external-user label",
|
|
4844
4873
|
});
|
|
4845
4874
|
}
|
|
4846
|
-
let
|
|
4847
|
-
if (
|
|
4848
|
-
const parsedEndUser =
|
|
4875
|
+
let scopeSubjectId: SessionScopeSubjectId | undefined;
|
|
4876
|
+
if (query.scopeSubjectId !== undefined) {
|
|
4877
|
+
const parsedEndUser = SessionScopeSubjectId.safeParse(query.scopeSubjectId);
|
|
4849
4878
|
if (!parsedEndUser.success) {
|
|
4850
4879
|
throw new HTTPException(400, {
|
|
4851
|
-
message:
|
|
4880
|
+
message: "scopeSubjectId must be a canonical OpenGeni user subject",
|
|
4852
4881
|
});
|
|
4853
4882
|
}
|
|
4854
|
-
|
|
4883
|
+
scopeSubjectId = parsedEndUser.data;
|
|
4855
4884
|
}
|
|
4856
4885
|
const hasPageFilters =
|
|
4857
4886
|
originSiteId !== undefined ||
|
|
@@ -4861,7 +4890,7 @@ function sessionListQuery(
|
|
|
4861
4890
|
updatedBefore !== undefined ||
|
|
4862
4891
|
createdFrom !== undefined ||
|
|
4863
4892
|
createdBefore !== undefined ||
|
|
4864
|
-
|
|
4893
|
+
scopeSubjectId !== undefined;
|
|
4865
4894
|
if (pinsOnly && !allowCursor) {
|
|
4866
4895
|
throw new HTTPException(400, { message: 'pinsOnly requires view="page"' });
|
|
4867
4896
|
}
|
|
@@ -4895,7 +4924,7 @@ function sessionListQuery(
|
|
|
4895
4924
|
updatedBefore,
|
|
4896
4925
|
createdFrom,
|
|
4897
4926
|
createdBefore,
|
|
4898
|
-
|
|
4927
|
+
scopeSubjectId,
|
|
4899
4928
|
hasPageFilters,
|
|
4900
4929
|
};
|
|
4901
4930
|
}
|
package/src/routes/social.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { requireAccessGrant, requireAccessGrantAuthorization } from "@opengeni/c
|
|
|
19
19
|
import {
|
|
20
20
|
assertPersonalConnectionOwnerPrincipal,
|
|
21
21
|
isPersonalConnectionOwnerPrincipal,
|
|
22
|
+
requireLegacyOAuthActor,
|
|
22
23
|
} from "../connection-ownership";
|
|
23
24
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
24
25
|
import { boundedLimit } from "../http/common";
|
|
@@ -111,6 +112,7 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
111
112
|
);
|
|
112
113
|
// The contract already defaults this request to workspace ownership; an
|
|
113
114
|
// explicit personal choice still needs a human who can own it.
|
|
115
|
+
requireLegacyOAuthActor(access);
|
|
114
116
|
const personalOwnershipAllowed = isPersonalConnectionOwnerPrincipal(access);
|
|
115
117
|
if (payload.ownership === "personal") {
|
|
116
118
|
assertPersonalConnectionOwnerPrincipal(access);
|
|
@@ -140,7 +142,9 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
140
142
|
requestUrl: c.req.url,
|
|
141
143
|
},
|
|
142
144
|
);
|
|
143
|
-
return
|
|
145
|
+
return result.exactReturn
|
|
146
|
+
? new Response(null, { status: 302, headers: { location: result.redirectTo } })
|
|
147
|
+
: c.redirect(result.redirectTo, 302);
|
|
144
148
|
});
|
|
145
149
|
|
|
146
150
|
app.get("/v1/workspaces/:workspaceId/social/posts", async (c) => {
|
package/src/routes/supergrok.ts
CHANGED
|
@@ -30,10 +30,19 @@ import {
|
|
|
30
30
|
updateXaiRotationSettings,
|
|
31
31
|
upsertXaiSubscriptionCredential,
|
|
32
32
|
wakeXaiCapacityWaiters,
|
|
33
|
+
encryptEnvironmentValue,
|
|
34
|
+
decryptEnvironmentValue,
|
|
35
|
+
getWorkspaceGrant,
|
|
33
36
|
type XaiSubscriptionAccountMetadata,
|
|
34
37
|
} from "@opengeni/db";
|
|
35
38
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
36
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
getManagedSession,
|
|
41
|
+
requireAccessGrant,
|
|
42
|
+
requireAccessGrantAuthorization,
|
|
43
|
+
externalActorContinuationForAuthorization,
|
|
44
|
+
type ApiRouteDeps,
|
|
45
|
+
} from "@opengeni/core";
|
|
37
46
|
import {
|
|
38
47
|
XAI_CLIENT_VERSION,
|
|
39
48
|
XaiSubscriptionError,
|
|
@@ -50,11 +59,14 @@ import type { Context, Hono } from "hono";
|
|
|
50
59
|
import { HTTPException } from "hono/http-exception";
|
|
51
60
|
import * as z from "zod/v4";
|
|
52
61
|
import { projectClientModel } from "../model-catalog";
|
|
62
|
+
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
63
|
+
import { requireConnectOwnerAuthority } from "../integrations/connect-authority";
|
|
53
64
|
|
|
54
65
|
type XaiAuthoritySnapshot = XaiProviderAccountAuthoritySnapshotV1;
|
|
55
66
|
type ManagedCookieHuman = { subjectId: string };
|
|
56
67
|
|
|
57
68
|
type SuperGrokConnectState = {
|
|
69
|
+
externalContinuationEncrypted?: string;
|
|
58
70
|
workspaceId: string;
|
|
59
71
|
scope: "workspace" | "user";
|
|
60
72
|
subjectId: string;
|
|
@@ -132,8 +144,28 @@ async function requirePrivateHuman(
|
|
|
132
144
|
workspaceId: string,
|
|
133
145
|
): Promise<{ accountId: string; subjectId: string }> {
|
|
134
146
|
if (c.req.header("authorization")) {
|
|
147
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
148
|
+
c,
|
|
149
|
+
deps,
|
|
150
|
+
workspaceId,
|
|
151
|
+
"connections:write",
|
|
152
|
+
);
|
|
153
|
+
const external = externalActorContinuationForAuthorization(authorization);
|
|
154
|
+
if (
|
|
155
|
+
external &&
|
|
156
|
+
authorization.contextIntegrity &&
|
|
157
|
+
external.actor.effectiveSubjectId === authorization.grant.subjectId
|
|
158
|
+
) {
|
|
159
|
+
// The existing xAI user-pool domain requires an ordinary workspace
|
|
160
|
+
// membership, not just the synthetic Personal-workspace owner grant.
|
|
161
|
+
if (!(await getWorkspaceGrant(deps.db, authorization.grant.subjectId, workspaceId)))
|
|
162
|
+
throw new HTTPException(409, {
|
|
163
|
+
message: "Private SuperGrok accounts require membership in an ordinary workspace",
|
|
164
|
+
});
|
|
165
|
+
return { accountId: authorization.grant.accountId, subjectId: authorization.grant.subjectId };
|
|
166
|
+
}
|
|
135
167
|
throw new HTTPException(403, {
|
|
136
|
-
message: "
|
|
168
|
+
message: "Private SuperGrok accounts require a verified owning user",
|
|
137
169
|
});
|
|
138
170
|
}
|
|
139
171
|
const human = await managedCookieHuman(c, deps);
|
|
@@ -148,6 +180,10 @@ async function requirePrivateHuman(
|
|
|
148
180
|
message: "managed browser identity mismatch",
|
|
149
181
|
});
|
|
150
182
|
}
|
|
183
|
+
if (!(await getWorkspaceGrant(deps.db, grant.subjectId, workspaceId)))
|
|
184
|
+
throw new HTTPException(409, {
|
|
185
|
+
message: "Private SuperGrok accounts require membership in an ordinary workspace",
|
|
186
|
+
});
|
|
151
187
|
return { accountId: grant.accountId, subjectId: grant.subjectId };
|
|
152
188
|
}
|
|
153
189
|
|
|
@@ -160,7 +196,9 @@ export async function requireScopeMutation(
|
|
|
160
196
|
if (scope === "organization")
|
|
161
197
|
throw new HTTPException(409, { message: "Manage this subscription in organization settings" });
|
|
162
198
|
if (scope === "user") {
|
|
163
|
-
|
|
199
|
+
// Server-side external-user assertions do not use browser cookies. Ordinary
|
|
200
|
+
// bearers remain rejected by requirePrivateHuman; native CSRF is unchanged.
|
|
201
|
+
if (!c.req.header("authorization")) requireSameOriginBrowserMutation(c, deps);
|
|
164
202
|
return await requirePrivateHuman(c, deps, workspaceId);
|
|
165
203
|
}
|
|
166
204
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
@@ -509,6 +547,16 @@ export function registerSuperGrokRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
509
547
|
const parsed = connectStartBody.safeParse(await c.req.json().catch(() => ({})));
|
|
510
548
|
if (!parsed.success) throw new HTTPException(400, { message: "invalid SuperGrok scope" });
|
|
511
549
|
const authority = await requireScopeMutation(c, deps, workspaceId, parsed.data.scope);
|
|
550
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
551
|
+
c,
|
|
552
|
+
deps,
|
|
553
|
+
workspaceId,
|
|
554
|
+
parsed.data.scope === "workspace" ? "workspace:admin" : "connections:write",
|
|
555
|
+
);
|
|
556
|
+
const continuation = externalActorContinuationForAuthorization(authorization);
|
|
557
|
+
const encryptionKey = environmentsEncryptionKeyBytes(deps.settings);
|
|
558
|
+
if (continuation && !encryptionKey)
|
|
559
|
+
throw new HTTPException(503, { message: "Credential encryption is unavailable" });
|
|
512
560
|
try {
|
|
513
561
|
const start = await requestXaiDeviceCode({
|
|
514
562
|
fetch: (deps.xaiFetch ?? fetch) as XaiFetch,
|
|
@@ -528,6 +576,14 @@ export function registerSuperGrokRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
528
576
|
deviceCode: start.deviceCode,
|
|
529
577
|
intervalSeconds: start.intervalSeconds,
|
|
530
578
|
expiresAt,
|
|
579
|
+
...(continuation
|
|
580
|
+
? {
|
|
581
|
+
externalContinuationEncrypted: encryptEnvironmentValue(
|
|
582
|
+
encryptionKey!,
|
|
583
|
+
JSON.stringify(continuation),
|
|
584
|
+
),
|
|
585
|
+
}
|
|
586
|
+
: {}),
|
|
531
587
|
}),
|
|
532
588
|
});
|
|
533
589
|
} catch (error) {
|
|
@@ -563,6 +619,30 @@ export function registerSuperGrokRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
563
619
|
message: "SuperGrok connect identity changed",
|
|
564
620
|
});
|
|
565
621
|
}
|
|
622
|
+
const originKey = environmentsEncryptionKeyBytes(deps.settings);
|
|
623
|
+
const origin =
|
|
624
|
+
state.externalContinuationEncrypted && originKey
|
|
625
|
+
? ExternalActorContinuation.parse(
|
|
626
|
+
JSON.parse(decryptEnvironmentValue(originKey, state.externalContinuationEncrypted)),
|
|
627
|
+
)
|
|
628
|
+
: null;
|
|
629
|
+
if (state.externalContinuationEncrypted && !origin)
|
|
630
|
+
throw new HTTPException(503, { message: "Connection origin unavailable" });
|
|
631
|
+
const requireOrigin = async () => {
|
|
632
|
+
if (origin)
|
|
633
|
+
await requireConnectOwnerAuthority(
|
|
634
|
+
deps.db,
|
|
635
|
+
{
|
|
636
|
+
accountId: authority.accountId,
|
|
637
|
+
workspaceId,
|
|
638
|
+
subjectId: authority.subjectId,
|
|
639
|
+
externalContinuation: origin,
|
|
640
|
+
},
|
|
641
|
+
state.scope === "workspace" ? "workspace:admin" : "connections:write",
|
|
642
|
+
origin,
|
|
643
|
+
);
|
|
644
|
+
};
|
|
645
|
+
await requireOrigin();
|
|
566
646
|
if (Math.floor(Date.now() / 1_000) >= state.expiresAt) {
|
|
567
647
|
return c.json({ status: "expired" as const });
|
|
568
648
|
}
|
|
@@ -579,6 +659,13 @@ export function registerSuperGrokRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
579
659
|
phase = "token_identity";
|
|
580
660
|
const identity = xaiIdentityFromDeviceTokens(poll.tokens);
|
|
581
661
|
phase = "credential_persist";
|
|
662
|
+
await requireOrigin();
|
|
663
|
+
const liveAuthority = await requireScopeMutation(c, deps, workspaceId, state.scope);
|
|
664
|
+
if (
|
|
665
|
+
liveAuthority.accountId !== authority.accountId ||
|
|
666
|
+
liveAuthority.subjectId !== authority.subjectId
|
|
667
|
+
)
|
|
668
|
+
throw new HTTPException(403, { message: "SuperGrok connection identity changed" });
|
|
582
669
|
const encryptionKey = environmentsEncryptionKeyBytes(deps.settings);
|
|
583
670
|
if (!encryptionKey) {
|
|
584
671
|
throw new HTTPException(500, {
|
package/src/routes/workspaces.ts
CHANGED
|
@@ -89,6 +89,8 @@ import {
|
|
|
89
89
|
accountScopedApiKeyWorkspaceAuthority,
|
|
90
90
|
hasPermission,
|
|
91
91
|
requireAccessContext,
|
|
92
|
+
listExternalActorWorkspaces,
|
|
93
|
+
addExternalWorkspaceMemberForRequest,
|
|
92
94
|
requireAccessGrant,
|
|
93
95
|
requireWorkspaceSettingsGrant,
|
|
94
96
|
requireFreshAccessGrant,
|
|
@@ -200,12 +202,25 @@ export function workspaceUpdateRequestsAccountTransfer(value: unknown): boolean
|
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
205
|
+
app.post("/v1/workspaces/:workspaceId/external-members", async (c) => {
|
|
206
|
+
return c.json(
|
|
207
|
+
await addExternalWorkspaceMemberForRequest(
|
|
208
|
+
c,
|
|
209
|
+
deps,
|
|
210
|
+
c.req.param("workspaceId"),
|
|
211
|
+
await c.req.json(),
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
});
|
|
203
215
|
app.get("/v1/access/me", async (c) => {
|
|
204
216
|
return c.json(await requireAccessContext(c, deps));
|
|
205
217
|
});
|
|
206
218
|
|
|
207
219
|
app.get("/v1/workspaces", async (c) => {
|
|
208
220
|
const context = await requireAccessContext(c, deps);
|
|
221
|
+
const externalWorkspaces = await listExternalActorWorkspaces(context, deps);
|
|
222
|
+
if (externalWorkspaces !== null)
|
|
223
|
+
return c.json(externalWorkspaces.map((workspace) => Workspace.parse(workspace)));
|
|
209
224
|
const accountScopedAuthority = accountScopedApiKeyWorkspaceAuthority(context);
|
|
210
225
|
if (
|
|
211
226
|
accountScopedAuthority &&
|
|
@@ -243,6 +258,7 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
243
258
|
requireAccountPermission(context, payload.accountId, "workspace:create");
|
|
244
259
|
try {
|
|
245
260
|
const existing = await findWorkspaceByExternalIdentity(deps.db, {
|
|
261
|
+
accountId: payload.accountId,
|
|
246
262
|
externalSource: payload.externalSource,
|
|
247
263
|
externalId: payload.externalId,
|
|
248
264
|
});
|
|
@@ -32,7 +32,11 @@ import {
|
|
|
32
32
|
type EstablishedSandboxSession,
|
|
33
33
|
type WorkspaceArchiveDescriptor,
|
|
34
34
|
} from "@opengeni/runtime/sandbox";
|
|
35
|
-
import
|
|
35
|
+
import {
|
|
36
|
+
downloadWorkspaceArchiveSpool,
|
|
37
|
+
WorkspaceArchiveStorageError,
|
|
38
|
+
type ObjectStorage,
|
|
39
|
+
} from "@opengeni/storage";
|
|
36
40
|
|
|
37
41
|
function hasWorkspaceArchive(envelope: Record<string, unknown> | null): boolean {
|
|
38
42
|
const sessionState =
|
|
@@ -73,6 +77,14 @@ async function materializeArchiveObjectRef(
|
|
|
73
77
|
"workspace archive object storage is not configured",
|
|
74
78
|
);
|
|
75
79
|
}
|
|
80
|
+
const descriptor = parseWorkspaceArchiveDescriptor(sessionState.workspaceArchiveMeta);
|
|
81
|
+
if (
|
|
82
|
+
process.platform === "linux" &&
|
|
83
|
+
descriptor?.version === 1 &&
|
|
84
|
+
descriptor.workspace.projection === "sdk_local_archive_v1"
|
|
85
|
+
) {
|
|
86
|
+
return envelope;
|
|
87
|
+
}
|
|
76
88
|
return {
|
|
77
89
|
...envelope,
|
|
78
90
|
sessionState: await inlineWorkspaceArchiveForRestore(sessionState, (key) =>
|
|
@@ -209,6 +221,26 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
209
221
|
established = await establishSandboxSessionFromEnvelope(input.settings, hydrateEnvelope, {
|
|
210
222
|
sessionId: input.sessionId,
|
|
211
223
|
recovery: "create-or-restore",
|
|
224
|
+
...(input.objectStorage
|
|
225
|
+
? {
|
|
226
|
+
loadHostWorkspaceArchive: async (ref) => {
|
|
227
|
+
try {
|
|
228
|
+
return await downloadWorkspaceArchiveSpool(input.objectStorage!, ref.key, {
|
|
229
|
+
bytes: ref.bytes,
|
|
230
|
+
sha256: ref.sha256,
|
|
231
|
+
});
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof WorkspaceArchiveStorageError) {
|
|
234
|
+
throw new WorkspaceArchiveIntegrityError(error.code, error.message, {
|
|
235
|
+
retryable: error.retryable,
|
|
236
|
+
cause: error,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
: {}),
|
|
212
244
|
backendOverride: input.backend as never,
|
|
213
245
|
environment: input.environment,
|
|
214
246
|
onSandboxCreated: async (created) => {
|