@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.
Files changed (139) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +33 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AssistantChat.tsx +4 -0
  5. package/corpus/core/src/client/MultiTabAssistantChat.tsx +5 -1
  6. package/corpus/core/src/client/composer/PromptComposer.tsx +8 -0
  7. package/corpus/core/src/client/composer/TiptapComposer.tsx +38 -1
  8. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +107 -32
  9. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +33 -0
  10. package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
  11. package/corpus/core/src/client/use-chat-models.ts +10 -2
  12. package/corpus/core/src/connections/catalog.ts +107 -6
  13. package/corpus/core/src/localization/default-messages.ts +2 -0
  14. package/corpus/core/src/mcp/actions/call-mcp-tool.ts +18 -0
  15. package/corpus/core/src/mcp/actions/list-mcp-tools.ts +18 -0
  16. package/corpus/core/src/mcp-client/app-api.ts +136 -0
  17. package/corpus/core/src/mcp-client/index.ts +21 -0
  18. package/corpus/core/src/mcp-client/oauth-client.ts +462 -0
  19. package/corpus/core/src/mcp-client/oauth-routes.ts +307 -0
  20. package/corpus/core/src/mcp-client/remote-store.ts +93 -118
  21. package/corpus/core/src/mcp-client/remote-url.ts +97 -0
  22. package/corpus/core/src/mcp-client/routes.ts +11 -0
  23. package/corpus/core/src/oauth-tokens/store.ts +10 -4
  24. package/corpus/core/src/provider-api/index.ts +690 -55
  25. package/corpus/core/src/secrets/register-framework-secrets.ts +30 -0
  26. package/corpus/core/src/server/action-discovery.ts +2 -0
  27. package/corpus/core/src/server/core-routes-plugin.ts +9 -1
  28. package/corpus/core/src/server/workspace-provider-oauth.ts +448 -61
  29. package/corpus/core/src/templates/default/AGENTS.md +9 -7
  30. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
  31. package/corpus/templates/analytics/app/lib/data-source-status.ts +2 -0
  32. package/corpus/templates/analytics/app/pages/DataSources.tsx +61 -0
  33. package/corpus/templates/analytics/server/handlers/jira.ts +7 -0
  34. package/corpus/templates/analytics/server/lib/jira.ts +14 -42
  35. package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +11 -0
  36. package/corpus/templates/dispatch/app/routes/integrations.tsx +57 -4
  37. package/dist/client/AssistantChat.d.ts +2 -0
  38. package/dist/client/AssistantChat.d.ts.map +1 -1
  39. package/dist/client/AssistantChat.js +2 -2
  40. package/dist/client/AssistantChat.js.map +1 -1
  41. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  42. package/dist/client/MultiTabAssistantChat.js +5 -2
  43. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  44. package/dist/client/composer/PromptComposer.d.ts +2 -0
  45. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  46. package/dist/client/composer/PromptComposer.js +5 -2
  47. package/dist/client/composer/PromptComposer.js.map +1 -1
  48. package/dist/client/composer/TiptapComposer.d.ts +4 -1
  49. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  50. package/dist/client/composer/TiptapComposer.js +13 -5
  51. package/dist/client/composer/TiptapComposer.js.map +1 -1
  52. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  53. package/dist/client/resources/McpIntegrationDialog.js +58 -14
  54. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  55. package/dist/client/resources/mcp-integration-catalog.d.ts +9 -0
  56. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  57. package/dist/client/resources/mcp-integration-catalog.js +13 -0
  58. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  59. package/dist/client/resources/use-mcp-servers.d.ts +1 -0
  60. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
  61. package/dist/client/resources/use-mcp-servers.js.map +1 -1
  62. package/dist/client/use-chat-models.d.ts +1 -0
  63. package/dist/client/use-chat-models.d.ts.map +1 -1
  64. package/dist/client/use-chat-models.js +8 -2
  65. package/dist/client/use-chat-models.js.map +1 -1
  66. package/dist/collab/awareness.d.ts +2 -2
  67. package/dist/collab/awareness.d.ts.map +1 -1
  68. package/dist/connections/catalog.d.ts +78 -7
  69. package/dist/connections/catalog.d.ts.map +1 -1
  70. package/dist/connections/catalog.js +97 -6
  71. package/dist/connections/catalog.js.map +1 -1
  72. package/dist/localization/default-messages.d.ts +2 -0
  73. package/dist/localization/default-messages.d.ts.map +1 -1
  74. package/dist/localization/default-messages.js +2 -0
  75. package/dist/localization/default-messages.js.map +1 -1
  76. package/dist/mcp/actions/call-mcp-tool.d.ts +7 -0
  77. package/dist/mcp/actions/call-mcp-tool.d.ts.map +1 -0
  78. package/dist/mcp/actions/call-mcp-tool.js +15 -0
  79. package/dist/mcp/actions/call-mcp-tool.js.map +1 -0
  80. package/dist/mcp/actions/list-mcp-tools.d.ts +5 -0
  81. package/dist/mcp/actions/list-mcp-tools.d.ts.map +1 -0
  82. package/dist/mcp/actions/list-mcp-tools.js +16 -0
  83. package/dist/mcp/actions/list-mcp-tools.js.map +1 -0
  84. package/dist/mcp-client/app-api.d.ts +34 -0
  85. package/dist/mcp-client/app-api.d.ts.map +1 -0
  86. package/dist/mcp-client/app-api.js +94 -0
  87. package/dist/mcp-client/app-api.js.map +1 -0
  88. package/dist/mcp-client/index.d.ts +3 -1
  89. package/dist/mcp-client/index.d.ts.map +1 -1
  90. package/dist/mcp-client/index.js +3 -1
  91. package/dist/mcp-client/index.js.map +1 -1
  92. package/dist/mcp-client/oauth-client.d.ts +113 -0
  93. package/dist/mcp-client/oauth-client.d.ts.map +1 -0
  94. package/dist/mcp-client/oauth-client.js +316 -0
  95. package/dist/mcp-client/oauth-client.js.map +1 -0
  96. package/dist/mcp-client/oauth-routes.d.ts +25 -0
  97. package/dist/mcp-client/oauth-routes.d.ts.map +1 -0
  98. package/dist/mcp-client/oauth-routes.js +218 -0
  99. package/dist/mcp-client/oauth-routes.js.map +1 -0
  100. package/dist/mcp-client/remote-store.d.ts +21 -6
  101. package/dist/mcp-client/remote-store.d.ts.map +1 -1
  102. package/dist/mcp-client/remote-store.js +72 -124
  103. package/dist/mcp-client/remote-store.js.map +1 -1
  104. package/dist/mcp-client/remote-url.d.ts +8 -0
  105. package/dist/mcp-client/remote-url.d.ts.map +1 -0
  106. package/dist/mcp-client/remote-url.js +103 -0
  107. package/dist/mcp-client/remote-url.js.map +1 -0
  108. package/dist/mcp-client/routes.d.ts +1 -0
  109. package/dist/mcp-client/routes.d.ts.map +1 -1
  110. package/dist/mcp-client/routes.js +9 -0
  111. package/dist/mcp-client/routes.js.map +1 -1
  112. package/dist/oauth-tokens/store.d.ts +2 -2
  113. package/dist/oauth-tokens/store.d.ts.map +1 -1
  114. package/dist/oauth-tokens/store.js +10 -6
  115. package/dist/oauth-tokens/store.js.map +1 -1
  116. package/dist/provider-api/index.d.ts +14 -0
  117. package/dist/provider-api/index.d.ts.map +1 -1
  118. package/dist/provider-api/index.js +499 -61
  119. package/dist/provider-api/index.js.map +1 -1
  120. package/dist/resources/handlers.d.ts +1 -1
  121. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  122. package/dist/secrets/register-framework-secrets.js +28 -0
  123. package/dist/secrets/register-framework-secrets.js.map +1 -1
  124. package/dist/server/action-discovery.d.ts.map +1 -1
  125. package/dist/server/action-discovery.js +2 -0
  126. package/dist/server/action-discovery.js.map +1 -1
  127. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  128. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  129. package/dist/server/core-routes-plugin.js +9 -1
  130. package/dist/server/core-routes-plugin.js.map +1 -1
  131. package/dist/server/workspace-provider-oauth.d.ts +10 -3
  132. package/dist/server/workspace-provider-oauth.d.ts.map +1 -1
  133. package/dist/server/workspace-provider-oauth.js +325 -43
  134. package/dist/server/workspace-provider-oauth.js.map +1 -1
  135. package/dist/templates/default/AGENTS.md +9 -7
  136. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
  137. package/package.json +2 -2
  138. package/src/templates/default/AGENTS.md +9 -7
  139. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
