@agent-native/core 0.105.0 → 0.106.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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +33 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +4 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +5 -1
- package/corpus/core/src/client/composer/PromptComposer.tsx +8 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +38 -1
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +107 -32
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +33 -0
- package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
- package/corpus/core/src/client/use-chat-models.ts +10 -2
- package/corpus/core/src/connections/catalog.ts +107 -6
- package/corpus/core/src/localization/default-messages.ts +2 -0
- package/corpus/core/src/mcp/actions/call-mcp-tool.ts +18 -0
- package/corpus/core/src/mcp/actions/list-mcp-tools.ts +18 -0
- package/corpus/core/src/mcp-client/app-api.ts +136 -0
- package/corpus/core/src/mcp-client/index.ts +21 -0
- package/corpus/core/src/mcp-client/oauth-client.ts +462 -0
- package/corpus/core/src/mcp-client/oauth-routes.ts +307 -0
- package/corpus/core/src/mcp-client/remote-store.ts +93 -118
- package/corpus/core/src/mcp-client/remote-url.ts +97 -0
- package/corpus/core/src/mcp-client/routes.ts +11 -0
- package/corpus/core/src/oauth-tokens/store.ts +10 -4
- package/corpus/core/src/provider-api/index.ts +690 -55
- package/corpus/core/src/secrets/register-framework-secrets.ts +30 -0
- package/corpus/core/src/server/action-discovery.ts +2 -0
- package/corpus/core/src/server/core-routes-plugin.ts +9 -1
- package/corpus/core/src/server/workspace-provider-oauth.ts +448 -61
- package/corpus/core/src/templates/default/AGENTS.md +9 -7
- package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
- package/corpus/templates/analytics/app/lib/data-source-status.ts +2 -0
- package/corpus/templates/analytics/app/pages/DataSources.tsx +61 -0
- package/corpus/templates/analytics/server/handlers/jira.ts +7 -0
- package/corpus/templates/analytics/server/lib/jira.ts +14 -42
- package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +11 -0
- package/corpus/templates/dispatch/app/routes/integrations.tsx +57 -4
- package/dist/client/AssistantChat.d.ts +2 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +5 -2
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts +2 -0
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +5 -2
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +4 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +13 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +58 -14
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts +9 -0
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +13 -0
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/resources/use-mcp-servers.d.ts +1 -0
- package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
- package/dist/client/resources/use-mcp-servers.js.map +1 -1
- package/dist/client/use-chat-models.d.ts +1 -0
- package/dist/client/use-chat-models.d.ts.map +1 -1
- package/dist/client/use-chat-models.js +8 -2
- package/dist/client/use-chat-models.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/connections/catalog.d.ts +78 -7
- package/dist/connections/catalog.d.ts.map +1 -1
- package/dist/connections/catalog.js +97 -6
- package/dist/connections/catalog.js.map +1 -1
- package/dist/localization/default-messages.d.ts +2 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +2 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp/actions/call-mcp-tool.d.ts +7 -0
- package/dist/mcp/actions/call-mcp-tool.d.ts.map +1 -0
- package/dist/mcp/actions/call-mcp-tool.js +15 -0
- package/dist/mcp/actions/call-mcp-tool.js.map +1 -0
- package/dist/mcp/actions/list-mcp-tools.d.ts +5 -0
- package/dist/mcp/actions/list-mcp-tools.d.ts.map +1 -0
- package/dist/mcp/actions/list-mcp-tools.js +16 -0
- package/dist/mcp/actions/list-mcp-tools.js.map +1 -0
- package/dist/mcp-client/app-api.d.ts +34 -0
- package/dist/mcp-client/app-api.d.ts.map +1 -0
- package/dist/mcp-client/app-api.js +94 -0
- package/dist/mcp-client/app-api.js.map +1 -0
- package/dist/mcp-client/index.d.ts +3 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +3 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/oauth-client.d.ts +113 -0
- package/dist/mcp-client/oauth-client.d.ts.map +1 -0
- package/dist/mcp-client/oauth-client.js +316 -0
- package/dist/mcp-client/oauth-client.js.map +1 -0
- package/dist/mcp-client/oauth-routes.d.ts +25 -0
- package/dist/mcp-client/oauth-routes.d.ts.map +1 -0
- package/dist/mcp-client/oauth-routes.js +218 -0
- package/dist/mcp-client/oauth-routes.js.map +1 -0
- package/dist/mcp-client/remote-store.d.ts +21 -6
- package/dist/mcp-client/remote-store.d.ts.map +1 -1
- package/dist/mcp-client/remote-store.js +72 -124
- package/dist/mcp-client/remote-store.js.map +1 -1
- package/dist/mcp-client/remote-url.d.ts +8 -0
- package/dist/mcp-client/remote-url.d.ts.map +1 -0
- package/dist/mcp-client/remote-url.js +103 -0
- package/dist/mcp-client/remote-url.js.map +1 -0
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +9 -0
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/oauth-tokens/store.d.ts +2 -2
- package/dist/oauth-tokens/store.d.ts.map +1 -1
- package/dist/oauth-tokens/store.js +10 -6
- package/dist/oauth-tokens/store.js.map +1 -1
- package/dist/provider-api/index.d.ts +14 -0
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +499 -61
- package/dist/provider-api/index.js.map +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
- package/dist/secrets/register-framework-secrets.js +28 -0
- package/dist/secrets/register-framework-secrets.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +2 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +9 -1
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/workspace-provider-oauth.d.ts +10 -3
- package/dist/server/workspace-provider-oauth.d.ts.map +1 -1
- package/dist/server/workspace-provider-oauth.js +325 -43
- package/dist/server/workspace-provider-oauth.js.map +1 -1
- package/dist/templates/default/AGENTS.md +9 -7
- package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
- package/package.json +2 -2
- package/src/templates/default/AGENTS.md +9 -7
- package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type WorkspaceConnectionProvider,
|
|
17
17
|
} from "../connections/catalog.js";
|
|
18
18
|
import { saveOAuthTokens, setOAuthDisplayName } from "../oauth-tokens/store.js";
|
|
19
|
+
import { getOrgContext } from "../org/context.js";
|
|
19
20
|
import { decryptSecretValue, encryptSecretValue } from "../secrets/crypto.js";
|
|
20
21
|
import {
|
|
21
22
|
listWorkspaceConnections,
|
|
@@ -33,11 +34,22 @@ import {
|
|
|
33
34
|
} from "./google-oauth.js";
|
|
34
35
|
import { runWithRequestContext } from "./request-context.js";
|
|
35
36
|
|
|
36
|
-
export type GenericWorkspaceOAuthProvider =
|
|
37
|
+
export type GenericWorkspaceOAuthProvider =
|
|
38
|
+
| "figma"
|
|
39
|
+
| "google_drive"
|
|
40
|
+
| "github"
|
|
41
|
+
| "hubspot"
|
|
42
|
+
| "jira"
|
|
43
|
+
| "sentry"
|
|
44
|
+
| "notion";
|
|
37
45
|
|
|
38
46
|
const SUPPORTED_PROVIDERS = new Set<GenericWorkspaceOAuthProvider>([
|
|
39
47
|
"figma",
|
|
40
48
|
"google_drive",
|
|
49
|
+
"github",
|
|
50
|
+
"hubspot",
|
|
51
|
+
"jira",
|
|
52
|
+
"sentry",
|
|
41
53
|
"notion",
|
|
42
54
|
]);
|
|
43
55
|
const FLOW_TTL_SECONDS = 10 * 60;
|
|
@@ -83,6 +95,21 @@ export async function handleWorkspaceProviderOAuthStart(
|
|
|
83
95
|
if (getMethod(event) !== "GET") return methodNotAllowed(event);
|
|
84
96
|
const session = await getSession(event).catch(() => null);
|
|
85
97
|
if (!session?.email) return unauthorized(event);
|
|
98
|
+
const orgContext = await requireWorkspaceProviderOAuthAdmin(event);
|
|
99
|
+
if (!orgContext) {
|
|
100
|
+
return {
|
|
101
|
+
error:
|
|
102
|
+
"Only organization owners and admins can connect shared OAuth accounts.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const orgId = orgContext.orgId;
|
|
106
|
+
if (!orgId) {
|
|
107
|
+
setResponseStatus(event, 403);
|
|
108
|
+
return {
|
|
109
|
+
error:
|
|
110
|
+
"Only organization owners and admins can connect shared OAuth accounts.",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
86
113
|
const provider = requiredProvider(providerId);
|
|
87
114
|
const query = getQuery(event);
|
|
88
115
|
const appId = normalizeAppId(
|
|
@@ -100,12 +127,10 @@ export async function handleWorkspaceProviderOAuthStart(
|
|
|
100
127
|
return { error: "Invalid OAuth redirect URI." };
|
|
101
128
|
}
|
|
102
129
|
return runWithRequestContext(
|
|
103
|
-
{ userEmail: session.email, orgId
|
|
130
|
+
{ userEmail: session.email, orgId },
|
|
104
131
|
async () => {
|
|
105
|
-
const [clientId, clientSecret] =
|
|
106
|
-
|
|
107
|
-
resolveSecret(clientCredentialKey(providerId, "secret")),
|
|
108
|
-
]);
|
|
132
|
+
const [clientId, clientSecret] =
|
|
133
|
+
await resolveProviderClientCredentials(providerId);
|
|
109
134
|
if (!clientId || !clientSecret) {
|
|
110
135
|
setResponseStatus(event, 503);
|
|
111
136
|
return {
|
|
@@ -125,7 +150,7 @@ export async function handleWorkspaceProviderOAuthStart(
|
|
|
125
150
|
const state = encodeOAuthState({
|
|
126
151
|
redirectUri,
|
|
127
152
|
owner: session.email,
|
|
128
|
-
orgId
|
|
153
|
+
orgId,
|
|
129
154
|
app: appId,
|
|
130
155
|
returnUrl,
|
|
131
156
|
flowId,
|
|
@@ -136,7 +161,7 @@ export async function handleWorkspaceProviderOAuthStart(
|
|
|
136
161
|
verifier,
|
|
137
162
|
redirectUri,
|
|
138
163
|
owner: session.email,
|
|
139
|
-
|
|
164
|
+
orgId,
|
|
140
165
|
appId,
|
|
141
166
|
expiresAt: Date.now() + FLOW_TTL_SECONDS * 1_000,
|
|
142
167
|
};
|
|
@@ -171,6 +196,21 @@ export async function handleWorkspaceProviderOAuthCallback(
|
|
|
171
196
|
if (getMethod(event) !== "GET") return methodNotAllowed(event);
|
|
172
197
|
const session = await getSession(event).catch(() => null);
|
|
173
198
|
if (!session?.email) return unauthorized(event);
|
|
199
|
+
const orgContext = await requireWorkspaceProviderOAuthAdmin(event);
|
|
200
|
+
if (!orgContext) {
|
|
201
|
+
return {
|
|
202
|
+
error:
|
|
203
|
+
"Only organization owners and admins can connect shared OAuth accounts.",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const orgId = orgContext.orgId;
|
|
207
|
+
if (!orgId) {
|
|
208
|
+
setResponseStatus(event, 403);
|
|
209
|
+
return {
|
|
210
|
+
error:
|
|
211
|
+
"Only organization owners and admins can connect shared OAuth accounts.",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
174
214
|
const query = getQuery(event);
|
|
175
215
|
const code = text(query.code);
|
|
176
216
|
const stateParam = text(query.state);
|
|
@@ -193,7 +233,7 @@ export async function handleWorkspaceProviderOAuthCallback(
|
|
|
193
233
|
state,
|
|
194
234
|
provider: providerId,
|
|
195
235
|
sessionEmail: session.email,
|
|
196
|
-
sessionOrgId:
|
|
236
|
+
sessionOrgId: orgId,
|
|
197
237
|
})
|
|
198
238
|
) {
|
|
199
239
|
setResponseStatus(event, 400);
|
|
@@ -201,12 +241,10 @@ export async function handleWorkspaceProviderOAuthCallback(
|
|
|
201
241
|
}
|
|
202
242
|
const provider = requiredProvider(providerId);
|
|
203
243
|
return runWithRequestContext(
|
|
204
|
-
{ userEmail: session.email, orgId
|
|
244
|
+
{ userEmail: session.email, orgId },
|
|
205
245
|
async () => {
|
|
206
|
-
const [clientId, clientSecret] =
|
|
207
|
-
|
|
208
|
-
resolveSecret(clientCredentialKey(providerId, "secret")),
|
|
209
|
-
]);
|
|
246
|
+
const [clientId, clientSecret] =
|
|
247
|
+
await resolveProviderClientCredentials(providerId);
|
|
210
248
|
if (!clientId || !clientSecret) {
|
|
211
249
|
setResponseStatus(event, 503);
|
|
212
250
|
return {
|
|
@@ -222,51 +260,71 @@ export async function handleWorkspaceProviderOAuthCallback(
|
|
|
222
260
|
verifier: flow.verifier,
|
|
223
261
|
redirectUri: flow.redirectUri,
|
|
224
262
|
});
|
|
225
|
-
const
|
|
263
|
+
const identities = await resolveWorkspaceProviderIdentities(
|
|
226
264
|
providerId,
|
|
227
265
|
tokens,
|
|
228
266
|
);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
await setOAuthDisplayName(
|
|
236
|
-
provider.oauth!.provider,
|
|
237
|
-
identity.accountId,
|
|
238
|
-
identity.label,
|
|
239
|
-
);
|
|
240
|
-
const existing = (
|
|
241
|
-
await listWorkspaceConnections({
|
|
242
|
-
provider: providerId,
|
|
243
|
-
includeDisabled: true,
|
|
244
|
-
})
|
|
245
|
-
).find((connection) => connection.accountId === identity.accountId);
|
|
246
|
-
const scopes = mergeWorkspaceOAuthValues(
|
|
247
|
-
existing?.scopes ?? [],
|
|
248
|
-
tokenScopes(tokens, provider.oauth!.scopes),
|
|
249
|
-
);
|
|
250
|
-
const connection = await upsertWorkspaceConnection({
|
|
251
|
-
...(existing ? { id: existing.id } : {}),
|
|
252
|
-
provider: providerId,
|
|
253
|
-
label: `${provider.label}: ${identity.label}`,
|
|
254
|
-
accountId: identity.accountId,
|
|
255
|
-
accountLabel: identity.label,
|
|
256
|
-
status: "connected",
|
|
257
|
-
scopes,
|
|
258
|
-
allowedApps: existing?.allowedApps ?? [],
|
|
259
|
-
config: { credentialMode: "oauth" },
|
|
260
|
-
lastCheckedAt: new Date(),
|
|
261
|
-
lastError: null,
|
|
262
|
-
});
|
|
263
|
-
await upsertWorkspaceConnectionGrant({
|
|
264
|
-
connectionId: connection.id,
|
|
265
|
-
appId: flow.appId,
|
|
267
|
+
if (!identities.length) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`${provider.label} OAuth did not return an accessible account.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const existingConnections = await listWorkspaceConnections({
|
|
266
273
|
provider: providerId,
|
|
267
|
-
|
|
268
|
-
config: { credentialMode: "oauth" },
|
|
274
|
+
includeDisabled: true,
|
|
269
275
|
});
|
|
276
|
+
for (const identity of identities) {
|
|
277
|
+
const accountId = scopedOAuthAccountId(
|
|
278
|
+
providerId,
|
|
279
|
+
session.email,
|
|
280
|
+
identity.accountId,
|
|
281
|
+
);
|
|
282
|
+
await saveOAuthTokens(
|
|
283
|
+
provider.oauth!.provider,
|
|
284
|
+
accountId,
|
|
285
|
+
tokens,
|
|
286
|
+
session.email,
|
|
287
|
+
);
|
|
288
|
+
await setOAuthDisplayName(
|
|
289
|
+
provider.oauth!.provider,
|
|
290
|
+
accountId,
|
|
291
|
+
identity.label,
|
|
292
|
+
);
|
|
293
|
+
const existing = existingConnections.find(
|
|
294
|
+
(connection) => connection.accountId === accountId,
|
|
295
|
+
);
|
|
296
|
+
const scopes = mergeWorkspaceOAuthValues(
|
|
297
|
+
existing?.scopes ?? [],
|
|
298
|
+
identity.scopes ?? tokenScopes(tokens, provider.oauth!.scopes),
|
|
299
|
+
);
|
|
300
|
+
const connectionConfig = {
|
|
301
|
+
credentialMode: "oauth",
|
|
302
|
+
...(providerId === "hubspot" || providerId === "jira"
|
|
303
|
+
? { externalAccountId: identity.accountId }
|
|
304
|
+
: {}),
|
|
305
|
+
...(identity.config ?? {}),
|
|
306
|
+
};
|
|
307
|
+
const connection = await upsertWorkspaceConnection({
|
|
308
|
+
...(existing ? { id: existing.id } : {}),
|
|
309
|
+
provider: providerId,
|
|
310
|
+
label: `${provider.label}: ${identity.label}`,
|
|
311
|
+
accountId,
|
|
312
|
+
accountLabel: identity.label,
|
|
313
|
+
status: "connected",
|
|
314
|
+
scopes,
|
|
315
|
+
allowedApps: existing?.allowedApps ?? [],
|
|
316
|
+
config: connectionConfig,
|
|
317
|
+
lastCheckedAt: new Date(),
|
|
318
|
+
lastError: null,
|
|
319
|
+
});
|
|
320
|
+
await upsertWorkspaceConnectionGrant({
|
|
321
|
+
connectionId: connection.id,
|
|
322
|
+
appId: flow.appId,
|
|
323
|
+
provider: providerId,
|
|
324
|
+
scopes,
|
|
325
|
+
config: connectionConfig,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
270
328
|
const returnPath =
|
|
271
329
|
state.returnUrl ??
|
|
272
330
|
`/settings/connections?connected=${encodeURIComponent(providerId)}`;
|
|
@@ -289,10 +347,17 @@ export function buildWorkspaceProviderAuthorizationUrl(input: {
|
|
|
289
347
|
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
290
348
|
url.searchParams.set("response_type", "code");
|
|
291
349
|
url.searchParams.set("state", input.state);
|
|
292
|
-
if (input.provider.id === "figma") {
|
|
350
|
+
if (input.provider.id === "figma" || input.provider.id === "sentry") {
|
|
293
351
|
url.searchParams.set("code_challenge", input.challenge);
|
|
294
352
|
url.searchParams.set("code_challenge_method", "S256");
|
|
295
353
|
}
|
|
354
|
+
if (input.provider.id === "github") {
|
|
355
|
+
url.searchParams.set("allow_signup", "true");
|
|
356
|
+
}
|
|
357
|
+
if (input.provider.id === "jira") {
|
|
358
|
+
url.searchParams.set("audience", "api.atlassian.com");
|
|
359
|
+
url.searchParams.set("prompt", "consent");
|
|
360
|
+
}
|
|
296
361
|
if (input.provider.id === "google_drive") {
|
|
297
362
|
url.searchParams.set("access_type", "offline");
|
|
298
363
|
url.searchParams.set("include_granted_scopes", "true");
|
|
@@ -317,11 +382,109 @@ export async function exchangeWorkspaceProviderOAuthCode(input: {
|
|
|
317
382
|
const tokenUrl = input.provider.oauth?.tokenUrl;
|
|
318
383
|
if (!tokenUrl) throw new Error("Provider does not support OAuth.");
|
|
319
384
|
const authorization = `Basic ${Buffer.from(`${input.clientId}:${input.clientSecret}`).toString("base64")}`;
|
|
385
|
+
if (input.providerId === "github") {
|
|
386
|
+
const { response, body } = await fetchBoundedProviderJson(
|
|
387
|
+
tokenUrl,
|
|
388
|
+
{
|
|
389
|
+
method: "POST",
|
|
390
|
+
headers: {
|
|
391
|
+
Accept: "application/json",
|
|
392
|
+
"Content-Type": "application/json",
|
|
393
|
+
},
|
|
394
|
+
body: JSON.stringify({
|
|
395
|
+
client_id: input.clientId,
|
|
396
|
+
client_secret: input.clientSecret,
|
|
397
|
+
code: input.code,
|
|
398
|
+
redirect_uri: input.redirectUri,
|
|
399
|
+
}),
|
|
400
|
+
},
|
|
401
|
+
input.provider.label,
|
|
402
|
+
);
|
|
403
|
+
const accessToken = text(body.access_token);
|
|
404
|
+
if (!response.ok || !accessToken) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`${input.provider.label} OAuth token exchange failed (${response.status}).`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
...body,
|
|
411
|
+
...(Number.isFinite(Number(body.expires_in)) &&
|
|
412
|
+
Number(body.expires_in) > 0
|
|
413
|
+
? { expiry_date: Date.now() + Number(body.expires_in) * 1_000 }
|
|
414
|
+
: {}),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (input.providerId === "hubspot") {
|
|
418
|
+
const { response, body } = await fetchBoundedProviderJson(
|
|
419
|
+
tokenUrl,
|
|
420
|
+
{
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
423
|
+
body: new URLSearchParams({
|
|
424
|
+
grant_type: "authorization_code",
|
|
425
|
+
client_id: input.clientId,
|
|
426
|
+
client_secret: input.clientSecret,
|
|
427
|
+
code: input.code,
|
|
428
|
+
redirect_uri: input.redirectUri,
|
|
429
|
+
}),
|
|
430
|
+
},
|
|
431
|
+
input.provider.label,
|
|
432
|
+
);
|
|
433
|
+
const accessToken = text(body.access_token);
|
|
434
|
+
if (!response.ok || !accessToken) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`${input.provider.label} OAuth token exchange failed (${response.status}).`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
...body,
|
|
441
|
+
...(Number.isFinite(Number(body.expires_in)) &&
|
|
442
|
+
Number(body.expires_in) > 0
|
|
443
|
+
? { expiry_date: Date.now() + Number(body.expires_in) * 1_000 }
|
|
444
|
+
: {}),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (input.providerId === "jira") {
|
|
448
|
+
const { response, body } = await fetchBoundedProviderJsonValue(
|
|
449
|
+
tokenUrl,
|
|
450
|
+
{
|
|
451
|
+
method: "POST",
|
|
452
|
+
headers: { "Content-Type": "application/json" },
|
|
453
|
+
body: JSON.stringify({
|
|
454
|
+
grant_type: "authorization_code",
|
|
455
|
+
client_id: input.clientId,
|
|
456
|
+
client_secret: input.clientSecret,
|
|
457
|
+
code: input.code,
|
|
458
|
+
redirect_uri: input.redirectUri,
|
|
459
|
+
}),
|
|
460
|
+
},
|
|
461
|
+
input.provider.label,
|
|
462
|
+
);
|
|
463
|
+
const tokenBody = record(body) ?? {};
|
|
464
|
+
const accessToken = text(tokenBody.access_token);
|
|
465
|
+
if (!response.ok || !accessToken) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`${input.provider.label} OAuth token exchange failed (${response.status}).`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
const expiresIn = Number(tokenBody.expires_in);
|
|
471
|
+
return {
|
|
472
|
+
...tokenBody,
|
|
473
|
+
...(Number.isFinite(expiresIn) && expiresIn > 0
|
|
474
|
+
? { expiry_date: Date.now() + expiresIn * 1_000 }
|
|
475
|
+
: {}),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
320
478
|
const fields = {
|
|
321
479
|
grant_type: "authorization_code",
|
|
322
480
|
code: input.code,
|
|
323
481
|
redirect_uri: input.redirectUri,
|
|
324
|
-
...(input.providerId === "figma"
|
|
482
|
+
...(input.providerId === "figma" || input.providerId === "sentry"
|
|
483
|
+
? { code_verifier: input.verifier }
|
|
484
|
+
: {}),
|
|
485
|
+
...(input.providerId === "sentry"
|
|
486
|
+
? { client_id: input.clientId, client_secret: input.clientSecret }
|
|
487
|
+
: {}),
|
|
325
488
|
...(input.providerId === "google_drive"
|
|
326
489
|
? { client_id: input.clientId, client_secret: input.clientSecret }
|
|
327
490
|
: {}),
|
|
@@ -368,7 +531,91 @@ export async function exchangeWorkspaceProviderOAuthCode(input: {
|
|
|
368
531
|
export async function resolveWorkspaceProviderIdentity(
|
|
369
532
|
providerId: GenericWorkspaceOAuthProvider,
|
|
370
533
|
tokens: Record<string, unknown>,
|
|
371
|
-
): Promise<
|
|
534
|
+
): Promise<WorkspaceProviderIdentity> {
|
|
535
|
+
const [identity] = await resolveWorkspaceProviderIdentities(
|
|
536
|
+
providerId,
|
|
537
|
+
tokens,
|
|
538
|
+
);
|
|
539
|
+
if (!identity) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`${providerId} OAuth response did not identify an account.`,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return identity;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export async function resolveWorkspaceProviderIdentities(
|
|
548
|
+
providerId: GenericWorkspaceOAuthProvider,
|
|
549
|
+
tokens: Record<string, unknown>,
|
|
550
|
+
): Promise<WorkspaceProviderIdentity[]> {
|
|
551
|
+
if (providerId === "jira") {
|
|
552
|
+
const accessToken = text(tokens.access_token);
|
|
553
|
+
if (!accessToken) {
|
|
554
|
+
throw new Error("Jira OAuth response did not include an access token.");
|
|
555
|
+
}
|
|
556
|
+
const { response, body } = await fetchBoundedProviderJsonValue(
|
|
557
|
+
"https://api.atlassian.com/oauth/token/accessible-resources",
|
|
558
|
+
{
|
|
559
|
+
headers: {
|
|
560
|
+
Accept: "application/json",
|
|
561
|
+
Authorization: `Bearer ${accessToken}`,
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
"Jira",
|
|
565
|
+
);
|
|
566
|
+
if (!response.ok || !Array.isArray(body)) {
|
|
567
|
+
throw new Error("Jira OAuth response did not identify accessible sites.");
|
|
568
|
+
}
|
|
569
|
+
return body.flatMap((resource) => {
|
|
570
|
+
const entry = record(resource);
|
|
571
|
+
const cloudId = text(entry?.id);
|
|
572
|
+
const siteUrl = text(entry?.url);
|
|
573
|
+
if (!cloudId || !siteUrl) return [];
|
|
574
|
+
let parsedUrl: URL;
|
|
575
|
+
try {
|
|
576
|
+
parsedUrl = new URL(siteUrl);
|
|
577
|
+
} catch {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
if (
|
|
581
|
+
parsedUrl.protocol !== "https:" ||
|
|
582
|
+
!parsedUrl.hostname.endsWith(".atlassian.net")
|
|
583
|
+
) {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
const scopes = Array.isArray(entry?.scopes)
|
|
587
|
+
? entry.scopes.filter(
|
|
588
|
+
(scope): scope is string => typeof scope === "string",
|
|
589
|
+
)
|
|
590
|
+
: undefined;
|
|
591
|
+
return [
|
|
592
|
+
{
|
|
593
|
+
accountId: cloudId,
|
|
594
|
+
label: text(entry?.name) ?? parsedUrl.hostname,
|
|
595
|
+
...(scopes?.length ? { scopes } : {}),
|
|
596
|
+
config: {
|
|
597
|
+
atlassianCloudId: cloudId,
|
|
598
|
+
atlassianApiBaseUrl: `https://api.atlassian.com/ex/jira/${encodeURIComponent(cloudId)}`,
|
|
599
|
+
siteUrl: parsedUrl.origin,
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
];
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return [await resolveWorkspaceProviderIdentitySingle(providerId, tokens)];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
type WorkspaceProviderIdentity = {
|
|
609
|
+
accountId: string;
|
|
610
|
+
label: string;
|
|
611
|
+
scopes?: string[];
|
|
612
|
+
config?: Record<string, unknown>;
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
async function resolveWorkspaceProviderIdentitySingle(
|
|
616
|
+
providerId: GenericWorkspaceOAuthProvider,
|
|
617
|
+
tokens: Record<string, unknown>,
|
|
618
|
+
): Promise<WorkspaceProviderIdentity> {
|
|
372
619
|
if (providerId === "notion") {
|
|
373
620
|
const owner = record(tokens.owner);
|
|
374
621
|
const user = record(owner?.user);
|
|
@@ -390,6 +637,71 @@ export async function resolveWorkspaceProviderIdentity(
|
|
|
390
637
|
"Notion workspace",
|
|
391
638
|
};
|
|
392
639
|
}
|
|
640
|
+
if (providerId === "github") {
|
|
641
|
+
const accessToken = text(tokens.access_token)!;
|
|
642
|
+
const { response, body: user } = await fetchBoundedProviderJson(
|
|
643
|
+
"https://api.github.com/user",
|
|
644
|
+
{
|
|
645
|
+
headers: {
|
|
646
|
+
Accept: "application/vnd.github+json",
|
|
647
|
+
Authorization: `Bearer ${accessToken}`,
|
|
648
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
"GitHub",
|
|
652
|
+
);
|
|
653
|
+
const accountId = text(user.login) ?? text(user.id);
|
|
654
|
+
if (!response.ok || !accountId) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
"GitHub OAuth response did not identify the connected user.",
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
accountId,
|
|
661
|
+
label: text(user.name) ?? text(user.login) ?? "GitHub account",
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
if (providerId === "hubspot") {
|
|
665
|
+
const accessToken = text(tokens.access_token)!;
|
|
666
|
+
const { response, body } = await fetchBoundedProviderJson(
|
|
667
|
+
`https://api.hubapi.com/oauth/v1/access-tokens/${encodeURIComponent(accessToken)}`,
|
|
668
|
+
{ headers: { Accept: "application/json" } },
|
|
669
|
+
"HubSpot",
|
|
670
|
+
);
|
|
671
|
+
const accountId = scalarText(body.hub_id) ?? scalarText(body.user_id);
|
|
672
|
+
if (!response.ok || !accountId) {
|
|
673
|
+
throw new Error(
|
|
674
|
+
"HubSpot OAuth response did not identify the connected portal.",
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
accountId,
|
|
679
|
+
label: text(body.hub_domain) ?? `HubSpot portal ${accountId}`,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if (providerId === "sentry") {
|
|
683
|
+
const accessToken = text(tokens.access_token)!;
|
|
684
|
+
const { response, body: user } = await fetchBoundedProviderJson(
|
|
685
|
+
"https://sentry.io/api/0/users/me/",
|
|
686
|
+
{
|
|
687
|
+
headers: {
|
|
688
|
+
Accept: "application/json",
|
|
689
|
+
Authorization: `Bearer ${accessToken}`,
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
"Sentry",
|
|
693
|
+
);
|
|
694
|
+
const accountId = text(user.id);
|
|
695
|
+
if (!response.ok || !accountId) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
"Sentry OAuth response did not identify the connected account.",
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
accountId,
|
|
702
|
+
label: text(user?.email) ?? text(user?.name) ?? "Sentry account",
|
|
703
|
+
};
|
|
704
|
+
}
|
|
393
705
|
if (providerId === "google_drive") {
|
|
394
706
|
const accessToken = text(tokens.access_token)!;
|
|
395
707
|
const { response, body } = await fetchBoundedProviderJson(
|
|
@@ -475,11 +787,45 @@ export function isWorkspaceProviderOAuthFlowValid(input: {
|
|
|
475
787
|
);
|
|
476
788
|
}
|
|
477
789
|
|
|
790
|
+
export function canConnectWorkspaceProviderOAuth(
|
|
791
|
+
orgId: string | null | undefined,
|
|
792
|
+
role: string | null | undefined,
|
|
793
|
+
): boolean {
|
|
794
|
+
return Boolean(orgId) && (role === "owner" || role === "admin");
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export function scopedOAuthAccountId(
|
|
798
|
+
provider: GenericWorkspaceOAuthProvider,
|
|
799
|
+
owner: string,
|
|
800
|
+
accountId: string,
|
|
801
|
+
): string {
|
|
802
|
+
if (provider !== "hubspot" && provider !== "jira") return accountId;
|
|
803
|
+
const ownerKey = crypto
|
|
804
|
+
.createHash("sha256")
|
|
805
|
+
.update(`${provider}:${owner.trim().toLowerCase()}`)
|
|
806
|
+
.digest("hex")
|
|
807
|
+
.slice(0, 24);
|
|
808
|
+
return `${accountId}::${ownerKey}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
478
811
|
async function fetchBoundedProviderJson(
|
|
479
812
|
url: string,
|
|
480
813
|
init: RequestInit,
|
|
481
814
|
providerLabel: string,
|
|
482
815
|
): Promise<{ response: Response; body: Record<string, unknown> }> {
|
|
816
|
+
const { response, body } = await fetchBoundedProviderJsonValue(
|
|
817
|
+
url,
|
|
818
|
+
init,
|
|
819
|
+
providerLabel,
|
|
820
|
+
);
|
|
821
|
+
return { response, body: record(body) ?? {} };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async function fetchBoundedProviderJsonValue(
|
|
825
|
+
url: string,
|
|
826
|
+
init: RequestInit,
|
|
827
|
+
providerLabel: string,
|
|
828
|
+
): Promise<{ response: Response; body: unknown }> {
|
|
483
829
|
let response: Response;
|
|
484
830
|
try {
|
|
485
831
|
response = await fetch(url, {
|
|
@@ -520,7 +866,7 @@ async function fetchBoundedProviderJson(
|
|
|
520
866
|
try {
|
|
521
867
|
const json = Buffer.concat(chunks).toString("utf8");
|
|
522
868
|
const body = JSON.parse(json) as unknown;
|
|
523
|
-
return { response, body
|
|
869
|
+
return { response, body };
|
|
524
870
|
} catch {
|
|
525
871
|
return { response, body: {} };
|
|
526
872
|
}
|
|
@@ -541,13 +887,37 @@ export function mergeWorkspaceOAuthValues(
|
|
|
541
887
|
return [...new Set([...existing, ...incoming])];
|
|
542
888
|
}
|
|
543
889
|
|
|
544
|
-
function
|
|
890
|
+
function clientCredentialKeys(
|
|
545
891
|
provider: GenericWorkspaceOAuthProvider,
|
|
546
892
|
field: "id" | "secret",
|
|
547
|
-
): string {
|
|
893
|
+
): string[] {
|
|
548
894
|
const prefix =
|
|
549
895
|
provider === "google_drive" ? "GOOGLE" : provider.toUpperCase();
|
|
550
|
-
|
|
896
|
+
const suffix = field === "id" ? "ID" : "SECRET";
|
|
897
|
+
if (provider === "github") {
|
|
898
|
+
const integrationPrefix = `GITHUB_INTEGRATION_CLIENT_${suffix}`;
|
|
899
|
+
return [integrationPrefix, `GITHUB_CLIENT_${suffix}`];
|
|
900
|
+
}
|
|
901
|
+
if (provider === "hubspot") {
|
|
902
|
+
const integrationPrefix = `HUBSPOT_INTEGRATION_CLIENT_${suffix}`;
|
|
903
|
+
return [integrationPrefix, `HUBSPOT_CLIENT_${suffix}`];
|
|
904
|
+
}
|
|
905
|
+
return [`${prefix}_CLIENT_${suffix}`];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function resolveProviderClientCredentials(
|
|
909
|
+
provider: GenericWorkspaceOAuthProvider,
|
|
910
|
+
): Promise<[string | null, string | null]> {
|
|
911
|
+
const [clientId, clientSecret] = await Promise.all(
|
|
912
|
+
(["id", "secret"] as const).map(async (field) => {
|
|
913
|
+
for (const key of clientCredentialKeys(provider, field)) {
|
|
914
|
+
const value = await resolveSecret(key);
|
|
915
|
+
if (value) return value;
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
}),
|
|
919
|
+
);
|
|
920
|
+
return [clientId, clientSecret];
|
|
551
921
|
}
|
|
552
922
|
|
|
553
923
|
function flowCookieName(provider: GenericWorkspaceOAuthProvider): string {
|
|
@@ -566,6 +936,11 @@ function text(value: unknown): string | undefined {
|
|
|
566
936
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
567
937
|
}
|
|
568
938
|
|
|
939
|
+
function scalarText(value: unknown): string | undefined {
|
|
940
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
941
|
+
return text(value);
|
|
942
|
+
}
|
|
943
|
+
|
|
569
944
|
function record(value: unknown): Record<string, unknown> | null {
|
|
570
945
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
571
946
|
? (value as Record<string, unknown>)
|
|
@@ -581,3 +956,15 @@ function unauthorized(event: H3Event) {
|
|
|
581
956
|
setResponseStatus(event, 401);
|
|
582
957
|
return { error: "Authentication required" };
|
|
583
958
|
}
|
|
959
|
+
|
|
960
|
+
async function requireWorkspaceProviderOAuthAdmin(event: H3Event) {
|
|
961
|
+
const context = await getOrgContext(event).catch(() => null);
|
|
962
|
+
if (
|
|
963
|
+
!context ||
|
|
964
|
+
!canConnectWorkspaceProviderOAuth(context.orgId, context.role)
|
|
965
|
+
) {
|
|
966
|
+
setResponseStatus(event, 403);
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
return context;
|
|
970
|
+
}
|
|
@@ -133,13 +133,15 @@ capability is missing, add or extend a `defineAction` so both the agent and UI
|
|
|
133
133
|
share the same operation. Do not create `/api/*` routes that only call,
|
|
134
134
|
repackage, or proxy an action.
|
|
135
135
|
|
|
136
|
-
| Action
|
|
137
|
-
|
|
|
138
|
-
| `view-screen`
|
|
139
|
-
| `navigate`
|
|
140
|
-
| `hello`
|
|
141
|
-
| `db-schema`
|
|
142
|
-
| `db-query`
|
|
136
|
+
| Action | Args | Purpose |
|
|
137
|
+
| ---------------- | ------------------------------------------------------ | --------------------------------------------------------- |
|
|
138
|
+
| `view-screen` | | See current UI state |
|
|
139
|
+
| `navigate` | `--view <name>` or `--path <url>` | Navigate the UI |
|
|
140
|
+
| `hello` | `[--name <name>]` | Example script |
|
|
141
|
+
| `db-schema` | | Show all tables, columns, types |
|
|
142
|
+
| `db-query` | `--sql "SELECT ..."` | Run a SELECT query |
|
|
143
|
+
| `list-mcp-tools` | `--serverId <id>` | List connected MCP tools visible to the authenticated app |
|
|
144
|
+
| `call-mcp-tool` | `--serverId <id> --toolName <name> --arguments <json>` | Call a connected MCP tool through the scoped app API |
|
|
143
145
|
|
|
144
146
|
**For data changes, pick the right surface:**
|
|
145
147
|
|