@agent-native/core 0.105.0 → 0.106.1

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 (194) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +39 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/application-state/index.ts +2 -0
  5. package/corpus/core/src/application-state/script-helpers.ts +12 -0
  6. package/corpus/core/src/application-state/store.ts +38 -0
  7. package/corpus/core/src/client/AssistantChat.tsx +4 -0
  8. package/corpus/core/src/client/MultiTabAssistantChat.tsx +5 -1
  9. package/corpus/core/src/client/composer/PromptComposer.tsx +8 -0
  10. package/corpus/core/src/client/composer/TiptapComposer.tsx +38 -1
  11. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +107 -32
  12. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +33 -0
  13. package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
  14. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +10 -3
  15. package/corpus/core/src/client/use-chat-models.ts +10 -2
  16. package/corpus/core/src/connections/catalog.ts +107 -6
  17. package/corpus/core/src/localization/default-messages.ts +2 -0
  18. package/corpus/core/src/mcp/actions/call-mcp-tool.ts +18 -0
  19. package/corpus/core/src/mcp/actions/list-mcp-tools.ts +18 -0
  20. package/corpus/core/src/mcp-client/app-api.ts +136 -0
  21. package/corpus/core/src/mcp-client/index.ts +21 -0
  22. package/corpus/core/src/mcp-client/oauth-client.ts +462 -0
  23. package/corpus/core/src/mcp-client/oauth-routes.ts +307 -0
  24. package/corpus/core/src/mcp-client/remote-store.ts +93 -118
  25. package/corpus/core/src/mcp-client/remote-url.ts +97 -0
  26. package/corpus/core/src/mcp-client/routes.ts +11 -0
  27. package/corpus/core/src/oauth-tokens/store.ts +10 -4
  28. package/corpus/core/src/provider-api/index.ts +690 -55
  29. package/corpus/core/src/secrets/register-framework-secrets.ts +30 -0
  30. package/corpus/core/src/server/action-discovery.ts +2 -0
  31. package/corpus/core/src/server/core-routes-plugin.ts +9 -1
  32. package/corpus/core/src/server/workspace-provider-oauth.ts +448 -61
  33. package/corpus/core/src/templates/default/AGENTS.md +9 -7
  34. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
  35. package/corpus/templates/analytics/app/lib/data-source-status.ts +2 -0
  36. package/corpus/templates/analytics/app/pages/DataSources.tsx +61 -0
  37. package/corpus/templates/analytics/server/handlers/jira.ts +7 -0
  38. package/corpus/templates/analytics/server/lib/jira.ts +14 -42
  39. package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +11 -0
  40. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +22 -0
  41. package/corpus/templates/design/AGENTS.md +16 -0
  42. package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +54 -0
  43. package/corpus/templates/design/actions/propose-node-rewrite.ts +290 -0
  44. package/corpus/templates/design/actions/resolve-node-rewrite.ts +257 -0
  45. package/corpus/templates/design/actions/view-screen.ts +98 -2
  46. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +71 -1
  47. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -0
  48. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -0
  49. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +194 -0
  50. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +21 -4
  51. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +18 -0
  52. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  53. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +638 -0
  54. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +535 -54
  55. package/corpus/templates/design/app/components/visual-editor/index.ts +5 -1
  56. package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +21 -3
  57. package/corpus/templates/design/app/i18n/ar-SA.ts +35 -0
  58. package/corpus/templates/design/app/i18n/de-DE.ts +35 -0
  59. package/corpus/templates/design/app/i18n/es-ES.ts +35 -0
  60. package/corpus/templates/design/app/i18n/fr-FR.ts +35 -0
  61. package/corpus/templates/design/app/i18n/hi-IN.ts +35 -0
  62. package/corpus/templates/design/app/i18n/ja-JP.ts +35 -0
  63. package/corpus/templates/design/app/i18n/ko-KR.ts +35 -0
  64. package/corpus/templates/design/app/i18n/pt-BR.ts +35 -0
  65. package/corpus/templates/design/app/i18n/zh-CN.ts +35 -0
  66. package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
  67. package/corpus/templates/design/app/i18n-data.ts +32 -0
  68. package/corpus/templates/design/app/lib/node-reprompt.ts +110 -0
  69. package/corpus/templates/design/changelog/2026-07-16-design-editing-and-review-are-now-more-predictable.md +6 -0
  70. package/corpus/templates/design/server/lib/reprompt-action-guard.ts +107 -0
  71. package/corpus/templates/design/server/plugins/agent-chat.ts +9 -1
  72. package/corpus/templates/design/shared/code-layer.ts +15 -0
  73. package/corpus/templates/design/shared/node-rewrite.ts +213 -0
  74. package/corpus/templates/design/shared/review-anchor.ts +26 -3
  75. package/corpus/templates/dispatch/app/routes/integrations.tsx +57 -4
  76. package/dist/application-state/index.d.ts +2 -2
  77. package/dist/application-state/index.d.ts.map +1 -1
  78. package/dist/application-state/index.js +2 -2
  79. package/dist/application-state/index.js.map +1 -1
  80. package/dist/application-state/script-helpers.d.ts +1 -0
  81. package/dist/application-state/script-helpers.d.ts.map +1 -1
  82. package/dist/application-state/script-helpers.js +7 -1
  83. package/dist/application-state/script-helpers.js.map +1 -1
  84. package/dist/application-state/store.d.ts +1 -0
  85. package/dist/application-state/store.d.ts.map +1 -1
  86. package/dist/application-state/store.js +28 -0
  87. package/dist/application-state/store.js.map +1 -1
  88. package/dist/client/AssistantChat.d.ts +2 -0
  89. package/dist/client/AssistantChat.d.ts.map +1 -1
  90. package/dist/client/AssistantChat.js +2 -2
  91. package/dist/client/AssistantChat.js.map +1 -1
  92. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  93. package/dist/client/MultiTabAssistantChat.js +5 -2
  94. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  95. package/dist/client/composer/PromptComposer.d.ts +2 -0
  96. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  97. package/dist/client/composer/PromptComposer.js +5 -2
  98. package/dist/client/composer/PromptComposer.js.map +1 -1
  99. package/dist/client/composer/TiptapComposer.d.ts +4 -1
  100. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  101. package/dist/client/composer/TiptapComposer.js +13 -5
  102. package/dist/client/composer/TiptapComposer.js.map +1 -1
  103. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  104. package/dist/client/resources/McpIntegrationDialog.js +58 -14
  105. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  106. package/dist/client/resources/mcp-integration-catalog.d.ts +9 -0
  107. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  108. package/dist/client/resources/mcp-integration-catalog.js +13 -0
  109. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  110. package/dist/client/resources/use-mcp-servers.d.ts +1 -0
  111. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
  112. package/dist/client/resources/use-mcp-servers.js.map +1 -1
  113. package/dist/client/review/ReviewCommentComposer.d.ts +4 -1
  114. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  115. package/dist/client/review/ReviewCommentComposer.js +3 -3
  116. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  117. package/dist/client/use-chat-models.d.ts +1 -0
  118. package/dist/client/use-chat-models.d.ts.map +1 -1
  119. package/dist/client/use-chat-models.js +8 -2
  120. package/dist/client/use-chat-models.js.map +1 -1
  121. package/dist/collab/struct-routes.d.ts +1 -1
  122. package/dist/connections/catalog.d.ts +78 -7
  123. package/dist/connections/catalog.d.ts.map +1 -1
  124. package/dist/connections/catalog.js +97 -6
  125. package/dist/connections/catalog.js.map +1 -1
  126. package/dist/localization/default-messages.d.ts +2 -0
  127. package/dist/localization/default-messages.d.ts.map +1 -1
  128. package/dist/localization/default-messages.js +2 -0
  129. package/dist/localization/default-messages.js.map +1 -1
  130. package/dist/mcp/actions/call-mcp-tool.d.ts +7 -0
  131. package/dist/mcp/actions/call-mcp-tool.d.ts.map +1 -0
  132. package/dist/mcp/actions/call-mcp-tool.js +15 -0
  133. package/dist/mcp/actions/call-mcp-tool.js.map +1 -0
  134. package/dist/mcp/actions/list-mcp-tools.d.ts +5 -0
  135. package/dist/mcp/actions/list-mcp-tools.d.ts.map +1 -0
  136. package/dist/mcp/actions/list-mcp-tools.js +16 -0
  137. package/dist/mcp/actions/list-mcp-tools.js.map +1 -0
  138. package/dist/mcp-client/app-api.d.ts +34 -0
  139. package/dist/mcp-client/app-api.d.ts.map +1 -0
  140. package/dist/mcp-client/app-api.js +94 -0
  141. package/dist/mcp-client/app-api.js.map +1 -0
  142. package/dist/mcp-client/index.d.ts +3 -1
  143. package/dist/mcp-client/index.d.ts.map +1 -1
  144. package/dist/mcp-client/index.js +3 -1
  145. package/dist/mcp-client/index.js.map +1 -1
  146. package/dist/mcp-client/oauth-client.d.ts +113 -0
  147. package/dist/mcp-client/oauth-client.d.ts.map +1 -0
  148. package/dist/mcp-client/oauth-client.js +316 -0
  149. package/dist/mcp-client/oauth-client.js.map +1 -0
  150. package/dist/mcp-client/oauth-routes.d.ts +25 -0
  151. package/dist/mcp-client/oauth-routes.d.ts.map +1 -0
  152. package/dist/mcp-client/oauth-routes.js +218 -0
  153. package/dist/mcp-client/oauth-routes.js.map +1 -0
  154. package/dist/mcp-client/remote-store.d.ts +21 -6
  155. package/dist/mcp-client/remote-store.d.ts.map +1 -1
  156. package/dist/mcp-client/remote-store.js +72 -124
  157. package/dist/mcp-client/remote-store.js.map +1 -1
  158. package/dist/mcp-client/remote-url.d.ts +8 -0
  159. package/dist/mcp-client/remote-url.d.ts.map +1 -0
  160. package/dist/mcp-client/remote-url.js +103 -0
  161. package/dist/mcp-client/remote-url.js.map +1 -0
  162. package/dist/mcp-client/routes.d.ts +1 -0
  163. package/dist/mcp-client/routes.d.ts.map +1 -1
  164. package/dist/mcp-client/routes.js +9 -0
  165. package/dist/mcp-client/routes.js.map +1 -1
  166. package/dist/notifications/routes.d.ts +2 -2
  167. package/dist/oauth-tokens/store.d.ts +2 -2
  168. package/dist/oauth-tokens/store.d.ts.map +1 -1
  169. package/dist/oauth-tokens/store.js +10 -6
  170. package/dist/oauth-tokens/store.js.map +1 -1
  171. package/dist/observability/routes.d.ts +5 -5
  172. package/dist/provider-api/index.d.ts +14 -0
  173. package/dist/provider-api/index.d.ts.map +1 -1
  174. package/dist/provider-api/index.js +499 -61
  175. package/dist/provider-api/index.js.map +1 -1
  176. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  177. package/dist/secrets/register-framework-secrets.js +28 -0
  178. package/dist/secrets/register-framework-secrets.js.map +1 -1
  179. package/dist/secrets/routes.d.ts +9 -9
  180. package/dist/server/action-discovery.d.ts.map +1 -1
  181. package/dist/server/action-discovery.js +2 -0
  182. package/dist/server/action-discovery.js.map +1 -1
  183. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  184. package/dist/server/core-routes-plugin.js +9 -1
  185. package/dist/server/core-routes-plugin.js.map +1 -1
  186. package/dist/server/workspace-provider-oauth.d.ts +10 -3
  187. package/dist/server/workspace-provider-oauth.d.ts.map +1 -1
  188. package/dist/server/workspace-provider-oauth.js +325 -43
  189. package/dist/server/workspace-provider-oauth.js.map +1 -1
  190. package/dist/templates/default/AGENTS.md +9 -7
  191. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
  192. package/package.json +2 -2
  193. package/src/templates/default/AGENTS.md +9 -7
  194. 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
