@agent-native/core 0.104.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 +64 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +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/deploy/build.ts +23 -0
- 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/clips/app/routes.ts +3 -1
- package/corpus/templates/dispatch/app/routes/integrations.tsx +57 -4
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/AgentPanel.js.map +1 -1
- 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/collab/routes.d.ts +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/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +17 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +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/notifications/routes.d.ts +3 -3
- 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/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +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/secrets/routes.d.ts +9 -9
- 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/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
|
@@ -355,6 +355,7 @@ export type ProviderApiAuthKind =
|
|
|
355
355
|
oauthProvider: string;
|
|
356
356
|
tokenLabel: string;
|
|
357
357
|
key: string;
|
|
358
|
+
fallbackKeys?: readonly string[];
|
|
358
359
|
header: string;
|
|
359
360
|
workspaceProvider: string;
|
|
360
361
|
}
|
|
@@ -363,6 +364,15 @@ export type ProviderApiAuthKind =
|
|
|
363
364
|
oauthProvider: string;
|
|
364
365
|
tokenLabel: string;
|
|
365
366
|
key: string;
|
|
367
|
+
fallbackKeys?: readonly string[];
|
|
368
|
+
workspaceProvider: string;
|
|
369
|
+
}
|
|
370
|
+
| {
|
|
371
|
+
type: "oauth-bearer-or-basic";
|
|
372
|
+
oauthProvider: string;
|
|
373
|
+
tokenLabel: string;
|
|
374
|
+
usernameKey: string;
|
|
375
|
+
passwordKey: string;
|
|
366
376
|
workspaceProvider: string;
|
|
367
377
|
}
|
|
368
378
|
| {
|
|
@@ -488,6 +498,11 @@ export interface ProviderApiRuntime {
|
|
|
488
498
|
): ReturnType<typeof listProviderApiCatalog> | Promise<unknown[]>;
|
|
489
499
|
fetchDocs(options: ProviderApiDocsOptions): Promise<unknown>;
|
|
490
500
|
executeRequest(args: ProviderApiRequestArgs): Promise<unknown>;
|
|
501
|
+
resolveOAuthAccessToken(args: {
|
|
502
|
+
provider: ProviderApiId;
|
|
503
|
+
connectionId?: string | null;
|
|
504
|
+
accountId?: string | null;
|
|
505
|
+
}): Promise<ProviderApiOAuthAccessToken>;
|
|
491
506
|
listGitHubRepositoryFiles(
|
|
492
507
|
args: GitHubRepositoryFileListArgs,
|
|
493
508
|
): Promise<GitHubRepositoryFileListResult>;
|
|
@@ -840,8 +855,10 @@ const PROVIDER_CONFIGS: Record<ProviderApiId, ProviderApiConfig> = {
|
|
|
840
855
|
label: "GitHub REST API",
|
|
841
856
|
defaultBaseUrl: "https://api.github.com",
|
|
842
857
|
auth: {
|
|
843
|
-
type: "bearer",
|
|
844
|
-
|
|
858
|
+
type: "oauth-bearer-or-bearer-key",
|
|
859
|
+
oauthProvider: "github",
|
|
860
|
+
tokenLabel: "GitHub OAuth token",
|
|
861
|
+
key: "GITHUB_TOKEN",
|
|
845
862
|
workspaceProvider: "github",
|
|
846
863
|
},
|
|
847
864
|
credentialKeys: ["GITHUB_TOKEN"],
|
|
@@ -1154,8 +1171,11 @@ const PROVIDER_CONFIGS: Record<ProviderApiId, ProviderApiConfig> = {
|
|
|
1154
1171
|
label: "HubSpot",
|
|
1155
1172
|
defaultBaseUrl: "https://api.hubapi.com",
|
|
1156
1173
|
auth: {
|
|
1157
|
-
type: "bearer",
|
|
1158
|
-
|
|
1174
|
+
type: "oauth-bearer-or-bearer-key",
|
|
1175
|
+
oauthProvider: "hubspot",
|
|
1176
|
+
tokenLabel: "HubSpot OAuth token",
|
|
1177
|
+
key: "HUBSPOT_PRIVATE_APP_TOKEN",
|
|
1178
|
+
fallbackKeys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
|
|
1159
1179
|
workspaceProvider: "hubspot",
|
|
1160
1180
|
},
|
|
1161
1181
|
credentialKeys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
|
|
@@ -1195,9 +1215,12 @@ const PROVIDER_CONFIGS: Record<ProviderApiId, ProviderApiConfig> = {
|
|
|
1195
1215
|
defaultBaseUrl: "https://example.atlassian.net",
|
|
1196
1216
|
baseUrlCredentialKey: "JIRA_BASE_URL",
|
|
1197
1217
|
auth: {
|
|
1198
|
-
type: "basic",
|
|
1218
|
+
type: "oauth-bearer-or-basic",
|
|
1219
|
+
oauthProvider: "jira",
|
|
1220
|
+
tokenLabel: "Jira",
|
|
1199
1221
|
usernameKey: "JIRA_USER_EMAIL",
|
|
1200
1222
|
passwordKey: "JIRA_API_TOKEN",
|
|
1223
|
+
workspaceProvider: "jira",
|
|
1201
1224
|
},
|
|
1202
1225
|
credentialKeys: ["JIRA_BASE_URL", "JIRA_USER_EMAIL", "JIRA_API_TOKEN"],
|
|
1203
1226
|
docsUrls: [
|
|
@@ -1329,8 +1352,12 @@ const PROVIDER_CONFIGS: Record<ProviderApiId, ProviderApiConfig> = {
|
|
|
1329
1352
|
label: "Sentry",
|
|
1330
1353
|
defaultBaseUrl: "https://sentry.io/api/0",
|
|
1331
1354
|
auth: {
|
|
1332
|
-
type: "bearer",
|
|
1333
|
-
|
|
1355
|
+
type: "oauth-bearer-or-bearer-key",
|
|
1356
|
+
oauthProvider: "sentry",
|
|
1357
|
+
tokenLabel: "Sentry OAuth token",
|
|
1358
|
+
key: "SENTRY_AUTH_TOKEN",
|
|
1359
|
+
fallbackKeys: ["SENTRY_AUTH_TOKEN", "SENTRY_SERVER_TOKEN"],
|
|
1360
|
+
workspaceProvider: "sentry",
|
|
1334
1361
|
},
|
|
1335
1362
|
credentialKeys: [
|
|
1336
1363
|
"SENTRY_AUTH_TOKEN",
|
|
@@ -1485,6 +1512,8 @@ export function createProviderApiRuntime(
|
|
|
1485
1512
|
fetchDocs: (docsOptions) =>
|
|
1486
1513
|
fetchProviderApiDocs(docsOptions, runtimeOptions),
|
|
1487
1514
|
executeRequest: (args) => executeProviderApiRequest(args, runtimeOptions),
|
|
1515
|
+
resolveOAuthAccessToken: (args) =>
|
|
1516
|
+
resolveProviderApiOAuthAccessToken(args, runtimeOptions),
|
|
1488
1517
|
listGitHubRepositoryFiles: (args) =>
|
|
1489
1518
|
listGitHubRepositoryFiles(args, runtimeOptions),
|
|
1490
1519
|
searchGitHubRepositoryFiles: (args) =>
|
|
@@ -1826,7 +1855,26 @@ export async function resolveProviderApiOAuthAccessToken(
|
|
|
1826
1855
|
await assertProviderAllowedAsync(args.provider, runtime);
|
|
1827
1856
|
const config = getProviderApiConfig(args.provider);
|
|
1828
1857
|
const auth = config.auth;
|
|
1829
|
-
|
|
1858
|
+
const oauthAuth =
|
|
1859
|
+
auth.type === "oauth-bearer"
|
|
1860
|
+
? auth
|
|
1861
|
+
: auth.type === "oauth-bearer-or-api-key-header" ||
|
|
1862
|
+
auth.type === "oauth-bearer-or-bearer-key"
|
|
1863
|
+
? {
|
|
1864
|
+
type: "oauth-bearer" as const,
|
|
1865
|
+
oauthProvider: auth.oauthProvider,
|
|
1866
|
+
tokenLabel: auth.tokenLabel,
|
|
1867
|
+
workspaceProvider: auth.workspaceProvider,
|
|
1868
|
+
}
|
|
1869
|
+
: auth.type === "oauth-bearer-or-basic"
|
|
1870
|
+
? {
|
|
1871
|
+
type: "oauth-bearer" as const,
|
|
1872
|
+
oauthProvider: auth.oauthProvider,
|
|
1873
|
+
tokenLabel: auth.tokenLabel,
|
|
1874
|
+
workspaceProvider: auth.workspaceProvider,
|
|
1875
|
+
}
|
|
1876
|
+
: null;
|
|
1877
|
+
if (!oauthAuth) {
|
|
1830
1878
|
throw new Error(
|
|
1831
1879
|
`Provider API ${args.provider} does not use a direct OAuth bearer token.`,
|
|
1832
1880
|
);
|
|
@@ -1835,21 +1883,25 @@ export async function resolveProviderApiOAuthAccessToken(
|
|
|
1835
1883
|
runtime,
|
|
1836
1884
|
config.credentialKeys[0] ?? config.id,
|
|
1837
1885
|
);
|
|
1838
|
-
const credential =
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1886
|
+
const credential = oauthAuth.workspaceProvider
|
|
1887
|
+
? await resolveOptionalConnectionBoundOAuthBearerToken({
|
|
1888
|
+
auth: oauthAuth,
|
|
1889
|
+
runtime,
|
|
1890
|
+
ctx,
|
|
1891
|
+
workspaceProvider: oauthAuth.workspaceProvider,
|
|
1892
|
+
connectionId: args.connectionId,
|
|
1893
|
+
accountId: args.accountId,
|
|
1894
|
+
})
|
|
1895
|
+
: await resolveOAuthBearerToken({
|
|
1896
|
+
auth: oauthAuth,
|
|
1897
|
+
ctx,
|
|
1898
|
+
accountId: args.accountId,
|
|
1899
|
+
});
|
|
1900
|
+
if (!credential) {
|
|
1901
|
+
throw new Error(
|
|
1902
|
+
`${oauthAuth.tokenLabel} workspace connection is not available to ${runtime.appId}.`,
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1853
1905
|
return {
|
|
1854
1906
|
accessToken: credential.value,
|
|
1855
1907
|
accountId: credential.accountId ?? null,
|
|
@@ -3061,10 +3113,13 @@ function describeAuth(auth: ProviderApiAuthKind): string {
|
|
|
3061
3113
|
if (auth.type === "google-service-account") return "google-service-account";
|
|
3062
3114
|
if (auth.type === "oauth-bearer") return `oauth-bearer:${auth.oauthProvider}`;
|
|
3063
3115
|
if (auth.type === "oauth-bearer-or-api-key-header") {
|
|
3064
|
-
return `oauth-bearer:${auth.oauthProvider}-or-api-key-header:${auth.header}`;
|
|
3116
|
+
return `oauth-bearer:${auth.oauthProvider}-or-api-key-header:${auth.header}:${(auth.fallbackKeys ?? [auth.key]).join(",")}`;
|
|
3065
3117
|
}
|
|
3066
3118
|
if (auth.type === "oauth-bearer-or-bearer-key") {
|
|
3067
|
-
return `oauth-bearer:${auth.oauthProvider}-or-bearer-key:${auth.key}`;
|
|
3119
|
+
return `oauth-bearer:${auth.oauthProvider}-or-bearer-key:${(auth.fallbackKeys ?? [auth.key]).join(",")}`;
|
|
3120
|
+
}
|
|
3121
|
+
if (auth.type === "oauth-bearer-or-basic") {
|
|
3122
|
+
return `oauth-bearer:${auth.oauthProvider}-or-basic:${auth.usernameKey}:${auth.passwordKey}`;
|
|
3068
3123
|
}
|
|
3069
3124
|
return "prometheus-basic-or-bearer";
|
|
3070
3125
|
}
|
|
@@ -3088,17 +3143,71 @@ async function resolveBaseUrl(
|
|
|
3088
3143
|
ctx: CredentialContext,
|
|
3089
3144
|
args: ProviderApiRequestArgs,
|
|
3090
3145
|
): Promise<string> {
|
|
3146
|
+
const oauthBaseUrl = await resolveWorkspaceOAuthBaseUrl(
|
|
3147
|
+
config,
|
|
3148
|
+
runtime,
|
|
3149
|
+
args,
|
|
3150
|
+
);
|
|
3151
|
+
if (oauthBaseUrl) return oauthBaseUrl;
|
|
3091
3152
|
if (!config.baseUrlCredentialKey) return config.defaultBaseUrl;
|
|
3153
|
+
const auth = config.auth;
|
|
3154
|
+
const workspaceProvider =
|
|
3155
|
+
auth.type === "oauth-bearer" ||
|
|
3156
|
+
auth.type === "oauth-bearer-or-api-key-header" ||
|
|
3157
|
+
auth.type === "oauth-bearer-or-bearer-key" ||
|
|
3158
|
+
auth.type === "oauth-bearer-or-basic"
|
|
3159
|
+
? auth.workspaceProvider
|
|
3160
|
+
: undefined;
|
|
3092
3161
|
const configured = await resolveCredentialValue({
|
|
3093
3162
|
config,
|
|
3094
3163
|
runtime,
|
|
3095
3164
|
ctx,
|
|
3096
3165
|
key: config.baseUrlCredentialKey,
|
|
3097
3166
|
args,
|
|
3167
|
+
workspaceProvider,
|
|
3098
3168
|
});
|
|
3099
3169
|
return (configured || config.defaultBaseUrl).replace(/\/+$/, "");
|
|
3100
3170
|
}
|
|
3101
3171
|
|
|
3172
|
+
async function resolveWorkspaceOAuthBaseUrl(
|
|
3173
|
+
config: ProviderApiConfig,
|
|
3174
|
+
runtime: ProviderApiRuntimeOptions,
|
|
3175
|
+
args: ProviderApiRequestArgs,
|
|
3176
|
+
): Promise<string | null> {
|
|
3177
|
+
const auth = config.auth;
|
|
3178
|
+
const workspaceProvider =
|
|
3179
|
+
auth.type === "oauth-bearer" ||
|
|
3180
|
+
auth.type === "oauth-bearer-or-api-key-header" ||
|
|
3181
|
+
auth.type === "oauth-bearer-or-bearer-key" ||
|
|
3182
|
+
auth.type === "oauth-bearer-or-basic"
|
|
3183
|
+
? auth.workspaceProvider
|
|
3184
|
+
: undefined;
|
|
3185
|
+
if (workspaceProvider !== "jira") return null;
|
|
3186
|
+
let resolved: Awaited<ReturnType<typeof resolveWorkspaceConnectionForApp>>;
|
|
3187
|
+
try {
|
|
3188
|
+
resolved = await resolveWorkspaceConnectionForApp({
|
|
3189
|
+
appId: runtime.appId,
|
|
3190
|
+
provider: workspaceProvider,
|
|
3191
|
+
connectionId: args.connectionId ?? undefined,
|
|
3192
|
+
requireConnected: true,
|
|
3193
|
+
});
|
|
3194
|
+
} catch (error) {
|
|
3195
|
+
if (
|
|
3196
|
+
error instanceof Error &&
|
|
3197
|
+
error.message === "Workspace connections require an authenticated user."
|
|
3198
|
+
) {
|
|
3199
|
+
return null;
|
|
3200
|
+
}
|
|
3201
|
+
throw error;
|
|
3202
|
+
}
|
|
3203
|
+
if (!resolved.available || !resolved.connection) return null;
|
|
3204
|
+
const connectionConfig = resolved.connection.config;
|
|
3205
|
+
if (connectionConfig.credentialMode !== "oauth") return null;
|
|
3206
|
+
const baseUrl = connectionConfig.atlassianApiBaseUrl;
|
|
3207
|
+
if (typeof baseUrl !== "string" || !baseUrl.trim()) return null;
|
|
3208
|
+
return baseUrl.replace(/\/+$/, "");
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3102
3211
|
async function resolvePlaceholders(
|
|
3103
3212
|
config: ProviderApiConfig,
|
|
3104
3213
|
runtime: ProviderApiRuntimeOptions,
|
|
@@ -3373,8 +3482,26 @@ async function resolveAuth(
|
|
|
3373
3482
|
};
|
|
3374
3483
|
}
|
|
3375
3484
|
if (auth.type === "oauth-bearer-or-api-key-header") {
|
|
3376
|
-
|
|
3377
|
-
|
|
3485
|
+
const explicitConnectionFallback =
|
|
3486
|
+
Boolean(args.connectionId?.trim()) && Boolean(runtime.resolveCredential);
|
|
3487
|
+
const resolvedFallback = explicitConnectionFallback
|
|
3488
|
+
? await resolveHybridFallbackCredential({
|
|
3489
|
+
auth,
|
|
3490
|
+
config,
|
|
3491
|
+
runtime,
|
|
3492
|
+
ctx,
|
|
3493
|
+
args,
|
|
3494
|
+
})
|
|
3495
|
+
: null;
|
|
3496
|
+
if (resolvedFallback) {
|
|
3497
|
+
return {
|
|
3498
|
+
headers: { [auth.header]: resolvedFallback.value },
|
|
3499
|
+
credentialSources: [omitCredentialValue(resolvedFallback)],
|
|
3500
|
+
secretValues: [resolvedFallback.value],
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
const workspaceCredential =
|
|
3504
|
+
await resolveOptionalConnectionBoundOAuthBearerToken({
|
|
3378
3505
|
auth: {
|
|
3379
3506
|
type: "oauth-bearer",
|
|
3380
3507
|
oauthProvider: auth.oauthProvider,
|
|
@@ -3385,17 +3512,35 @@ async function resolveAuth(
|
|
|
3385
3512
|
connectionId: args.connectionId,
|
|
3386
3513
|
accountId: args.accountId,
|
|
3387
3514
|
workspaceProvider: auth.workspaceProvider,
|
|
3515
|
+
allowUnbound: true,
|
|
3388
3516
|
});
|
|
3517
|
+
if (workspaceCredential) {
|
|
3389
3518
|
return {
|
|
3390
|
-
headers: { Authorization: `Bearer ${
|
|
3391
|
-
credentialSources: [omitCredentialValue(
|
|
3392
|
-
secretValues: [
|
|
3519
|
+
headers: { Authorization: `Bearer ${workspaceCredential.value}` },
|
|
3520
|
+
credentialSources: [omitCredentialValue(workspaceCredential)],
|
|
3521
|
+
secretValues: [workspaceCredential.value],
|
|
3393
3522
|
};
|
|
3394
3523
|
}
|
|
3395
|
-
|
|
3524
|
+
if (!explicitConnectionFallback) {
|
|
3525
|
+
const resolvedFallback = await resolveHybridFallbackCredential({
|
|
3526
|
+
auth,
|
|
3527
|
+
config,
|
|
3528
|
+
runtime,
|
|
3529
|
+
ctx,
|
|
3530
|
+
args,
|
|
3531
|
+
});
|
|
3532
|
+
if (resolvedFallback) {
|
|
3533
|
+
return {
|
|
3534
|
+
headers: { [auth.header]: resolvedFallback.value },
|
|
3535
|
+
credentialSources: [omitCredentialValue(resolvedFallback)],
|
|
3536
|
+
secretValues: [resolvedFallback.value],
|
|
3537
|
+
};
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
const credential = await resolveAnyCredential({
|
|
3396
3541
|
provider: config.id,
|
|
3397
3542
|
workspaceProvider: auth.workspaceProvider,
|
|
3398
|
-
|
|
3543
|
+
keys: auth.fallbackKeys ?? [auth.key],
|
|
3399
3544
|
ctx,
|
|
3400
3545
|
runtime,
|
|
3401
3546
|
});
|
|
@@ -3406,8 +3551,26 @@ async function resolveAuth(
|
|
|
3406
3551
|
};
|
|
3407
3552
|
}
|
|
3408
3553
|
if (auth.type === "oauth-bearer-or-bearer-key") {
|
|
3409
|
-
|
|
3410
|
-
|
|
3554
|
+
const explicitConnectionFallback =
|
|
3555
|
+
Boolean(args.connectionId?.trim()) && Boolean(runtime.resolveCredential);
|
|
3556
|
+
const resolvedFallback = explicitConnectionFallback
|
|
3557
|
+
? await resolveHybridFallbackCredential({
|
|
3558
|
+
auth,
|
|
3559
|
+
config,
|
|
3560
|
+
runtime,
|
|
3561
|
+
ctx,
|
|
3562
|
+
args,
|
|
3563
|
+
})
|
|
3564
|
+
: null;
|
|
3565
|
+
if (resolvedFallback) {
|
|
3566
|
+
return {
|
|
3567
|
+
headers: { Authorization: `Bearer ${resolvedFallback.value}` },
|
|
3568
|
+
credentialSources: [omitCredentialValue(resolvedFallback)],
|
|
3569
|
+
secretValues: [resolvedFallback.value],
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
const workspaceCredential =
|
|
3573
|
+
await resolveOptionalConnectionBoundOAuthBearerToken({
|
|
3411
3574
|
auth: {
|
|
3412
3575
|
type: "oauth-bearer",
|
|
3413
3576
|
oauthProvider: auth.oauthProvider,
|
|
@@ -3418,17 +3581,35 @@ async function resolveAuth(
|
|
|
3418
3581
|
connectionId: args.connectionId,
|
|
3419
3582
|
accountId: args.accountId,
|
|
3420
3583
|
workspaceProvider: auth.workspaceProvider,
|
|
3584
|
+
allowUnbound: true,
|
|
3421
3585
|
});
|
|
3586
|
+
if (workspaceCredential) {
|
|
3422
3587
|
return {
|
|
3423
|
-
headers: { Authorization: `Bearer ${
|
|
3424
|
-
credentialSources: [omitCredentialValue(
|
|
3425
|
-
secretValues: [
|
|
3588
|
+
headers: { Authorization: `Bearer ${workspaceCredential.value}` },
|
|
3589
|
+
credentialSources: [omitCredentialValue(workspaceCredential)],
|
|
3590
|
+
secretValues: [workspaceCredential.value],
|
|
3426
3591
|
};
|
|
3427
3592
|
}
|
|
3428
|
-
|
|
3593
|
+
if (!explicitConnectionFallback) {
|
|
3594
|
+
const resolvedFallback = await resolveHybridFallbackCredential({
|
|
3595
|
+
auth,
|
|
3596
|
+
config,
|
|
3597
|
+
runtime,
|
|
3598
|
+
ctx,
|
|
3599
|
+
args,
|
|
3600
|
+
});
|
|
3601
|
+
if (resolvedFallback) {
|
|
3602
|
+
return {
|
|
3603
|
+
headers: { Authorization: `Bearer ${resolvedFallback.value}` },
|
|
3604
|
+
credentialSources: [omitCredentialValue(resolvedFallback)],
|
|
3605
|
+
secretValues: [resolvedFallback.value],
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
const credential = await resolveAnyCredential({
|
|
3429
3610
|
provider: config.id,
|
|
3430
3611
|
workspaceProvider: auth.workspaceProvider,
|
|
3431
|
-
|
|
3612
|
+
keys: auth.fallbackKeys ?? [auth.key],
|
|
3432
3613
|
ctx,
|
|
3433
3614
|
runtime,
|
|
3434
3615
|
});
|
|
@@ -3438,6 +3619,56 @@ async function resolveAuth(
|
|
|
3438
3619
|
secretValues: [credential.value],
|
|
3439
3620
|
};
|
|
3440
3621
|
}
|
|
3622
|
+
if (auth.type === "oauth-bearer-or-basic") {
|
|
3623
|
+
const workspaceCredential =
|
|
3624
|
+
await resolveOptionalConnectionBoundOAuthBearerToken({
|
|
3625
|
+
auth: {
|
|
3626
|
+
type: "oauth-bearer",
|
|
3627
|
+
oauthProvider: auth.oauthProvider,
|
|
3628
|
+
tokenLabel: auth.tokenLabel,
|
|
3629
|
+
},
|
|
3630
|
+
runtime,
|
|
3631
|
+
ctx,
|
|
3632
|
+
connectionId: args.connectionId,
|
|
3633
|
+
accountId: args.accountId,
|
|
3634
|
+
workspaceProvider: auth.workspaceProvider,
|
|
3635
|
+
allowUnbound: true,
|
|
3636
|
+
});
|
|
3637
|
+
if (workspaceCredential) {
|
|
3638
|
+
return {
|
|
3639
|
+
headers: { Authorization: `Bearer ${workspaceCredential.value}` },
|
|
3640
|
+
credentialSources: [omitCredentialValue(workspaceCredential)],
|
|
3641
|
+
secretValues: [workspaceCredential.value],
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
const username = await resolveRequiredCredential({
|
|
3645
|
+
provider: config.id,
|
|
3646
|
+
workspaceProvider: auth.workspaceProvider,
|
|
3647
|
+
key: auth.usernameKey,
|
|
3648
|
+
ctx,
|
|
3649
|
+
runtime,
|
|
3650
|
+
connectionId: args.connectionId,
|
|
3651
|
+
});
|
|
3652
|
+
const password = await resolveRequiredCredential({
|
|
3653
|
+
provider: config.id,
|
|
3654
|
+
workspaceProvider: auth.workspaceProvider,
|
|
3655
|
+
key: auth.passwordKey,
|
|
3656
|
+
ctx,
|
|
3657
|
+
runtime,
|
|
3658
|
+
connectionId: args.connectionId,
|
|
3659
|
+
});
|
|
3660
|
+
const encoded = Buffer.from(`${username.value}:${password.value}`).toString(
|
|
3661
|
+
"base64",
|
|
3662
|
+
);
|
|
3663
|
+
return {
|
|
3664
|
+
headers: { Authorization: `Basic ${encoded}` },
|
|
3665
|
+
credentialSources: [
|
|
3666
|
+
omitCredentialValue(username),
|
|
3667
|
+
omitCredentialValue(password),
|
|
3668
|
+
],
|
|
3669
|
+
secretValues: [username.value, password.value, encoded],
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3441
3672
|
|
|
3442
3673
|
const bearer = await resolveCredentialValue({
|
|
3443
3674
|
config,
|
|
@@ -3499,6 +3730,28 @@ function emptyAuth(): ResolvedAuth {
|
|
|
3499
3730
|
return { headers: {}, credentialSources: [], secretValues: [] };
|
|
3500
3731
|
}
|
|
3501
3732
|
|
|
3733
|
+
async function resolveHybridFallbackCredential(options: {
|
|
3734
|
+
auth:
|
|
3735
|
+
| Extract<ProviderApiAuthKind, { type: "oauth-bearer-or-api-key-header" }>
|
|
3736
|
+
| Extract<ProviderApiAuthKind, { type: "oauth-bearer-or-bearer-key" }>;
|
|
3737
|
+
config: ProviderApiConfig;
|
|
3738
|
+
runtime: ProviderApiRuntimeOptions;
|
|
3739
|
+
ctx: CredentialContext;
|
|
3740
|
+
args: ProviderApiRequestArgs;
|
|
3741
|
+
}): Promise<ProviderApiResolvedCredential | null> {
|
|
3742
|
+
// App-specific resolvers keep existing provider credentials working when a
|
|
3743
|
+
// caller explicitly selects a connection or account.
|
|
3744
|
+
if (!options.runtime.resolveCredential) return null;
|
|
3745
|
+
return resolveOptionalCredential({
|
|
3746
|
+
provider: options.config.id,
|
|
3747
|
+
workspaceProvider: options.auth.workspaceProvider,
|
|
3748
|
+
key: options.auth.key,
|
|
3749
|
+
ctx: options.ctx,
|
|
3750
|
+
runtime: options.runtime,
|
|
3751
|
+
connectionId: options.args.connectionId,
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3502
3755
|
async function resolveAnyCredential(options: {
|
|
3503
3756
|
provider: ProviderApiId;
|
|
3504
3757
|
workspaceProvider: string | undefined;
|
|
@@ -3648,14 +3901,16 @@ async function resolveOAuthBearerToken(options: {
|
|
|
3648
3901
|
auth: Extract<ProviderApiAuthKind, { type: "oauth-bearer" }>;
|
|
3649
3902
|
ctx: CredentialContext;
|
|
3650
3903
|
accountId?: string | null;
|
|
3904
|
+
ownerEmail?: string | null;
|
|
3651
3905
|
}): Promise<ProviderApiResolvedCredential> {
|
|
3906
|
+
const ownerEmail = options.ownerEmail?.trim() || options.ctx.userEmail;
|
|
3652
3907
|
const accounts = await listOAuthAccountsByOwner(
|
|
3653
3908
|
options.auth.oauthProvider,
|
|
3654
|
-
|
|
3909
|
+
ownerEmail,
|
|
3655
3910
|
);
|
|
3656
3911
|
if (accounts.length === 0) {
|
|
3657
3912
|
throw new Error(
|
|
3658
|
-
`${options.auth.tokenLabel} is not connected for ${
|
|
3913
|
+
`${options.auth.tokenLabel} is not connected for ${ownerEmail}.`,
|
|
3659
3914
|
);
|
|
3660
3915
|
}
|
|
3661
3916
|
const accountId = options.accountId?.trim();
|
|
@@ -3664,14 +3919,14 @@ async function resolveOAuthBearerToken(options: {
|
|
|
3664
3919
|
: accounts[0];
|
|
3665
3920
|
if (!account) {
|
|
3666
3921
|
throw new Error(
|
|
3667
|
-
`${options.auth.tokenLabel} account ${accountId} is not available to ${
|
|
3922
|
+
`${options.auth.tokenLabel} account ${accountId} is not available to ${ownerEmail}.`,
|
|
3668
3923
|
);
|
|
3669
3924
|
}
|
|
3670
3925
|
const tokens = account.tokens as OAuthTokens;
|
|
3671
3926
|
const token = await getValidOAuthAccessToken({
|
|
3672
3927
|
oauthProvider: options.auth.oauthProvider,
|
|
3673
3928
|
accountId: account.accountId,
|
|
3674
|
-
ownerEmail
|
|
3929
|
+
ownerEmail,
|
|
3675
3930
|
tokens,
|
|
3676
3931
|
});
|
|
3677
3932
|
return {
|
|
@@ -3692,25 +3947,50 @@ async function resolveConnectionBoundOAuthBearerToken(options: {
|
|
|
3692
3947
|
connectionId?: string | null;
|
|
3693
3948
|
accountId?: string | null;
|
|
3694
3949
|
}): Promise<ProviderApiResolvedCredential> {
|
|
3950
|
+
const credential =
|
|
3951
|
+
await resolveOptionalConnectionBoundOAuthBearerToken(options);
|
|
3952
|
+
if (credential) return credential;
|
|
3953
|
+
throw new Error(
|
|
3954
|
+
`${options.auth.tokenLabel} requires an available workspace connection.`,
|
|
3955
|
+
);
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
async function resolveOptionalConnectionBoundOAuthBearerToken(options: {
|
|
3959
|
+
auth: Extract<ProviderApiAuthKind, { type: "oauth-bearer" }>;
|
|
3960
|
+
runtime: ProviderApiRuntimeOptions;
|
|
3961
|
+
ctx: CredentialContext;
|
|
3962
|
+
workspaceProvider: string;
|
|
3963
|
+
connectionId?: string | null;
|
|
3964
|
+
accountId?: string | null;
|
|
3965
|
+
allowUnbound?: boolean;
|
|
3966
|
+
}): Promise<ProviderApiResolvedCredential | null> {
|
|
3695
3967
|
const requestedConnectionId = options.connectionId?.trim();
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3968
|
+
let resolved: Awaited<ReturnType<typeof resolveWorkspaceConnectionForApp>>;
|
|
3969
|
+
try {
|
|
3970
|
+
resolved = await resolveWorkspaceConnectionForApp({
|
|
3971
|
+
appId: options.runtime.appId,
|
|
3972
|
+
provider: options.workspaceProvider,
|
|
3973
|
+
connectionId: requestedConnectionId,
|
|
3974
|
+
requireConnected: true,
|
|
3975
|
+
});
|
|
3976
|
+
} catch (error) {
|
|
3977
|
+
if (
|
|
3978
|
+
error instanceof Error &&
|
|
3979
|
+
error.message === "Workspace connections require an authenticated user."
|
|
3980
|
+
) {
|
|
3981
|
+
return null;
|
|
3982
|
+
}
|
|
3983
|
+
throw error;
|
|
3700
3984
|
}
|
|
3701
|
-
const resolved = await resolveWorkspaceConnectionForApp({
|
|
3702
|
-
appId: options.runtime.appId,
|
|
3703
|
-
provider: options.workspaceProvider,
|
|
3704
|
-
connectionId: requestedConnectionId,
|
|
3705
|
-
requireConnected: true,
|
|
3706
|
-
});
|
|
3707
3985
|
if (!resolved.available || !resolved.connection) {
|
|
3708
|
-
throw new Error(resolved.reason);
|
|
3986
|
+
if (requestedConnectionId) throw new Error(resolved.reason);
|
|
3987
|
+
return null;
|
|
3709
3988
|
}
|
|
3710
3989
|
const connectionAccountId = resolved.connection.accountId?.trim();
|
|
3711
3990
|
if (!connectionAccountId) {
|
|
3991
|
+
if (options.allowUnbound) return null;
|
|
3712
3992
|
throw new Error(
|
|
3713
|
-
`${options.auth.tokenLabel} workspace connection ${
|
|
3993
|
+
`${options.auth.tokenLabel} workspace connection ${resolved.connection.id} is missing its OAuth account binding.`,
|
|
3714
3994
|
);
|
|
3715
3995
|
}
|
|
3716
3996
|
const requestedAccountId = options.accountId?.trim();
|
|
@@ -3723,6 +4003,7 @@ async function resolveConnectionBoundOAuthBearerToken(options: {
|
|
|
3723
4003
|
auth: options.auth,
|
|
3724
4004
|
ctx: options.ctx,
|
|
3725
4005
|
accountId: connectionAccountId,
|
|
4006
|
+
ownerEmail: resolved.connection.ownerEmail,
|
|
3726
4007
|
});
|
|
3727
4008
|
return {
|
|
3728
4009
|
...credential,
|
|
@@ -3768,6 +4049,18 @@ async function getValidOAuthAccessToken(options: {
|
|
|
3768
4049
|
if (options.oauthProvider === "notion") {
|
|
3769
4050
|
return refreshNotionOAuthToken(options, refreshToken);
|
|
3770
4051
|
}
|
|
4052
|
+
if (options.oauthProvider === "github") {
|
|
4053
|
+
return refreshGitHubOAuthToken(options, refreshToken);
|
|
4054
|
+
}
|
|
4055
|
+
if (options.oauthProvider === "hubspot") {
|
|
4056
|
+
return refreshHubSpotOAuthToken(options, refreshToken);
|
|
4057
|
+
}
|
|
4058
|
+
if (options.oauthProvider === "jira") {
|
|
4059
|
+
return refreshJiraOAuthToken(options, refreshToken);
|
|
4060
|
+
}
|
|
4061
|
+
if (options.oauthProvider === "sentry") {
|
|
4062
|
+
return refreshSentryOAuthToken(options, refreshToken);
|
|
4063
|
+
}
|
|
3771
4064
|
throw new Error(
|
|
3772
4065
|
`${options.oauthProvider} OAuth token is expired and automatic refresh is not configured for provider-api.`,
|
|
3773
4066
|
);
|
|
@@ -3888,6 +4181,322 @@ async function refreshNotionOAuthToken(
|
|
|
3888
4181
|
return data.access_token;
|
|
3889
4182
|
}
|
|
3890
4183
|
|
|
4184
|
+
async function refreshGitHubOAuthToken(
|
|
4185
|
+
options: {
|
|
4186
|
+
oauthProvider: string;
|
|
4187
|
+
accountId: string;
|
|
4188
|
+
ownerEmail: string;
|
|
4189
|
+
tokens: OAuthTokens;
|
|
4190
|
+
},
|
|
4191
|
+
refreshToken: string,
|
|
4192
|
+
): Promise<string> {
|
|
4193
|
+
const credentials = await resolveOAuthClientCredentialCandidates("GITHUB");
|
|
4194
|
+
if (!credentials.length) {
|
|
4195
|
+
throw new Error(
|
|
4196
|
+
"GITHUB_CLIENT_ID/SECRET not set for GitHub OAuth refresh.",
|
|
4197
|
+
);
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
let response: Response | null = null;
|
|
4201
|
+
let data: {
|
|
4202
|
+
access_token?: string;
|
|
4203
|
+
refresh_token?: string;
|
|
4204
|
+
expires_in?: number;
|
|
4205
|
+
token_type?: string;
|
|
4206
|
+
} = {};
|
|
4207
|
+
for (const credential of credentials) {
|
|
4208
|
+
const result = await fetchBoundedOAuthRefreshJson<{
|
|
4209
|
+
access_token?: string;
|
|
4210
|
+
refresh_token?: string;
|
|
4211
|
+
expires_in?: number;
|
|
4212
|
+
token_type?: string;
|
|
4213
|
+
}>(
|
|
4214
|
+
"https://github.com/login/oauth/access_token",
|
|
4215
|
+
{
|
|
4216
|
+
method: "POST",
|
|
4217
|
+
headers: {
|
|
4218
|
+
Accept: "application/json",
|
|
4219
|
+
"Content-Type": "application/json",
|
|
4220
|
+
},
|
|
4221
|
+
body: JSON.stringify({
|
|
4222
|
+
client_id: credential.clientId,
|
|
4223
|
+
client_secret: credential.clientSecret,
|
|
4224
|
+
grant_type: "refresh_token",
|
|
4225
|
+
refresh_token: refreshToken,
|
|
4226
|
+
}),
|
|
4227
|
+
},
|
|
4228
|
+
"GitHub",
|
|
4229
|
+
);
|
|
4230
|
+
response = result.response;
|
|
4231
|
+
data = result.data;
|
|
4232
|
+
if (response.ok && data.access_token) break;
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
if (!response?.ok || !data.access_token) {
|
|
4236
|
+
throw new Error(`GitHub OAuth refresh failed (${response?.status ?? 0}).`);
|
|
4237
|
+
}
|
|
4238
|
+
const updated = {
|
|
4239
|
+
...options.tokens,
|
|
4240
|
+
access_token: data.access_token,
|
|
4241
|
+
refresh_token: data.refresh_token ?? refreshToken,
|
|
4242
|
+
token_type: data.token_type ?? options.tokens.token_type,
|
|
4243
|
+
...(Number.isFinite(data.expires_in) && (data.expires_in ?? 0) > 0
|
|
4244
|
+
? { expiry_date: Date.now() + (data.expires_in as number) * 1_000 }
|
|
4245
|
+
: {}),
|
|
4246
|
+
};
|
|
4247
|
+
await saveOAuthTokens(
|
|
4248
|
+
options.oauthProvider,
|
|
4249
|
+
options.accountId,
|
|
4250
|
+
updated as unknown as Record<string, unknown>,
|
|
4251
|
+
options.ownerEmail,
|
|
4252
|
+
);
|
|
4253
|
+
return data.access_token;
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
async function saveOAuthTokensAndLinkedJiraAccounts(options: {
|
|
4257
|
+
options: {
|
|
4258
|
+
oauthProvider: string;
|
|
4259
|
+
accountId: string;
|
|
4260
|
+
ownerEmail: string;
|
|
4261
|
+
tokens: OAuthTokens;
|
|
4262
|
+
};
|
|
4263
|
+
previousRefreshToken: string;
|
|
4264
|
+
updated: OAuthTokens;
|
|
4265
|
+
}): Promise<void> {
|
|
4266
|
+
const serializedTokens = options.updated as unknown as Record<
|
|
4267
|
+
string,
|
|
4268
|
+
unknown
|
|
4269
|
+
>;
|
|
4270
|
+
await saveOAuthTokens(
|
|
4271
|
+
options.options.oauthProvider,
|
|
4272
|
+
options.options.accountId,
|
|
4273
|
+
serializedTokens,
|
|
4274
|
+
options.options.ownerEmail,
|
|
4275
|
+
);
|
|
4276
|
+
|
|
4277
|
+
const accounts = await listOAuthAccountsByOwner(
|
|
4278
|
+
options.options.oauthProvider,
|
|
4279
|
+
options.options.ownerEmail,
|
|
4280
|
+
);
|
|
4281
|
+
for (const account of accounts) {
|
|
4282
|
+
if (account.accountId === options.options.accountId) continue;
|
|
4283
|
+
const accountRefreshToken =
|
|
4284
|
+
(account.tokens.refresh_token as string | undefined) ??
|
|
4285
|
+
(account.tokens.refreshToken as string | undefined);
|
|
4286
|
+
if (accountRefreshToken !== options.previousRefreshToken) continue;
|
|
4287
|
+
await saveOAuthTokens(
|
|
4288
|
+
options.options.oauthProvider,
|
|
4289
|
+
account.accountId,
|
|
4290
|
+
serializedTokens,
|
|
4291
|
+
options.options.ownerEmail,
|
|
4292
|
+
);
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
async function refreshHubSpotOAuthToken(
|
|
4297
|
+
options: {
|
|
4298
|
+
oauthProvider: string;
|
|
4299
|
+
accountId: string;
|
|
4300
|
+
ownerEmail: string;
|
|
4301
|
+
tokens: OAuthTokens;
|
|
4302
|
+
},
|
|
4303
|
+
refreshToken: string,
|
|
4304
|
+
): Promise<string> {
|
|
4305
|
+
const credentials = await resolveOAuthClientCredentialCandidates("HUBSPOT");
|
|
4306
|
+
if (!credentials.length) {
|
|
4307
|
+
throw new Error(
|
|
4308
|
+
"HUBSPOT_CLIENT_ID/SECRET not set for HubSpot OAuth refresh.",
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
let response: Response | null = null;
|
|
4313
|
+
let data: {
|
|
4314
|
+
access_token?: string;
|
|
4315
|
+
refresh_token?: string;
|
|
4316
|
+
expires_in?: number;
|
|
4317
|
+
token_type?: string;
|
|
4318
|
+
} = {};
|
|
4319
|
+
for (const credential of credentials) {
|
|
4320
|
+
const result = await fetchBoundedOAuthRefreshJson<{
|
|
4321
|
+
access_token?: string;
|
|
4322
|
+
refresh_token?: string;
|
|
4323
|
+
expires_in?: number;
|
|
4324
|
+
token_type?: string;
|
|
4325
|
+
}>(
|
|
4326
|
+
"https://api.hubapi.com/oauth/v3/token",
|
|
4327
|
+
{
|
|
4328
|
+
method: "POST",
|
|
4329
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
4330
|
+
body: new URLSearchParams({
|
|
4331
|
+
grant_type: "refresh_token",
|
|
4332
|
+
client_id: credential.clientId,
|
|
4333
|
+
client_secret: credential.clientSecret,
|
|
4334
|
+
refresh_token: refreshToken,
|
|
4335
|
+
}),
|
|
4336
|
+
},
|
|
4337
|
+
"HubSpot",
|
|
4338
|
+
);
|
|
4339
|
+
response = result.response;
|
|
4340
|
+
data = result.data;
|
|
4341
|
+
if (response.ok && data.access_token) break;
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
if (!response?.ok || !data.access_token) {
|
|
4345
|
+
throw new Error(`HubSpot OAuth refresh failed (${response?.status ?? 0}).`);
|
|
4346
|
+
}
|
|
4347
|
+
const updated = {
|
|
4348
|
+
...options.tokens,
|
|
4349
|
+
access_token: data.access_token,
|
|
4350
|
+
refresh_token: data.refresh_token ?? refreshToken,
|
|
4351
|
+
token_type: data.token_type ?? options.tokens.token_type,
|
|
4352
|
+
...(Number.isFinite(data.expires_in) && (data.expires_in ?? 0) > 0
|
|
4353
|
+
? { expiry_date: Date.now() + (data.expires_in as number) * 1_000 }
|
|
4354
|
+
: {}),
|
|
4355
|
+
};
|
|
4356
|
+
await saveOAuthTokens(
|
|
4357
|
+
options.oauthProvider,
|
|
4358
|
+
options.accountId,
|
|
4359
|
+
updated as unknown as Record<string, unknown>,
|
|
4360
|
+
options.ownerEmail,
|
|
4361
|
+
);
|
|
4362
|
+
return data.access_token;
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
async function refreshJiraOAuthToken(
|
|
4366
|
+
options: {
|
|
4367
|
+
oauthProvider: string;
|
|
4368
|
+
accountId: string;
|
|
4369
|
+
ownerEmail: string;
|
|
4370
|
+
tokens: OAuthTokens;
|
|
4371
|
+
},
|
|
4372
|
+
refreshToken: string,
|
|
4373
|
+
): Promise<string> {
|
|
4374
|
+
const credentials = await resolveOAuthClientCredentialCandidates("JIRA");
|
|
4375
|
+
if (!credentials.length) {
|
|
4376
|
+
throw new Error("JIRA_CLIENT_ID/SECRET not set for Jira OAuth refresh.");
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
let response: Response | null = null;
|
|
4380
|
+
let data: {
|
|
4381
|
+
access_token?: string;
|
|
4382
|
+
refresh_token?: string;
|
|
4383
|
+
expires_in?: number;
|
|
4384
|
+
token_type?: string;
|
|
4385
|
+
} = {};
|
|
4386
|
+
for (const credential of credentials) {
|
|
4387
|
+
const result = await fetchBoundedOAuthRefreshJson<{
|
|
4388
|
+
access_token?: string;
|
|
4389
|
+
refresh_token?: string;
|
|
4390
|
+
expires_in?: number;
|
|
4391
|
+
token_type?: string;
|
|
4392
|
+
}>(
|
|
4393
|
+
"https://auth.atlassian.com/oauth/token",
|
|
4394
|
+
{
|
|
4395
|
+
method: "POST",
|
|
4396
|
+
headers: { "Content-Type": "application/json" },
|
|
4397
|
+
body: JSON.stringify({
|
|
4398
|
+
grant_type: "refresh_token",
|
|
4399
|
+
client_id: credential.clientId,
|
|
4400
|
+
client_secret: credential.clientSecret,
|
|
4401
|
+
refresh_token: refreshToken,
|
|
4402
|
+
}),
|
|
4403
|
+
},
|
|
4404
|
+
"Jira",
|
|
4405
|
+
);
|
|
4406
|
+
response = result.response;
|
|
4407
|
+
data = result.data;
|
|
4408
|
+
if (response.ok && data.access_token) break;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
if (!response?.ok || !data.access_token) {
|
|
4412
|
+
throw new Error(`Jira OAuth refresh failed (${response?.status ?? 0}).`);
|
|
4413
|
+
}
|
|
4414
|
+
const updated = {
|
|
4415
|
+
...options.tokens,
|
|
4416
|
+
access_token: data.access_token,
|
|
4417
|
+
refresh_token: data.refresh_token ?? refreshToken,
|
|
4418
|
+
token_type: data.token_type ?? options.tokens.token_type,
|
|
4419
|
+
...(Number.isFinite(data.expires_in) && (data.expires_in ?? 0) > 0
|
|
4420
|
+
? { expiry_date: Date.now() + (data.expires_in as number) * 1_000 }
|
|
4421
|
+
: {}),
|
|
4422
|
+
};
|
|
4423
|
+
await saveOAuthTokensAndLinkedJiraAccounts({
|
|
4424
|
+
options,
|
|
4425
|
+
previousRefreshToken: refreshToken,
|
|
4426
|
+
updated,
|
|
4427
|
+
});
|
|
4428
|
+
return data.access_token;
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
async function refreshSentryOAuthToken(
|
|
4432
|
+
options: {
|
|
4433
|
+
oauthProvider: string;
|
|
4434
|
+
accountId: string;
|
|
4435
|
+
ownerEmail: string;
|
|
4436
|
+
tokens: OAuthTokens;
|
|
4437
|
+
},
|
|
4438
|
+
refreshToken: string,
|
|
4439
|
+
): Promise<string> {
|
|
4440
|
+
const credentials = await resolveOAuthClientCredentialCandidates("SENTRY");
|
|
4441
|
+
if (!credentials.length) {
|
|
4442
|
+
throw new Error(
|
|
4443
|
+
"SENTRY_CLIENT_ID/SECRET not set for Sentry OAuth refresh.",
|
|
4444
|
+
);
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
let response: Response | null = null;
|
|
4448
|
+
let data: {
|
|
4449
|
+
access_token?: string;
|
|
4450
|
+
refresh_token?: string;
|
|
4451
|
+
expires_in?: number;
|
|
4452
|
+
token_type?: string;
|
|
4453
|
+
} = {};
|
|
4454
|
+
for (const credential of credentials) {
|
|
4455
|
+
const result = await fetchBoundedOAuthRefreshJson<{
|
|
4456
|
+
access_token?: string;
|
|
4457
|
+
refresh_token?: string;
|
|
4458
|
+
expires_in?: number;
|
|
4459
|
+
token_type?: string;
|
|
4460
|
+
}>(
|
|
4461
|
+
"https://sentry.io/oauth/token/",
|
|
4462
|
+
{
|
|
4463
|
+
method: "POST",
|
|
4464
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
4465
|
+
body: new URLSearchParams({
|
|
4466
|
+
grant_type: "refresh_token",
|
|
4467
|
+
client_id: credential.clientId,
|
|
4468
|
+
client_secret: credential.clientSecret,
|
|
4469
|
+
refresh_token: refreshToken,
|
|
4470
|
+
}),
|
|
4471
|
+
},
|
|
4472
|
+
"Sentry",
|
|
4473
|
+
);
|
|
4474
|
+
response = result.response;
|
|
4475
|
+
data = result.data;
|
|
4476
|
+
if (response.ok && data.access_token) break;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
if (!response?.ok || !data.access_token) {
|
|
4480
|
+
throw new Error(`Sentry OAuth refresh failed (${response?.status ?? 0}).`);
|
|
4481
|
+
}
|
|
4482
|
+
const updated = {
|
|
4483
|
+
...options.tokens,
|
|
4484
|
+
access_token: data.access_token,
|
|
4485
|
+
refresh_token: data.refresh_token ?? refreshToken,
|
|
4486
|
+
token_type: data.token_type ?? options.tokens.token_type,
|
|
4487
|
+
...(Number.isFinite(data.expires_in) && (data.expires_in ?? 0) > 0
|
|
4488
|
+
? { expiry_date: Date.now() + (data.expires_in as number) * 1_000 }
|
|
4489
|
+
: {}),
|
|
4490
|
+
};
|
|
4491
|
+
await saveOAuthTokens(
|
|
4492
|
+
options.oauthProvider,
|
|
4493
|
+
options.accountId,
|
|
4494
|
+
updated as unknown as Record<string, unknown>,
|
|
4495
|
+
options.ownerEmail,
|
|
4496
|
+
);
|
|
4497
|
+
return data.access_token;
|
|
4498
|
+
}
|
|
4499
|
+
|
|
3891
4500
|
const OAUTH_REFRESH_TIMEOUT_MS = 10_000;
|
|
3892
4501
|
const OAUTH_REFRESH_MAX_BYTES = 256 * 1024;
|
|
3893
4502
|
|
|
@@ -4047,6 +4656,32 @@ function dedupeGoogleOAuthCredentials(
|
|
|
4047
4656
|
});
|
|
4048
4657
|
}
|
|
4049
4658
|
|
|
4659
|
+
async function resolveOAuthClientCredentialCandidates(
|
|
4660
|
+
provider: string,
|
|
4661
|
+
): Promise<Array<{ clientId: string; clientSecret: string }>> {
|
|
4662
|
+
const [clientId, clientSecret, integrationClientId, integrationClientSecret] =
|
|
4663
|
+
await Promise.all([
|
|
4664
|
+
resolveSecret(`${provider}_CLIENT_ID`),
|
|
4665
|
+
resolveSecret(`${provider}_CLIENT_SECRET`),
|
|
4666
|
+
resolveSecret(`${provider}_INTEGRATION_CLIENT_ID`),
|
|
4667
|
+
resolveSecret(`${provider}_INTEGRATION_CLIENT_SECRET`),
|
|
4668
|
+
]);
|
|
4669
|
+
const candidates = [
|
|
4670
|
+
integrationClientId && integrationClientSecret
|
|
4671
|
+
? { clientId: integrationClientId, clientSecret: integrationClientSecret }
|
|
4672
|
+
: null,
|
|
4673
|
+
clientId && clientSecret ? { clientId, clientSecret } : null,
|
|
4674
|
+
].filter((value): value is { clientId: string; clientSecret: string } =>
|
|
4675
|
+
Boolean(value),
|
|
4676
|
+
);
|
|
4677
|
+
const seen = new Set<string>();
|
|
4678
|
+
return candidates.filter((candidate) => {
|
|
4679
|
+
if (seen.has(candidate.clientId)) return false;
|
|
4680
|
+
seen.add(candidate.clientId);
|
|
4681
|
+
return true;
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4050
4685
|
function normalizeMethod(
|
|
4051
4686
|
method: ProviderApiMethod | undefined,
|
|
4052
4687
|
): ProviderApiMethod {
|