@opengeni/api-router 3.0.1 → 4.0.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/admission-trace.d.ts +8 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
- package/dist/auth/managed-auth.d.ts +1 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
- package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
- package/dist/connection-ownership.d.ts +1 -2
- package/dist/fatal-process-boundary.d.ts +1 -1
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +5 -65
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/oauth-client.d.ts +119 -11
- package/dist/integrations/oauth-profiles.d.ts +8 -13
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
- package/dist/mcp-oauth.d.ts +22 -1
- package/dist/routes/api-integrations.d.ts +1 -0
- package/dist/routes/codex.d.ts +28 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/dist/routes/sessions.d.ts +24 -2
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/workspace-tool-gateway.d.ts +3 -1
- package/package.json +15 -15
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +141 -94
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-session-adapter.ts +11 -0
- package/src/auth/managed-auth.ts +39 -2
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/connection-ownership.ts +2 -9
- package/src/editable-artifact-production.ts +6 -0
- package/src/fatal-process-boundary.ts +17 -2
- package/src/index.ts +3 -4
- package/src/integrations/atlassian.ts +184 -76
- package/src/integrations/fiken.ts +57 -35
- package/src/integrations/github-app-connect.ts +51 -15
- package/src/integrations/github-lens-connect.ts +7 -28
- package/src/integrations/google-drive.ts +296 -125
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/oauth-client.ts +231 -132
- package/src/integrations/oauth-profiles.ts +10 -45
- package/src/integrations/personal-github.ts +55 -31
- package/src/integrations/provider-oauth.ts +57 -82
- package/src/integrations/social-oauth.ts +83 -56
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/editable-artifacts.ts +7 -2
- package/src/mcp/first-party-tool-permissions.ts +1 -0
- package/src/mcp/knowledge-entries.ts +25 -8
- package/src/mcp/server.ts +135 -43
- package/src/mcp-oauth.ts +336 -34
- package/src/routes/api-integrations.ts +116 -4
- package/src/routes/automations.ts +5 -13
- package/src/routes/billing.ts +52 -0
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +51 -1
- package/src/routes/codex.ts +4 -2
- package/src/routes/connect.ts +90 -31
- package/src/routes/connections.ts +487 -307
- package/src/routes/files.ts +21 -12
- package/src/routes/github.ts +48 -19
- package/src/routes/insights.ts +17 -7
- package/src/routes/integration-facets.ts +66 -25
- package/src/routes/knowledge.ts +48 -1
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/organization-integration-policy.ts +113 -0
- package/src/routes/organization-memberships.ts +22 -2
- package/src/routes/plugins.ts +42 -4
- package/src/routes/pr-review-github.ts +67 -57
- package/src/routes/pr-review.ts +74 -71
- package/src/routes/rigs.ts +32 -13
- package/src/routes/scheduled-tasks.ts +6 -6
- package/src/routes/sessions.ts +233 -11
- package/src/routes/skill-content.ts +19 -3
- package/src/routes/social.ts +24 -13
- package/src/sandbox/channel-a.ts +7 -1
- package/src/sandbox/rematerialize.ts +2 -1
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/workspace-tool-gateway.ts +31 -31
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
|
@@ -88,6 +88,7 @@ export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
88
88
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
89
89
|
requirePermission(grant, "secrets:write");
|
|
90
90
|
const request = CreateAutomationSourceRequest.parse(await c.req.json());
|
|
91
|
+
assertGenericTriggerMutable(request);
|
|
91
92
|
requireAutomationAdapter(request.adapterId).validateSourceConfiguration(request.configuration);
|
|
92
93
|
const key = requireEncryptionKey(deps);
|
|
93
94
|
const source = await createAutomationSource(deps.db, {
|
|
@@ -176,11 +177,6 @@ export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
176
177
|
grant,
|
|
177
178
|
request.sessionTemplate.bundledSkillIds,
|
|
178
179
|
);
|
|
179
|
-
if (request.packInstallationId || request.packTemplateId) {
|
|
180
|
-
throw new HTTPException(409, {
|
|
181
|
-
message: "Pack-owned automation triggers must be created through their Pack setup API",
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
180
|
const source = await requireSource(deps, grant.accountId, workspaceId, request.sourceId);
|
|
185
181
|
assertGenericSourceMutable(source);
|
|
186
182
|
if (source.status !== "active") {
|
|
@@ -667,17 +663,13 @@ async function requireSource(
|
|
|
667
663
|
}
|
|
668
664
|
|
|
669
665
|
function assertGenericSourceMutable(source: AutomationSourceSecret): void {
|
|
670
|
-
|
|
671
|
-
throw new HTTPException(409, {
|
|
672
|
-
message: "Pack-owned automation sources must be managed through their Pack setup API",
|
|
673
|
-
});
|
|
674
|
-
}
|
|
666
|
+
assertGenericTriggerMutable(source);
|
|
675
667
|
}
|
|
676
668
|
|
|
677
|
-
function assertGenericTriggerMutable(trigger: {
|
|
678
|
-
if (trigger.
|
|
669
|
+
function assertGenericTriggerMutable(trigger: { adapterId: string }): void {
|
|
670
|
+
if (trigger.adapterId === "source-control.pull-request.v1") {
|
|
679
671
|
throw new HTTPException(409, {
|
|
680
|
-
message: "
|
|
672
|
+
message: "PR Review automations must be managed through the PR Review setup API",
|
|
681
673
|
});
|
|
682
674
|
}
|
|
683
675
|
}
|
package/src/routes/billing.ts
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
CreateBillingPortalResponse,
|
|
4
4
|
CreateCheckoutRequest,
|
|
5
5
|
CreateCheckoutResponse,
|
|
6
|
+
OrganizationUsageQuery,
|
|
7
|
+
OrganizationUsageWorkspacePageQuery,
|
|
6
8
|
type AccessContext,
|
|
7
9
|
type Permission,
|
|
8
10
|
} from "@opengeni/contracts";
|
|
@@ -14,6 +16,9 @@ import {
|
|
|
14
16
|
hasCreditLedgerEntry,
|
|
15
17
|
isStripeWebhookProcessed,
|
|
16
18
|
listUsageEvents,
|
|
19
|
+
getOrganizationUsageSummary,
|
|
20
|
+
getOrganizationUsageWorkspacePage,
|
|
21
|
+
withSessionRlsActorContext,
|
|
17
22
|
getManagedAccount,
|
|
18
23
|
markStripeWebhookProcessed,
|
|
19
24
|
recordStripeWebhookEvent,
|
|
@@ -24,6 +29,7 @@ import { HTTPException } from "hono/http-exception";
|
|
|
24
29
|
import Stripe from "stripe";
|
|
25
30
|
import { requireAccessContext } from "@opengeni/core";
|
|
26
31
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
32
|
+
import { withAccessGrantSessionRlsContext } from "../access-grant-rls";
|
|
27
33
|
|
|
28
34
|
export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
29
35
|
app.get("/v1/billing", async (c) => {
|
|
@@ -57,6 +63,33 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
57
63
|
});
|
|
58
64
|
});
|
|
59
65
|
|
|
66
|
+
app.get("/v1/billing/usage-summary", async (c) => {
|
|
67
|
+
const context = await requireAccessContext(c, deps);
|
|
68
|
+
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
69
|
+
const parsed = OrganizationUsageQuery.safeParse(c.req.query());
|
|
70
|
+
if (!parsed.success) {
|
|
71
|
+
throw new HTTPException(400, {
|
|
72
|
+
message: parsed.error.issues[0]?.message ?? "invalid usage query",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return await withBillingUsageActor(deps, context, accountId, async () =>
|
|
76
|
+
c.json(await getOrganizationUsageSummary(deps.db, { accountId, ...parsed.data })),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
app.get("/v1/billing/usage-workspaces", async (c) => {
|
|
81
|
+
const context = await requireAccessContext(c, deps);
|
|
82
|
+
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
83
|
+
const parsed = OrganizationUsageWorkspacePageQuery.safeParse(c.req.query());
|
|
84
|
+
if (!parsed.success)
|
|
85
|
+
throw new HTTPException(400, {
|
|
86
|
+
message: parsed.error.issues[0]?.message ?? "invalid usage page query",
|
|
87
|
+
});
|
|
88
|
+
return await withBillingUsageActor(deps, context, accountId, async () =>
|
|
89
|
+
c.json(await getOrganizationUsageWorkspacePage(deps.db, { accountId, ...parsed.data })),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
60
93
|
app.get("/v1/billing/entitlements", async (c) => {
|
|
61
94
|
const context = await requireAccessContext(c, deps);
|
|
62
95
|
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
@@ -638,6 +671,25 @@ export function stripeCustomerProvider(
|
|
|
638
671
|
: "stripe:test";
|
|
639
672
|
}
|
|
640
673
|
|
|
674
|
+
/** Billing routes do not traverse the workspace actor middleware. Always bind
|
|
675
|
+
* these reads explicitly; only a revalidated live attempt may supply a human
|
|
676
|
+
* initiator. Neither query parameters nor serviceInitiator claims are proof. */
|
|
677
|
+
async function withBillingUsageActor<T>(
|
|
678
|
+
deps: ApiRouteDeps,
|
|
679
|
+
context: AccessContext,
|
|
680
|
+
accountId: string,
|
|
681
|
+
read: () => Promise<T>,
|
|
682
|
+
): Promise<T> {
|
|
683
|
+
const attemptGrant = context.workspaceGrants.find(
|
|
684
|
+
(grant) =>
|
|
685
|
+
grant.accountId === accountId &&
|
|
686
|
+
grant.subjectId === context.subjectId &&
|
|
687
|
+
grant.principalKind === "agent_attempt",
|
|
688
|
+
);
|
|
689
|
+
if (attemptGrant) return await withAccessGrantSessionRlsContext(deps, attemptGrant, read);
|
|
690
|
+
return await withSessionRlsActorContext({ subjectId: context.subjectId }, read);
|
|
691
|
+
}
|
|
692
|
+
|
|
641
693
|
function requireSelectedAccount(
|
|
642
694
|
context: AccessContext,
|
|
643
695
|
requested: string | undefined,
|
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
import type { Context, Hono } from "hono";
|
|
24
24
|
import { HTTPException } from "hono/http-exception";
|
|
25
25
|
import { z } from "zod";
|
|
26
|
+
import { admitManagedSignInMutation } from "./managed-sign-in-methods";
|
|
27
|
+
import { managedAuthSecretRequestDigest } from "@opengeni/core/managed-auth-session-sets";
|
|
28
|
+
import { deliverManagedSignInNotification } from "../auth/managed-sign-in-notifications";
|
|
26
29
|
|
|
27
30
|
const BindingId = z.string().uuid();
|
|
28
31
|
|
|
@@ -86,6 +89,7 @@ async function mutate(
|
|
|
86
89
|
operationId: string;
|
|
87
90
|
expectedIdentityRevision: number;
|
|
88
91
|
reason: string;
|
|
92
|
+
verifiedManagedRecovery?: boolean;
|
|
89
93
|
},
|
|
90
94
|
): Promise<Response> {
|
|
91
95
|
const identity = await requestIdentity(context, deps);
|
|
@@ -104,10 +108,27 @@ async function mutate(
|
|
|
104
108
|
...(input.providerId ? { providerId: input.providerId } : {}),
|
|
105
109
|
...(input.providerAccountId ? { providerAccountId: input.providerAccountId } : {}),
|
|
106
110
|
reason: input.reason,
|
|
111
|
+
...(input.verifiedManagedRecovery
|
|
112
|
+
? {
|
|
113
|
+
verifiedRecovery: {
|
|
114
|
+
authSessionId: identity.authSessionId,
|
|
115
|
+
requestDigest: managedAuthSecretRequestDigest(
|
|
116
|
+
deps.settings.betterAuthSecret!,
|
|
117
|
+
input,
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
: {}),
|
|
107
122
|
...(actorFence ? { actorFence } : {}),
|
|
108
123
|
}),
|
|
109
124
|
);
|
|
110
125
|
if (actorFence) markManagedAuthRequestActorTransitionApplied(context.req.raw);
|
|
126
|
+
if (input.verifiedManagedRecovery)
|
|
127
|
+
await deliverManagedSignInNotification(
|
|
128
|
+
deps.db,
|
|
129
|
+
deps.managedEmailTransport,
|
|
130
|
+
input.operationId,
|
|
131
|
+
);
|
|
111
132
|
return context.json(response);
|
|
112
133
|
} catch (error) {
|
|
113
134
|
return identityError(context, error);
|
|
@@ -132,6 +153,11 @@ export function registerCanonicalHumanIdentityRoutes(app: Hono, deps: ApiRouteDe
|
|
|
132
153
|
|
|
133
154
|
app.post(`${base}/login-bindings`, async (context) => {
|
|
134
155
|
const request = await parseBody(context, LinkCanonicalHumanLoginBindingRequest);
|
|
156
|
+
if (["google", "github", "credential"].includes(request.providerId.trim().toLowerCase())) {
|
|
157
|
+
throw new HTTPException(403, {
|
|
158
|
+
message: "Use personal sign-in method settings with fresh provider proof",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
135
161
|
return await mutate(context, deps, {
|
|
136
162
|
operationType: "link",
|
|
137
163
|
...request,
|
|
@@ -141,6 +167,19 @@ export function registerCanonicalHumanIdentityRoutes(app: Hono, deps: ApiRouteDe
|
|
|
141
167
|
|
|
142
168
|
app.delete(`${base}/login-bindings/:bindingId`, async (context) => {
|
|
143
169
|
const request = await parseBody(context, CanonicalHumanBindingOperationRequest);
|
|
170
|
+
const actor = await requestIdentity(context, deps);
|
|
171
|
+
const projection = await getCanonicalHumanIdentityProjection(deps.db, actor.authUserId);
|
|
172
|
+
if (
|
|
173
|
+
projection.loginBindings.some(
|
|
174
|
+
(binding) =>
|
|
175
|
+
binding.id === bindingId(context) &&
|
|
176
|
+
["google", "github", "credential"].includes(binding.providerId),
|
|
177
|
+
)
|
|
178
|
+
) {
|
|
179
|
+
throw new HTTPException(403, {
|
|
180
|
+
message: "Use personal sign-in method settings with last-method protection",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
144
183
|
return await mutate(context, deps, {
|
|
145
184
|
operationType: "unlink",
|
|
146
185
|
bindingId: bindingId(context),
|
|
@@ -151,6 +190,19 @@ export function registerCanonicalHumanIdentityRoutes(app: Hono, deps: ApiRouteDe
|
|
|
151
190
|
|
|
152
191
|
app.post(`${base}/login-bindings/:bindingId/recovery`, async (context) => {
|
|
153
192
|
const request = await parseBody(context, CanonicalHumanBindingOperationRequest);
|
|
193
|
+
const actor = await requestIdentity(context, deps);
|
|
194
|
+
const projection = await getCanonicalHumanIdentityProjection(deps.db, actor.authUserId);
|
|
195
|
+
if (
|
|
196
|
+
projection.loginBindings.some(
|
|
197
|
+
(binding) =>
|
|
198
|
+
binding.id === bindingId(context) &&
|
|
199
|
+
["google", "github", "credential"].includes(binding.providerId),
|
|
200
|
+
)
|
|
201
|
+
) {
|
|
202
|
+
throw new HTTPException(403, {
|
|
203
|
+
message: "Managed-provider recovery cannot be initiated through raw binding management",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
154
206
|
return await mutate(context, deps, {
|
|
155
207
|
operationType: "begin_recovery",
|
|
156
208
|
bindingId: bindingId(context),
|
|
@@ -161,11 +213,20 @@ export function registerCanonicalHumanIdentityRoutes(app: Hono, deps: ApiRouteDe
|
|
|
161
213
|
|
|
162
214
|
app.post(`${base}/login-bindings/:bindingId/recovery/complete`, async (context) => {
|
|
163
215
|
const request = await parseBody(context, CanonicalHumanBindingOperationRequest);
|
|
216
|
+
const actor = await requestIdentity(context, deps);
|
|
217
|
+
const projection = await getCanonicalHumanIdentityProjection(deps.db, actor.authUserId);
|
|
218
|
+
const managed = projection.loginBindings.some(
|
|
219
|
+
(binding) =>
|
|
220
|
+
binding.id === bindingId(context) &&
|
|
221
|
+
["google", "github", "credential"].includes(binding.providerId),
|
|
222
|
+
);
|
|
223
|
+
if (managed) await admitManagedSignInMutation(context, deps);
|
|
164
224
|
return await mutate(context, deps, {
|
|
165
225
|
operationType: "recover",
|
|
166
226
|
bindingId: bindingId(context),
|
|
167
227
|
...request,
|
|
168
228
|
operationId: request.operationId ?? randomUUID(),
|
|
229
|
+
...(managed ? { verifiedManagedRecovery: true } : {}),
|
|
169
230
|
});
|
|
170
231
|
});
|
|
171
232
|
}
|
|
@@ -3,9 +3,16 @@ import {
|
|
|
3
3
|
CreateCapabilityCatalogItemRequest,
|
|
4
4
|
DiscoverMcpCapabilitiesResponse,
|
|
5
5
|
EnableCapabilityRequest,
|
|
6
|
+
ConnectorToolPermissionsResponse,
|
|
7
|
+
UpdateConnectorToolPermissionsRequest,
|
|
6
8
|
} from "@opengeni/contracts";
|
|
7
9
|
import type { Hono } from "hono";
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
requireAccessGrant,
|
|
12
|
+
requireAccessGrantAuthorization,
|
|
13
|
+
getConnectorToolPermissions,
|
|
14
|
+
updateConnectorToolPermissions,
|
|
15
|
+
} from "@opengeni/core";
|
|
9
16
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
10
17
|
import {
|
|
11
18
|
buildCapabilityCatalog,
|
|
@@ -15,6 +22,7 @@ import {
|
|
|
15
22
|
enableCapability,
|
|
16
23
|
officialMcpRegistryUrl,
|
|
17
24
|
} from "@opengeni/core";
|
|
25
|
+
import { isPersonalConnectionOwnerPrincipal } from "../connection-ownership";
|
|
18
26
|
import { boundedLimit } from "../http/common";
|
|
19
27
|
import { z } from "zod";
|
|
20
28
|
import pluginSnapshot from "../../../../data/catalog/plugins-snapshot.json";
|
|
@@ -110,6 +118,48 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
110
118
|
);
|
|
111
119
|
});
|
|
112
120
|
|
|
121
|
+
app.get("/v1/workspaces/:workspaceId/capabilities/:capabilityId/tool-permissions", async (c) => {
|
|
122
|
+
const workspaceId = c.req.param("workspaceId");
|
|
123
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:read");
|
|
124
|
+
return c.json(
|
|
125
|
+
ConnectorToolPermissionsResponse.parse(
|
|
126
|
+
await getConnectorToolPermissions({
|
|
127
|
+
db,
|
|
128
|
+
settings,
|
|
129
|
+
workspaceId,
|
|
130
|
+
grant: access.grant,
|
|
131
|
+
capabilityId: decodeURIComponent(c.req.param("capabilityId")),
|
|
132
|
+
personalOwnerVerified: isPersonalConnectionOwnerPrincipal(access),
|
|
133
|
+
}),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
app.patch(
|
|
139
|
+
"/v1/workspaces/:workspaceId/capabilities/:capabilityId/tool-permissions",
|
|
140
|
+
async (c) => {
|
|
141
|
+
const workspaceId = c.req.param("workspaceId");
|
|
142
|
+
const access = await requireAccessGrantAuthorization(
|
|
143
|
+
c,
|
|
144
|
+
deps,
|
|
145
|
+
workspaceId,
|
|
146
|
+
"capabilities:manage",
|
|
147
|
+
);
|
|
148
|
+
const payload = UpdateConnectorToolPermissionsRequest.safeParse(await c.req.json());
|
|
149
|
+
if (!payload.success) return c.json({ error: "Invalid connector permission target" }, 400);
|
|
150
|
+
await updateConnectorToolPermissions({
|
|
151
|
+
db,
|
|
152
|
+
settings,
|
|
153
|
+
workspaceId,
|
|
154
|
+
grant: access.grant,
|
|
155
|
+
capabilityId: decodeURIComponent(c.req.param("capabilityId")),
|
|
156
|
+
personalOwnerVerified: isPersonalConnectionOwnerPrincipal(access),
|
|
157
|
+
payload: payload.data,
|
|
158
|
+
});
|
|
159
|
+
return c.json({ saved: true });
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
113
163
|
app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/enable", async (c) => {
|
|
114
164
|
const workspaceId = c.req.param("workspaceId");
|
|
115
165
|
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
package/src/routes/codex.ts
CHANGED
|
@@ -84,7 +84,7 @@ const CODEX_PROVIDER_LABEL = "Codex subscription · no credits";
|
|
|
84
84
|
// The wire shape for one Codex account (metadata only; never the secret column).
|
|
85
85
|
// P2: fiveHour/weekly ride along, built from the CACHED usage columns (zero
|
|
86
86
|
// provider calls, zero decrypts) so the bars render instantly off this read.
|
|
87
|
-
function codexAccountJson(
|
|
87
|
+
export function codexAccountJson(
|
|
88
88
|
row: CodexAccountStatus,
|
|
89
89
|
options: {
|
|
90
90
|
appsCredentialId?: string | null;
|
|
@@ -1214,7 +1214,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1214
1214
|
accountId: grant.accountId,
|
|
1215
1215
|
workspaceId,
|
|
1216
1216
|
subjectId: grant.subjectId,
|
|
1217
|
-
|
|
1217
|
+
// Connecting local capacity does not override an explicit source choice.
|
|
1218
|
+
// Automatic naturally prefers the newly connected workspace pool.
|
|
1219
|
+
mode: sourceBeforeConnect.mode,
|
|
1218
1220
|
effectiveSourceBeforeMutation: sourceBeforeConnect.effectiveSource,
|
|
1219
1221
|
});
|
|
1220
1222
|
const rotation = await getCodexRotationSettings(tx, workspaceId);
|
package/src/routes/connect.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
2
2
|
import type { Hono } from "hono";
|
|
3
3
|
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { getConnectBeginReplay } from "@opengeni/db/connect-attempts";
|
|
4
5
|
import {
|
|
5
6
|
AdvanceConnectRequest,
|
|
6
7
|
BeginConnectRequest,
|
|
@@ -12,15 +13,15 @@ import {
|
|
|
12
13
|
API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
|
|
13
14
|
InstallApiIntegrationRequest,
|
|
14
15
|
stableJson,
|
|
16
|
+
assertOrganizationIntegrationAllowed,
|
|
15
17
|
OPENGENI_PERSONAL_SLACK_MCP_URL,
|
|
16
|
-
OPENGENI_PR_REVIEW_PACK_ID,
|
|
17
18
|
} from "@opengeni/contracts";
|
|
18
19
|
import {
|
|
19
20
|
CORE_INTEGRATION_DEFINITIONS,
|
|
20
21
|
createPinnedIntegrationTransport,
|
|
21
22
|
} from "@opengeni/capabilities";
|
|
22
23
|
import {
|
|
23
|
-
beginConnectAttempt,
|
|
24
|
+
beginConnectAttempt as persistConnectAttempt,
|
|
24
25
|
getConnectAttempt,
|
|
25
26
|
listPendingConnectAttempts,
|
|
26
27
|
withWorkspaceSubjectRls,
|
|
@@ -30,9 +31,8 @@ import {
|
|
|
30
31
|
createConnection,
|
|
31
32
|
updateConnection,
|
|
32
33
|
encryptEnvironmentValue,
|
|
33
|
-
|
|
34
|
+
normalizedCredentialHeaders,
|
|
34
35
|
listGitHubInstallationAccessForWorkspace,
|
|
35
|
-
getPackInstallation,
|
|
36
36
|
listPrReviewAppRegistrations,
|
|
37
37
|
listSocialConnections,
|
|
38
38
|
getSocialConnection,
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
isOpenGeniSlackBotConnection,
|
|
47
47
|
prepareCapabilityEnable,
|
|
48
48
|
buildCapabilityCatalog,
|
|
49
|
+
integrationKeyForConnectProvider,
|
|
49
50
|
type ApiRouteDeps,
|
|
50
51
|
} from "@opengeni/core";
|
|
51
52
|
import { isPersonalConnectionOwnerPrincipal } from "../connection-ownership";
|
|
@@ -74,6 +75,7 @@ import {
|
|
|
74
75
|
} from "../integrations/provider-oauth";
|
|
75
76
|
import { resolveForRoute, validatedIntegrationInstallInput } from "./api-integrations";
|
|
76
77
|
import { executeConnectOperation } from "@opengeni/core";
|
|
78
|
+
import { withOrganizationIntegrationPolicyFence } from "@opengeni/db/organization-integration-policy";
|
|
77
79
|
import { startMcpOAuth, requireIntegrationsStateSecret } from "../integrations/oauth-client";
|
|
78
80
|
import { OFFICIAL_GMAIL_MCP_URL } from "../integrations/oauth-profiles";
|
|
79
81
|
import { z } from "zod";
|
|
@@ -101,13 +103,13 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
101
103
|
"workspace:read",
|
|
102
104
|
);
|
|
103
105
|
const external = authorization.contextIntegrity;
|
|
104
|
-
const
|
|
105
|
-
const personal = isPersonalConnectionOwnerPrincipal(authorization);
|
|
106
|
-
const lensPack = await getPackInstallation(
|
|
106
|
+
const policy = await withOrganizationIntegrationPolicyFence(
|
|
107
107
|
deps.db,
|
|
108
|
-
authorization.grant
|
|
109
|
-
|
|
108
|
+
authorization.grant,
|
|
109
|
+
async (_tx, current) => current,
|
|
110
110
|
);
|
|
111
|
+
const canWrite = hasPermission(authorization.grant.permissions, "connections:write");
|
|
112
|
+
const personal = isPersonalConnectionOwnerPrincipal(authorization);
|
|
111
113
|
let mcpConfigured = false;
|
|
112
114
|
let credentialConfigured = false;
|
|
113
115
|
try {
|
|
@@ -118,7 +120,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
118
120
|
} catch {
|
|
119
121
|
/* Missing operator configuration is catalog readiness, not actor authority. */
|
|
120
122
|
}
|
|
121
|
-
|
|
123
|
+
const providers = [
|
|
122
124
|
...(["x", "reddit"] as const).map((providerId) => {
|
|
123
125
|
let configured = mcpConfigured;
|
|
124
126
|
try {
|
|
@@ -233,27 +235,27 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
233
235
|
label: "Gmail",
|
|
234
236
|
family: "google",
|
|
235
237
|
readiness:
|
|
236
|
-
!external || !canWrite
|
|
238
|
+
!external || !canWrite
|
|
237
239
|
? "unsupported"
|
|
238
240
|
: mcpConfigured
|
|
239
241
|
? "available"
|
|
240
242
|
: "needs_configuration",
|
|
241
|
-
ownership: personal ? ["personal"] : [],
|
|
243
|
+
ownership: personal ? ["workspace", "personal"] : ["workspace"],
|
|
242
244
|
setup: ["oauth"],
|
|
243
245
|
}),
|
|
244
246
|
ConnectProvider.parse({
|
|
245
247
|
id: "slack-personal",
|
|
246
|
-
label: "
|
|
248
|
+
label: "Slack account",
|
|
247
249
|
family: "slack",
|
|
248
250
|
readiness:
|
|
249
|
-
!external || !canWrite
|
|
251
|
+
!external || !canWrite
|
|
250
252
|
? "unsupported"
|
|
251
253
|
: mcpConfigured &&
|
|
252
254
|
deps.settings.slackClientId?.trim() &&
|
|
253
255
|
deps.settings.slackClientSecret?.trim()
|
|
254
256
|
? "available"
|
|
255
257
|
: "needs_configuration",
|
|
256
|
-
ownership: personal ? ["personal"] : [],
|
|
258
|
+
ownership: personal ? ["workspace", "personal"] : ["workspace"],
|
|
257
259
|
setup: ["oauth"],
|
|
258
260
|
}),
|
|
259
261
|
ConnectProvider.parse({
|
|
@@ -296,7 +298,6 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
296
298
|
!hasPermission(authorization.grant.permissions, "secrets:write")
|
|
297
299
|
? "unsupported"
|
|
298
300
|
: credentialConfigured &&
|
|
299
|
-
lensPack?.status === "active" &&
|
|
300
301
|
deps.settings.sandboxBackend !== "selfhosted" &&
|
|
301
302
|
prReviewGitHubAppMissingSettings(deps.settings).length === 0
|
|
302
303
|
? "available"
|
|
@@ -391,7 +392,16 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
391
392
|
ownership: personal ? ["workspace", "personal"] : ["workspace"],
|
|
392
393
|
setup: ["credentials"],
|
|
393
394
|
}),
|
|
394
|
-
]
|
|
395
|
+
];
|
|
396
|
+
return c.json(
|
|
397
|
+
providers.filter(
|
|
398
|
+
(provider) =>
|
|
399
|
+
policy.mode === "unrestricted" ||
|
|
400
|
+
policy.allowedIntegrationKeys.includes(
|
|
401
|
+
integrationKeyForConnectProvider(provider.id) ?? "",
|
|
402
|
+
),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
395
405
|
});
|
|
396
406
|
app.get("/v1/workspaces/:workspaceId/connect/accounts", async (c) => {
|
|
397
407
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -717,7 +727,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
717
727
|
await requireConnectOwnerAuthority(tx, scope, installationPermission, origin);
|
|
718
728
|
if (before.providerId === "github-lens") {
|
|
719
729
|
await requireConnectOwnerAuthority(tx, scope, "secrets:write", origin);
|
|
720
|
-
await requireGitHubLensConnect({ ...deps, db: tx }
|
|
730
|
+
await requireGitHubLensConnect({ ...deps, db: tx });
|
|
721
731
|
}
|
|
722
732
|
},
|
|
723
733
|
execute: async (attempt) =>
|
|
@@ -754,6 +764,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
754
764
|
const resolved = await resolveForRoute({
|
|
755
765
|
deps,
|
|
756
766
|
transport,
|
|
767
|
+
accountId: scope.accountId,
|
|
757
768
|
workspaceId,
|
|
758
769
|
subjectId: scope.subjectId,
|
|
759
770
|
payload: {
|
|
@@ -904,7 +915,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
904
915
|
try {
|
|
905
916
|
credentialHeaders =
|
|
906
917
|
"headers" in values
|
|
907
|
-
?
|
|
918
|
+
? normalizedCredentialHeaders(
|
|
908
919
|
z.record(z.string(), z.string()).parse(JSON.parse(values.headers)),
|
|
909
920
|
)
|
|
910
921
|
: { authorization: `Bearer ${values.token}` };
|
|
@@ -1052,6 +1063,9 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1052
1063
|
throw new HTTPException(409, { message: "Reload the current connection setup" });
|
|
1053
1064
|
const started = await startMcpOAuth(deps, {
|
|
1054
1065
|
...scope,
|
|
1066
|
+
...(before.providerId === "gmail" || before.providerId === "slack-personal"
|
|
1067
|
+
? { integrationKey: before.providerId }
|
|
1068
|
+
: {}),
|
|
1055
1069
|
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1056
1070
|
connectAttemptId: attempt.id,
|
|
1057
1071
|
personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(authorization),
|
|
@@ -1144,6 +1158,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1144
1158
|
const resolved = await resolveForRoute({
|
|
1145
1159
|
deps,
|
|
1146
1160
|
transport,
|
|
1161
|
+
accountId: scope.accountId,
|
|
1147
1162
|
workspaceId,
|
|
1148
1163
|
subjectId: scope.subjectId,
|
|
1149
1164
|
payload: previewRequest,
|
|
@@ -1237,6 +1252,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1237
1252
|
inputDigest: createHash("sha256")
|
|
1238
1253
|
.update(stableJson({ ...input, action: "cancel" }))
|
|
1239
1254
|
.digest("hex"),
|
|
1255
|
+
purpose: "cancellation",
|
|
1240
1256
|
authorize: async (tx, attempt, origin) => {
|
|
1241
1257
|
const permission =
|
|
1242
1258
|
attempt.providerId === "mcp-install"
|
|
@@ -1320,8 +1336,59 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1320
1336
|
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1321
1337
|
};
|
|
1322
1338
|
return c.json(
|
|
1323
|
-
await
|
|
1324
|
-
|
|
1339
|
+
await (async () => {
|
|
1340
|
+
// Remote setup preparation stays outside the policy and actor transactions.
|
|
1341
|
+
// Repeat both admissions when persisting the resulting attempt.
|
|
1342
|
+
const tx = deps.db;
|
|
1343
|
+
const admit = () =>
|
|
1344
|
+
withOrganizationIntegrationPolicyFence(deps.db, scope, async (policyTx, policy) => {
|
|
1345
|
+
return withWorkspaceSubjectRls(
|
|
1346
|
+
policyTx,
|
|
1347
|
+
workspaceId,
|
|
1348
|
+
scope.subjectId,
|
|
1349
|
+
async (actorTx) => {
|
|
1350
|
+
await requireConnectOwnerAuthority(actorTx, scope, setupPermission);
|
|
1351
|
+
const replay = await getConnectBeginReplay(actorTx, scope, {
|
|
1352
|
+
idempotencyKey: input.idempotencyKey,
|
|
1353
|
+
requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
1354
|
+
returnUrl: input.returnUrl,
|
|
1355
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1356
|
+
});
|
|
1357
|
+
if (replay) return replay;
|
|
1358
|
+
assertOrganizationIntegrationAllowed(
|
|
1359
|
+
policy,
|
|
1360
|
+
integrationKeyForConnectProvider(input.providerId),
|
|
1361
|
+
);
|
|
1362
|
+
return null;
|
|
1363
|
+
},
|
|
1364
|
+
);
|
|
1365
|
+
});
|
|
1366
|
+
const replay = await admit();
|
|
1367
|
+
if (replay) return replay;
|
|
1368
|
+
const beginConnectAttempt = async (
|
|
1369
|
+
_db: Parameters<typeof persistConnectAttempt>[0],
|
|
1370
|
+
_scope: Parameters<typeof persistConnectAttempt>[1],
|
|
1371
|
+
attemptInput: Parameters<typeof persistConnectAttempt>[2],
|
|
1372
|
+
) =>
|
|
1373
|
+
withOrganizationIntegrationPolicyFence(deps.db, scope, async (policyTx, policy) => {
|
|
1374
|
+
return withWorkspaceSubjectRls(
|
|
1375
|
+
policyTx,
|
|
1376
|
+
workspaceId,
|
|
1377
|
+
scope.subjectId,
|
|
1378
|
+
async (actorTx) => {
|
|
1379
|
+
await requireConnectOwnerAuthority(actorTx, scope, setupPermission);
|
|
1380
|
+
return persistConnectAttempt(actorTx, scope, {
|
|
1381
|
+
...attemptInput,
|
|
1382
|
+
authorizeAcquisition: async () => {
|
|
1383
|
+
assertOrganizationIntegrationAllowed(
|
|
1384
|
+
policy,
|
|
1385
|
+
integrationKeyForConnectProvider(input.providerId),
|
|
1386
|
+
);
|
|
1387
|
+
},
|
|
1388
|
+
});
|
|
1389
|
+
},
|
|
1390
|
+
);
|
|
1391
|
+
});
|
|
1325
1392
|
const id = randomUUID();
|
|
1326
1393
|
if (input.providerId === "mcp-install") {
|
|
1327
1394
|
if (
|
|
@@ -1467,7 +1534,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1467
1534
|
if (!hasPermission(authorization.grant.permissions, "secrets:write"))
|
|
1468
1535
|
throw new HTTPException(403, { message: "secrets:write required" });
|
|
1469
1536
|
await requireConnectOwnerAuthority(tx, scope, "secrets:write");
|
|
1470
|
-
await requireGitHubLensConnect({ ...deps, db: tx }
|
|
1537
|
+
await requireGitHubLensConnect({ ...deps, db: tx });
|
|
1471
1538
|
}
|
|
1472
1539
|
const navigation = githubAppConnectNavigation(
|
|
1473
1540
|
deps,
|
|
@@ -1595,14 +1662,6 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1595
1662
|
"fiken-token",
|
|
1596
1663
|
].includes(input.providerId)
|
|
1597
1664
|
) {
|
|
1598
|
-
if (input.providerId === "slack-personal" && input.ownership !== "personal")
|
|
1599
|
-
throw new HTTPException(422, {
|
|
1600
|
-
message: "Hosted Slack MCP requires personal ownership",
|
|
1601
|
-
});
|
|
1602
|
-
if (input.providerId === "gmail" && input.ownership !== "personal")
|
|
1603
|
-
throw new HTTPException(422, {
|
|
1604
|
-
message: "Gmail requires personal ownership; each user connects their own account",
|
|
1605
|
-
});
|
|
1606
1665
|
if (input.providerId === "fiken-token" && input.ownership !== "workspace")
|
|
1607
1666
|
throw new HTTPException(422, { message: "Fiken is workspace-owned" });
|
|
1608
1667
|
requireEnvironmentEncryption(deps.settings);
|
|
@@ -1867,7 +1926,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1867
1926
|
expiresAt: started.expiresAt,
|
|
1868
1927
|
},
|
|
1869
1928
|
});
|
|
1870
|
-
}),
|
|
1929
|
+
})(),
|
|
1871
1930
|
);
|
|
1872
1931
|
});
|
|
1873
1932
|
app.get("/v1/workspaces/:workspaceId/connect/attempts", async (c) => {
|