- keys: ["GITHUB_TOKEN"],
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
- keys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
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
- keys: ["SENTRY_AUTH_TOKEN", "SENTRY_SERVER_TOKEN"],
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
- if (auth.type !== "oauth-bearer") {
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
- auth.workspaceProvider && args.connectionId
1840
- ? await resolveConnectionBoundOAuthBearerToken({
1841
- auth,
1842
- runtime,
1843
- ctx,
1844
- workspaceProvider: auth.workspaceProvider,
1845
- connectionId: args.connectionId,
1846
- accountId: args.accountId,
1847
- })
1848
- : await resolveOAuthBearerToken({
1849
- auth,
1850
- ctx,
1851
- accountId: args.accountId,
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
- if (args.connectionId) {
3377
- const credential = await resolveConnectionBoundOAuthBearerToken({
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 ${credential.value}` },
3391
- credentialSources: [omitCredentialValue(credential)],
3392
- secretValues: [credential.value],
3519
+ headers: { Authorization: `Bearer ${workspaceCredential.value}` },
3520
+ credentialSources: [omitCredentialValue(workspaceCredential)],
3521
+ secretValues: [workspaceCredential.value],
3393
3522
  };
3394
3523
  }
3395
- const credential = await resolveRequiredCredential({
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
- key: auth.key,
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
- if (args.connectionId) {
3410
- const credential = await resolveConnectionBoundOAuthBearerToken({
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 ${credential.value}` },
3424
- credentialSources: [omitCredentialValue(credential)],
3425
- secretValues: [credential.value],
3588
+ headers: { Authorization: `Bearer ${workspaceCredential.value}` },
3589
+ credentialSources: [omitCredentialValue(workspaceCredential)],
3590
+ secretValues: [workspaceCredential.value],
3426
3591
  };
3427
3592
  }
3428
- const credential = await resolveRequiredCredential({
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
- key: auth.key,
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
- options.ctx.userEmail,
3909
+ ownerEmail,
3655
3910
  );
3656
3911
  if (accounts.length === 0) {
3657
3912
  throw new Error(
3658
- `${options.auth.tokenLabel} is not connected for ${options.ctx.userEmail}.`,
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 ${options.ctx.userEmail}.`,
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: options.ctx.userEmail,
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
- if (!requestedConnectionId) {
3697
- throw new Error(
3698
- `${options.auth.tokenLabel} requires an explicit workspace connection selection.`,
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 ${requestedConnectionId} is missing its OAuth account binding.`,
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 {