@@ -320,8 +320,10 @@ const PROVIDER_CONFIGS = {
320
320
  label: "GitHub REST API",
321
321
  defaultBaseUrl: "https://api.github.com",
322
322
  auth: {
323
- type: "bearer",
324
- keys: ["GITHUB_TOKEN"],
323
+ type: "oauth-bearer-or-bearer-key",
324
+ oauthProvider: "github",
325
+ tokenLabel: "GitHub OAuth token",
326
+ key: "GITHUB_TOKEN",
325
327
  workspaceProvider: "github",
326
328
  },
327
329
  credentialKeys: ["GITHUB_TOKEN"],
@@ -632,8 +634,11 @@ const PROVIDER_CONFIGS = {
632
634
  label: "HubSpot",
633
635
  defaultBaseUrl: "https://api.hubapi.com",
634
636
  auth: {
635
- type: "bearer",
636
- keys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
637
+ type: "oauth-bearer-or-bearer-key",
638
+ oauthProvider: "hubspot",
639
+ tokenLabel: "HubSpot OAuth token",
640
+ key: "HUBSPOT_PRIVATE_APP_TOKEN",
641
+ fallbackKeys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
637
642
  workspaceProvider: "hubspot",
638
643
  },
639
644
  credentialKeys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
@@ -673,9 +678,12 @@ const PROVIDER_CONFIGS = {
673
678
  defaultBaseUrl: "https://example.atlassian.net",
674
679
  baseUrlCredentialKey: "JIRA_BASE_URL",
675
680
  auth: {
676
- type: "basic",
681
+ type: "oauth-bearer-or-basic",
682
+ oauthProvider: "jira",
683
+ tokenLabel: "Jira",
677
684
  usernameKey: "JIRA_USER_EMAIL",
678
685
  passwordKey: "JIRA_API_TOKEN",
686
+ workspaceProvider: "jira",
679
687
  },
680
688
  credentialKeys: ["JIRA_BASE_URL", "JIRA_USER_EMAIL", "JIRA_API_TOKEN"],
681
689
  docsUrls: [
@@ -807,8 +815,12 @@ const PROVIDER_CONFIGS = {
807
815
  label: "Sentry",
808
816
  defaultBaseUrl: "https://sentry.io/api/0",
809
817
  auth: {
810
- type: "bearer",
811
- keys: ["SENTRY_AUTH_TOKEN", "SENTRY_SERVER_TOKEN"],
818
+ type: "oauth-bearer-or-bearer-key",
819
+ oauthProvider: "sentry",
820
+ tokenLabel: "Sentry OAuth token",
821
+ key: "SENTRY_AUTH_TOKEN",
822
+ fallbackKeys: ["SENTRY_AUTH_TOKEN", "SENTRY_SERVER_TOKEN"],
823
+ workspaceProvider: "sentry",
812
824
  },
813
825
  credentialKeys: [
814
826
  "SENTRY_AUTH_TOKEN",
@@ -939,6 +951,7 @@ export function createProviderApiRuntime(options) {
939
951
  listCatalog: (provider) => listProviderApiCatalogWithCustom(provider, { providerIds }, runtimeOptions),
940
952
  fetchDocs: (docsOptions) => fetchProviderApiDocs(docsOptions, runtimeOptions),
941
953
  executeRequest: (args) => executeProviderApiRequest(args, runtimeOptions),
954
+ resolveOAuthAccessToken: (args) => resolveProviderApiOAuthAccessToken(args, runtimeOptions),
942
955
  listGitHubRepositoryFiles: (args) => listGitHubRepositoryFiles(args, runtimeOptions),
943
956
  searchGitHubRepositoryFiles: (args) => searchGitHubRepositoryFiles(args, runtimeOptions),
944
957
  readGitHubRepositoryFile: (args) => readGitHubRepositoryFile(args, runtimeOptions),
@@ -1201,24 +1214,45 @@ export async function resolveProviderApiOAuthAccessToken(args, runtime) {
1201
1214
  await assertProviderAllowedAsync(args.provider, runtime);
1202
1215
  const config = getProviderApiConfig(args.provider);
1203
1216
  const auth = config.auth;
1204
- if (auth.type !== "oauth-bearer") {
1217
+ const oauthAuth = auth.type === "oauth-bearer"
1218
+ ? auth
1219
+ : auth.type === "oauth-bearer-or-api-key-header" ||
1220
+ auth.type === "oauth-bearer-or-bearer-key"
1221
+ ? {
1222
+ type: "oauth-bearer",
1223
+ oauthProvider: auth.oauthProvider,
1224
+ tokenLabel: auth.tokenLabel,
1225
+ workspaceProvider: auth.workspaceProvider,
1226
+ }
1227
+ : auth.type === "oauth-bearer-or-basic"
1228
+ ? {
1229
+ type: "oauth-bearer",
1230
+ oauthProvider: auth.oauthProvider,
1231
+ tokenLabel: auth.tokenLabel,
1232
+ workspaceProvider: auth.workspaceProvider,
1233
+ }
1234
+ : null;
1235
+ if (!oauthAuth) {
1205
1236
  throw new Error(`Provider API ${args.provider} does not use a direct OAuth bearer token.`);
1206
1237
  }
1207
1238
  const ctx = requireRuntimeCredentialContext(runtime, config.credentialKeys[0] ?? config.id);
1208
- const credential = auth.workspaceProvider && args.connectionId
1209
- ? await resolveConnectionBoundOAuthBearerToken({
1210
- auth,
1239
+ const credential = oauthAuth.workspaceProvider
1240
+ ? await resolveOptionalConnectionBoundOAuthBearerToken({
1241
+ auth: oauthAuth,
1211
1242
  runtime,
1212
1243
  ctx,
1213
- workspaceProvider: auth.workspaceProvider,
1244
+ workspaceProvider: oauthAuth.workspaceProvider,
1214
1245
  connectionId: args.connectionId,
1215
1246
  accountId: args.accountId,
1216
1247
  })
1217
1248
  : await resolveOAuthBearerToken({
1218
- auth,
1249
+ auth: oauthAuth,
1219
1250
  ctx,
1220
1251
  accountId: args.accountId,
1221
1252
  });
1253
+ if (!credential) {
1254
+ throw new Error(`${oauthAuth.tokenLabel} workspace connection is not available to ${runtime.appId}.`);
1255
+ }
1222
1256
  return {
1223
1257
  accessToken: credential.value,
1224
1258
  accountId: credential.accountId ?? null,
@@ -2184,10 +2218,13 @@ function describeAuth(auth) {
2184
2218
  if (auth.type === "oauth-bearer")
2185
2219
  return `oauth-bearer:${auth.oauthProvider}`;
2186
2220
  if (auth.type === "oauth-bearer-or-api-key-header") {
2187
- return `oauth-bearer:${auth.oauthProvider}-or-api-key-header:${auth.header}`;
2221
+ return `oauth-bearer:${auth.oauthProvider}-or-api-key-header:${auth.header}:${(auth.fallbackKeys ?? [auth.key]).join(",")}`;
2188
2222
  }
2189
2223
  if (auth.type === "oauth-bearer-or-bearer-key") {
2190
- return `oauth-bearer:${auth.oauthProvider}-or-bearer-key:${auth.key}`;
2224
+ return `oauth-bearer:${auth.oauthProvider}-or-bearer-key:${(auth.fallbackKeys ?? [auth.key]).join(",")}`;
2225
+ }
2226
+ if (auth.type === "oauth-bearer-or-basic") {
2227
+ return `oauth-bearer:${auth.oauthProvider}-or-basic:${auth.usernameKey}:${auth.passwordKey}`;
2191
2228
  }
2192
2229
  return "prometheus-basic-or-bearer";
2193
2230
  }
@@ -2199,17 +2236,64 @@ function requireRuntimeCredentialContext(runtime, credentialKey) {
2199
2236
  return ctx;
2200
2237
  }
2201
2238
  async function resolveBaseUrl(config, runtime, ctx, args) {
2239
+ const oauthBaseUrl = await resolveWorkspaceOAuthBaseUrl(config, runtime, args);
2240
+ if (oauthBaseUrl)
2241
+ return oauthBaseUrl;
2202
2242
  if (!config.baseUrlCredentialKey)
2203
2243
  return config.defaultBaseUrl;
2244
+ const auth = config.auth;
2245
+ const workspaceProvider = auth.type === "oauth-bearer" ||
2246
+ auth.type === "oauth-bearer-or-api-key-header" ||
2247
+ auth.type === "oauth-bearer-or-bearer-key" ||
2248
+ auth.type === "oauth-bearer-or-basic"
2249
+ ? auth.workspaceProvider
2250
+ : undefined;
2204
2251
  const configured = await resolveCredentialValue({
2205
2252
  config,
2206
2253
  runtime,
2207
2254
  ctx,
2208
2255
  key: config.baseUrlCredentialKey,
2209
2256
  args,
2257
+ workspaceProvider,
2210
2258
  });
2211
2259
  return (configured || config.defaultBaseUrl).replace(/\/+$/, "");
2212
2260
  }
2261
+ async function resolveWorkspaceOAuthBaseUrl(config, runtime, args) {
2262
+ const auth = config.auth;
2263
+ const workspaceProvider = auth.type === "oauth-bearer" ||
2264
+ auth.type === "oauth-bearer-or-api-key-header" ||
2265
+ auth.type === "oauth-bearer-or-bearer-key" ||
2266
+ auth.type === "oauth-bearer-or-basic"
2267
+ ? auth.workspaceProvider
2268
+ : undefined;
2269
+ if (workspaceProvider !== "jira")
2270
+ return null;
2271
+ let resolved;
2272
+ try {
2273
+ resolved = await resolveWorkspaceConnectionForApp({
2274
+ appId: runtime.appId,
2275
+ provider: workspaceProvider,
2276
+ connectionId: args.connectionId ?? undefined,
2277
+ requireConnected: true,
2278
+ });
2279
+ }
2280
+ catch (error) {
2281
+ if (error instanceof Error &&
2282
+ error.message === "Workspace connections require an authenticated user.") {
2283
+ return null;
2284
+ }
2285
+ throw error;
2286
+ }
2287
+ if (!resolved.available || !resolved.connection)
2288
+ return null;
2289
+ const connectionConfig = resolved.connection.config;
2290
+ if (connectionConfig.credentialMode !== "oauth")
2291
+ return null;
2292
+ const baseUrl = connectionConfig.atlassianApiBaseUrl;
2293
+ if (typeof baseUrl !== "string" || !baseUrl.trim())
2294
+ return null;
2295
+ return baseUrl.replace(/\/+$/, "");
2296
+ }
2213
2297
  async function resolvePlaceholders(config, runtime, ctx, args) {
2214
2298
  const placeholders = {};
2215
2299
  for (const placeholder of config.placeholders ?? []) {
@@ -2440,29 +2524,63 @@ async function resolveAuth(config, runtime, ctx, args) {
2440
2524
  };
2441
2525
  }
2442
2526
  if (auth.type === "oauth-bearer-or-api-key-header") {
2443
- if (args.connectionId) {
2444
- const credential = await resolveConnectionBoundOAuthBearerToken({
2445
- auth: {
2446
- type: "oauth-bearer",
2447
- oauthProvider: auth.oauthProvider,
2448
- tokenLabel: auth.tokenLabel,
2449
- },
2527
+ const explicitConnectionFallback = Boolean(args.connectionId?.trim()) && Boolean(runtime.resolveCredential);
2528
+ const resolvedFallback = explicitConnectionFallback
2529
+ ? await resolveHybridFallbackCredential({
2530
+ auth,
2531
+ config,
2450
2532
  runtime,
2451
2533
  ctx,
2452
- connectionId: args.connectionId,
2453
- accountId: args.accountId,
2454
- workspaceProvider: auth.workspaceProvider,
2455
- });
2534
+ args,
2535
+ })
2536
+ : null;
2537
+ if (resolvedFallback) {
2456
2538
  return {
2457
- headers: { Authorization: `Bearer ${credential.value}` },
2458
- credentialSources: [omitCredentialValue(credential)],
2459
- secretValues: [credential.value],
2539
+ headers: { [auth.header]: resolvedFallback.value },
2540
+ credentialSources: [omitCredentialValue(resolvedFallback)],
2541
+ secretValues: [resolvedFallback.value],
2460
2542
  };
2461
2543
  }
2462
- const credential = await resolveRequiredCredential({
2544
+ const workspaceCredential = await resolveOptionalConnectionBoundOAuthBearerToken({
2545
+ auth: {
2546
+ type: "oauth-bearer",
2547
+ oauthProvider: auth.oauthProvider,
2548
+ tokenLabel: auth.tokenLabel,
2549
+ },
2550
+ runtime,
2551
+ ctx,
2552
+ connectionId: args.connectionId,
2553
+ accountId: args.accountId,
2554
+ workspaceProvider: auth.workspaceProvider,
2555
+ allowUnbound: true,
2556
+ });
2557
+ if (workspaceCredential) {
2558
+ return {
2559
+ headers: { Authorization: `Bearer ${workspaceCredential.value}` },
2560
+ credentialSources: [omitCredentialValue(workspaceCredential)],
2561
+ secretValues: [workspaceCredential.value],
2562
+ };
2563
+ }
2564
+ if (!explicitConnectionFallback) {
2565
+ const resolvedFallback = await resolveHybridFallbackCredential({
2566
+ auth,
2567
+ config,
2568
+ runtime,
2569
+ ctx,
2570
+ args,
2571
+ });
2572
+ if (resolvedFallback) {
2573
+ return {
2574
+ headers: { [auth.header]: resolvedFallback.value },
2575
+ credentialSources: [omitCredentialValue(resolvedFallback)],
2576
+ secretValues: [resolvedFallback.value],
2577
+ };
2578
+ }
2579
+ }
2580
+ const credential = await resolveAnyCredential({
2463
2581
  provider: config.id,
2464
2582
  workspaceProvider: auth.workspaceProvider,
2465
- key: auth.key,
2583
+ keys: auth.fallbackKeys ?? [auth.key],
2466
2584
  ctx,
2467
2585
  runtime,
2468
2586
  });
@@ -2473,29 +2591,63 @@ async function resolveAuth(config, runtime, ctx, args) {
2473
2591
  };
2474
2592
  }
2475
2593
  if (auth.type === "oauth-bearer-or-bearer-key") {
2476
- if (args.connectionId) {
2477
- const credential = await resolveConnectionBoundOAuthBearerToken({
2478
- auth: {
2479
- type: "oauth-bearer",
2480
- oauthProvider: auth.oauthProvider,
2481
- tokenLabel: auth.tokenLabel,
2482
- },
2594
+ const explicitConnectionFallback = Boolean(args.connectionId?.trim()) && Boolean(runtime.resolveCredential);
2595
+ const resolvedFallback = explicitConnectionFallback
2596
+ ? await resolveHybridFallbackCredential({
2597
+ auth,
2598
+ config,
2483
2599
  runtime,
2484
2600
  ctx,
2485
- connectionId: args.connectionId,
2486
- accountId: args.accountId,
2487
- workspaceProvider: auth.workspaceProvider,
2488
- });
2601
+ args,
2602
+ })
2603
+ : null;
2604
+ if (resolvedFallback) {
2489
2605
  return {
2490
- headers: { Authorization: `Bearer ${credential.value}` },
2491
- credentialSources: [omitCredentialValue(credential)],
2492
- secretValues: [credential.value],
2606
+ headers: { Authorization: `Bearer ${resolvedFallback.value}` },
2607
+ credentialSources: [omitCredentialValue(resolvedFallback)],
2608
+ secretValues: [resolvedFallback.value],
2493
2609
  };
2494
2610
  }
2495
- const credential = await resolveRequiredCredential({
2611
+ const workspaceCredential = await resolveOptionalConnectionBoundOAuthBearerToken({
2612
+ auth: {
2613
+ type: "oauth-bearer",
2614
+ oauthProvider: auth.oauthProvider,
2615
+ tokenLabel: auth.tokenLabel,
2616
+ },
2617
+ runtime,
2618
+ ctx,
2619
+ connectionId: args.connectionId,
2620
+ accountId: args.accountId,
2621
+ workspaceProvider: auth.workspaceProvider,
2622
+ allowUnbound: true,
2623
+ });
2624
+ if (workspaceCredential) {
2625
+ return {
2626
+ headers: { Authorization: `Bearer ${workspaceCredential.value}` },
2627
+ credentialSources: [omitCredentialValue(workspaceCredential)],
2628
+ secretValues: [workspaceCredential.value],
2629
+ };
2630
+ }
2631
+ if (!explicitConnectionFallback) {
2632
+ const resolvedFallback = await resolveHybridFallbackCredential({
2633
+ auth,
2634
+ config,
2635
+ runtime,
2636
+ ctx,
2637
+ args,
2638
+ });
2639
+ if (resolvedFallback) {
2640
+ return {
2641
+ headers: { Authorization: `Bearer ${resolvedFallback.value}` },
2642
+ credentialSources: [omitCredentialValue(resolvedFallback)],
2643
+ secretValues: [resolvedFallback.value],
2644
+ };
2645
+ }
2646
+ }
2647
+ const credential = await resolveAnyCredential({
2496
2648
  provider: config.id,
2497
2649
  workspaceProvider: auth.workspaceProvider,
2498
- key: auth.key,
2650
+ keys: auth.fallbackKeys ?? [auth.key],
2499
2651
  ctx,
2500
2652
  runtime,
2501
2653
  });
@@ -2505,6 +2657,53 @@ async function resolveAuth(config, runtime, ctx, args) {
2505
2657
  secretValues: [credential.value],
2506
2658
  };
2507
2659
  }
2660
+ if (auth.type === "oauth-bearer-or-basic") {
2661
+ const workspaceCredential = await resolveOptionalConnectionBoundOAuthBearerToken({
2662
+ auth: {
2663
+ type: "oauth-bearer",
2664
+ oauthProvider: auth.oauthProvider,
2665
+ tokenLabel: auth.tokenLabel,
2666
+ },
2667
+ runtime,
2668
+ ctx,
2669
+ connectionId: args.connectionId,
2670
+ accountId: args.accountId,
2671
+ workspaceProvider: auth.workspaceProvider,
2672
+ allowUnbound: true,
2673
+ });
2674
+ if (workspaceCredential) {
2675
+ return {
2676
+ headers: { Authorization: `Bearer ${workspaceCredential.value}` },
2677
+ credentialSources: [omitCredentialValue(workspaceCredential)],
2678
+ secretValues: [workspaceCredential.value],
2679
+ };
2680
+ }
2681
+ const username = await resolveRequiredCredential({
2682
+ provider: config.id,
2683
+ workspaceProvider: auth.workspaceProvider,
2684
+ key: auth.usernameKey,
2685
+ ctx,
2686
+ runtime,
2687
+ connectionId: args.connectionId,
2688
+ });
2689
+ const password = await resolveRequiredCredential({
2690
+ provider: config.id,
2691
+ workspaceProvider: auth.workspaceProvider,
2692
+ key: auth.passwordKey,
2693
+ ctx,
2694
+ runtime,
2695
+ connectionId: args.connectionId,
2696
+ });
2697
+ const encoded = Buffer.from(`${username.value}:${password.value}`).toString("base64");
2698
+ return {
2699
+ headers: { Authorization: `Basic ${encoded}` },
2700
+ credentialSources: [
2701
+ omitCredentialValue(username),
2702
+ omitCredentialValue(password),
2703
+ ],
2704
+ secretValues: [username.value, password.value, encoded],
2705
+ };
2706
+ }
2508
2707
  const bearer = await resolveCredentialValue({
2509
2708
  config,
2510
2709
  runtime,
@@ -2563,6 +2762,20 @@ async function resolveAuth(config, runtime, ctx, args) {
2563
2762
  function emptyAuth() {
2564
2763
  return { headers: {}, credentialSources: [], secretValues: [] };
2565
2764
  }
2765
+ async function resolveHybridFallbackCredential(options) {
2766
+ // App-specific resolvers keep existing provider credentials working when a
2767
+ // caller explicitly selects a connection or account.
2768
+ if (!options.runtime.resolveCredential)
2769
+ return null;
2770
+ return resolveOptionalCredential({
2771
+ provider: options.config.id,
2772
+ workspaceProvider: options.auth.workspaceProvider,
2773
+ key: options.auth.key,
2774
+ ctx: options.ctx,
2775
+ runtime: options.runtime,
2776
+ connectionId: options.args.connectionId,
2777
+ });
2778
+ }
2566
2779
  async function resolveAnyCredential(options) {
2567
2780
  for (const key of options.keys) {
2568
2781
  const credential = await resolveOptionalCredential({ ...options, key });
@@ -2653,22 +2866,23 @@ async function getGoogleServiceAccountToken(scopes, runtime, ctx) {
2653
2866
  return data.access_token;
2654
2867
  }
2655
2868
  async function resolveOAuthBearerToken(options) {
2656
- const accounts = await listOAuthAccountsByOwner(options.auth.oauthProvider, options.ctx.userEmail);
2869
+ const ownerEmail = options.ownerEmail?.trim() || options.ctx.userEmail;
2870
+ const accounts = await listOAuthAccountsByOwner(options.auth.oauthProvider, ownerEmail);
2657
2871
  if (accounts.length === 0) {
2658
- throw new Error(`${options.auth.tokenLabel} is not connected for ${options.ctx.userEmail}.`);
2872
+ throw new Error(`${options.auth.tokenLabel} is not connected for ${ownerEmail}.`);
2659
2873
  }
2660
2874
  const accountId = options.accountId?.trim();
2661
2875
  const account = accountId
2662
2876
  ? accounts.find((entry) => entry.accountId === accountId)
2663
2877
  : accounts[0];
2664
2878
  if (!account) {
2665
- throw new Error(`${options.auth.tokenLabel} account ${accountId} is not available to ${options.ctx.userEmail}.`);
2879
+ throw new Error(`${options.auth.tokenLabel} account ${accountId} is not available to ${ownerEmail}.`);
2666
2880
  }
2667
2881
  const tokens = account.tokens;
2668
2882
  const token = await getValidOAuthAccessToken({
2669
2883
  oauthProvider: options.auth.oauthProvider,
2670
2884
  accountId: account.accountId,
2671
- ownerEmail: options.ctx.userEmail,
2885
+ ownerEmail,
2672
2886
  tokens,
2673
2887
  });
2674
2888
  return {
@@ -2681,22 +2895,39 @@ async function resolveOAuthBearerToken(options) {
2681
2895
  };
2682
2896
  }
2683
2897
  async function resolveConnectionBoundOAuthBearerToken(options) {
2898
+ const credential = await resolveOptionalConnectionBoundOAuthBearerToken(options);
2899
+ if (credential)
2900
+ return credential;
2901
+ throw new Error(`${options.auth.tokenLabel} requires an available workspace connection.`);
2902
+ }
2903
+ async function resolveOptionalConnectionBoundOAuthBearerToken(options) {
2684
2904
  const requestedConnectionId = options.connectionId?.trim();
2685
- if (!requestedConnectionId) {
2686
- throw new Error(`${options.auth.tokenLabel} requires an explicit workspace connection selection.`);
2905
+ let resolved;
2906
+ try {
2907
+ resolved = await resolveWorkspaceConnectionForApp({
2908
+ appId: options.runtime.appId,
2909
+ provider: options.workspaceProvider,
2910
+ connectionId: requestedConnectionId,
2911
+ requireConnected: true,
2912
+ });
2913
+ }
2914
+ catch (error) {
2915
+ if (error instanceof Error &&
2916
+ error.message === "Workspace connections require an authenticated user.") {
2917
+ return null;
2918
+ }
2919
+ throw error;
2687
2920
  }
2688
- const resolved = await resolveWorkspaceConnectionForApp({
2689
- appId: options.runtime.appId,
2690
- provider: options.workspaceProvider,
2691
- connectionId: requestedConnectionId,
2692
- requireConnected: true,
2693
- });
2694
2921
  if (!resolved.available || !resolved.connection) {
2695
- throw new Error(resolved.reason);
2922
+ if (requestedConnectionId)
2923
+ throw new Error(resolved.reason);
2924
+ return null;
2696
2925
  }
2697
2926
  const connectionAccountId = resolved.connection.accountId?.trim();
2698
2927
  if (!connectionAccountId) {
2699
- throw new Error(`${options.auth.tokenLabel} workspace connection ${requestedConnectionId} is missing its OAuth account binding.`);
2928
+ if (options.allowUnbound)
2929
+ return null;
2930
+ throw new Error(`${options.auth.tokenLabel} workspace connection ${resolved.connection.id} is missing its OAuth account binding.`);
2700
2931
  }
2701
2932
  const requestedAccountId = options.accountId?.trim();
2702
2933
  if (requestedAccountId && requestedAccountId !== connectionAccountId) {
@@ -2706,6 +2937,7 @@ async function resolveConnectionBoundOAuthBearerToken(options) {
2706
2937
  auth: options.auth,
2707
2938
  ctx: options.ctx,
2708
2939
  accountId: connectionAccountId,
2940
+ ownerEmail: resolved.connection.ownerEmail,
2709
2941
  });
2710
2942
  return {
2711
2943
  ...credential,
@@ -2737,6 +2969,18 @@ async function getValidOAuthAccessToken(options) {
2737
2969
  if (options.oauthProvider === "notion") {
2738
2970
  return refreshNotionOAuthToken(options, refreshToken);
2739
2971
  }
2972
+ if (options.oauthProvider === "github") {
2973
+ return refreshGitHubOAuthToken(options, refreshToken);
2974
+ }
2975
+ if (options.oauthProvider === "hubspot") {
2976
+ return refreshHubSpotOAuthToken(options, refreshToken);
2977
+ }
2978
+ if (options.oauthProvider === "jira") {
2979
+ return refreshJiraOAuthToken(options, refreshToken);
2980
+ }
2981
+ if (options.oauthProvider === "sentry") {
2982
+ return refreshSentryOAuthToken(options, refreshToken);
2983
+ }
2740
2984
  throw new Error(`${options.oauthProvider} OAuth token is expired and automatic refresh is not configured for provider-api.`);
2741
2985
  }
2742
2986
  async function refreshFigmaOAuthToken(options, refreshToken) {
@@ -2804,6 +3048,179 @@ async function refreshNotionOAuthToken(options, refreshToken) {
2804
3048
  await saveOAuthTokens(options.oauthProvider, options.accountId, updated, options.ownerEmail);
2805
3049
  return data.access_token;
2806
3050
  }
3051
+ async function refreshGitHubOAuthToken(options, refreshToken) {
3052
+ const credentials = await resolveOAuthClientCredentialCandidates("GITHUB");
3053
+ if (!credentials.length) {
3054
+ throw new Error("GITHUB_CLIENT_ID/SECRET not set for GitHub OAuth refresh.");
3055
+ }
3056
+ let response = null;
3057
+ let data = {};
3058
+ for (const credential of credentials) {
3059
+ const result = await fetchBoundedOAuthRefreshJson("https://github.com/login/oauth/access_token", {
3060
+ method: "POST",
3061
+ headers: {
3062
+ Accept: "application/json",
3063
+ "Content-Type": "application/json",
3064
+ },
3065
+ body: JSON.stringify({
3066
+ client_id: credential.clientId,
3067
+ client_secret: credential.clientSecret,
3068
+ grant_type: "refresh_token",
3069
+ refresh_token: refreshToken,
3070
+ }),
3071
+ }, "GitHub");
3072
+ response = result.response;
3073
+ data = result.data;
3074
+ if (response.ok && data.access_token)
3075
+ break;
3076
+ }
3077
+ if (!response?.ok || !data.access_token) {
3078
+ throw new Error(`GitHub OAuth refresh failed (${response?.status ?? 0}).`);
3079
+ }
3080
+ const updated = {
3081
+ ...options.tokens,
3082
+ access_token: data.access_token,
3083
+ refresh_token: data.refresh_token ?? refreshToken,
3084
+ token_type: data.token_type ?? options.tokens.token_type,
3085
+ ...(Number.isFinite(data.expires_in) && (data.expires_in ?? 0) > 0
3086
+ ? { expiry_date: Date.now() + data.expires_in * 1_000 }
3087
+ : {}),
3088
+ };
3089
+ await saveOAuthTokens(options.oauthProvider, options.accountId, updated, options.ownerEmail);
3090
+ return data.access_token;
3091
+ }
3092
+ async function saveOAuthTokensAndLinkedJiraAccounts(options) {
3093
+ const serializedTokens = options.updated;
3094
+ await saveOAuthTokens(options.options.oauthProvider, options.options.accountId, serializedTokens, options.options.ownerEmail);
3095
+ const accounts = await listOAuthAccountsByOwner(options.options.oauthProvider, options.options.ownerEmail);
3096
+ for (const account of accounts) {
3097
+ if (account.accountId === options.options.accountId)
3098
+ continue;
3099
+ const accountRefreshToken = account.tokens.refresh_token ??
3100
+ account.tokens.refreshToken;
3101
+ if (accountRefreshToken !== options.previousRefreshToken)
3102
+ continue;
3103
+ await saveOAuthTokens(options.options.oauthProvider, account.accountId, serializedTokens, options.options.ownerEmail);
3104
+ }
3105
+ }
3106
+ async function refreshHubSpotOAuthToken(options, refreshToken) {
3107
+ const credentials = await resolveOAuthClientCredentialCandidates("HUBSPOT");
3108
+ if (!credentials.length) {
3109
+ throw new Error("HUBSPOT_CLIENT_ID/SECRET not set for HubSpot OAuth refresh.");
3110
+ }
3111
+ let response = null;
3112
+ let data = {};
3113
+ for (const credential of credentials) {
3114
+ const result = await fetchBoundedOAuthRefreshJson("https://api.hubapi.com/oauth/v3/token", {
3115
+ method: "POST",
3116
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3117
+ body: new URLSearchParams({
3118
+ grant_type: "refresh_token",
3119
+ client_id: credential.clientId,
3120
+ client_secret: credential.clientSecret,
3121
+ refresh_token: refreshToken,
3122
+ }),
3123
+ }, "HubSpot");
3124
+ response = result.response;
3125
+ data = result.data;
3126
+ if (response.ok && data.access_token)
3127
+ break;
3128
+ }
3129
+ if (!response?.ok || !data.access_token) {
3130
+ throw new Error(`HubSpot OAuth refresh failed (${response?.status ?? 0}).`);
3131
+ }
3132
+ const updated = {
3133
+ ...options.tokens,
3134
+ access_token: data.access_token,
3135
+ refresh_token: data.refresh_token ?? refreshToken,
3136
+ token_type: data.token_type ?? options.tokens.token_type,
3137
+ ...(Number.isFinite(data.expires_in) && (data.expires_in ?? 0) > 0
3138
+ ? { expiry_date: Date.now() + data.expires_in * 1_000 }
3139
+ : {}),
3140
+ };
3141
+ await saveOAuthTokens(options.oauthProvider, options.accountId, updated, options.ownerEmail);
3142
+ return data.access_token;
3143
+ }
3144
+ async function refreshJiraOAuthToken(options, refreshToken) {
3145
+ const credentials = await resolveOAuthClientCredentialCandidates("JIRA");
3146
+ if (!credentials.length) {
3147
+ throw new Error("JIRA_CLIENT_ID/SECRET not set for Jira OAuth refresh.");
3148
+ }
3149
+ let response = null;
3150
+ let data = {};
3151
+ for (const credential of credentials) {
3152
+ const result = await fetchBoundedOAuthRefreshJson("https://auth.atlassian.com/oauth/token", {
3153
+ method: "POST",
3154
+ headers: { "Content-Type": "application/json" },
3155
+ body: JSON.stringify({
3156
+ grant_type: "refresh_token",
3157
+ client_id: credential.clientId,
3158
+ client_secret: credential.clientSecret,
3159
+ refresh_token: refreshToken,
3160
+ }),
3161
+ }, "Jira");
3162
+ response = result.response;
3163
+ data = result.data;
3164
+ if (response.ok && data.access_token)
3165
+ break;
3166
+ }
3167
+ if (!response?.ok || !data.access_token) {
3168
+ throw new Error(`Jira OAuth refresh failed (${response?.status ?? 0}).`);
3169
+ }
3170
+ const updated = {
3171
+ ...options.tokens,
3172
+ access_token: data.access_token,
3173
+ refresh_token: data.refresh_token ?? refreshToken,
3174
+ token_type: data.token_type ?? options.tokens.token_type,
3175
+ ...(Number.isFinite(data.expires_in) && (data.expires_in ?? 0) > 0
3176
+ ? { expiry_date: Date.now() + data.expires_in * 1_000 }
3177
+ : {}),
3178
+ };
3179
+ await saveOAuthTokensAndLinkedJiraAccounts({
3180
+ options,
3181
+ previousRefreshToken: refreshToken,
3182
+ updated,
3183
+ });
3184
+ return data.access_token;
3185
+ }
3186
+ async function refreshSentryOAuthToken(options, refreshToken) {
3187
+ const credentials = await resolveOAuthClientCredentialCandidates("SENTRY");
3188
+ if (!credentials.length) {
3189
+ throw new Error("SENTRY_CLIENT_ID/SECRET not set for Sentry OAuth refresh.");
3190
+ }
3191
+ let response = null;
3192
+ let data = {};
3193
+ for (const credential of credentials) {
3194
+ const result = await fetchBoundedOAuthRefreshJson("https://sentry.io/oauth/token/", {
3195
+ method: "POST",
3196
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3197
+ body: new URLSearchParams({
3198
+ grant_type: "refresh_token",
3199
+ client_id: credential.clientId,
3200
+ client_secret: credential.clientSecret,
3201
+ refresh_token: refreshToken,
3202
+ }),
3203
+ }, "Sentry");
3204
+ response = result.response;
3205
+ data = result.data;
3206
+ if (response.ok && data.access_token)
3207
+ break;
3208
+ }
3209
+ if (!response?.ok || !data.access_token) {
3210
+ throw new Error(`Sentry OAuth refresh failed (${response?.status ?? 0}).`);
3211
+ }
3212
+ const updated = {
3213
+ ...options.tokens,
3214
+ access_token: data.access_token,
3215
+ refresh_token: data.refresh_token ?? refreshToken,
3216
+ token_type: data.token_type ?? options.tokens.token_type,
3217
+ ...(Number.isFinite(data.expires_in) && (data.expires_in ?? 0) > 0
3218
+ ? { expiry_date: Date.now() + data.expires_in * 1_000 }
3219
+ : {}),
3220
+ };
3221
+ await saveOAuthTokens(options.oauthProvider, options.accountId, updated, options.ownerEmail);
3222
+ return data.access_token;
3223
+ }
2807
3224
  const OAUTH_REFRESH_TIMEOUT_MS = 10_000;
2808
3225
  const OAUTH_REFRESH_MAX_BYTES = 256 * 1024;
2809
3226
  async function fetchBoundedOAuthRefreshJson(url, init, providerLabel) {
@@ -2922,6 +3339,27 @@ function dedupeGoogleOAuthCredentials(candidates) {
2922
3339
  return true;
2923
3340
  });
2924
3341
  }
3342
+ async function resolveOAuthClientCredentialCandidates(provider) {
3343
+ const [clientId, clientSecret, integrationClientId, integrationClientSecret] = await Promise.all([
3344
+ resolveSecret(`${provider}_CLIENT_ID`),
3345
+ resolveSecret(`${provider}_CLIENT_SECRET`),
3346
+ resolveSecret(`${provider}_INTEGRATION_CLIENT_ID`),
3347
+ resolveSecret(`${provider}_INTEGRATION_CLIENT_SECRET`),
3348
+ ]);
3349
+ const candidates = [
3350
+ integrationClientId && integrationClientSecret
3351
+ ? { clientId: integrationClientId, clientSecret: integrationClientSecret }
3352
+ : null,
3353
+ clientId && clientSecret ? { clientId, clientSecret } : null,
3354
+ ].filter((value) => Boolean(value));
3355
+ const seen = new Set();
3356
+ return candidates.filter((candidate) => {
3357
+ if (seen.has(candidate.clientId))
3358
+ return false;
3359
+ seen.add(candidate.clientId);
3360
+ return true;
3361
+ });
3362
+ }
2925
3363
  function normalizeMethod(method) {
2926
3364
  const normalized = String(method || "GET").toUpperCase();
2927
3365
  if (normalized === "GET" ||