@shipfox/api-integration-jira 11.0.0 → 12.1.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 (126) hide show
  1. package/.turbo/turbo-build.log +17 -2
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +58 -2
  4. package/dist/api/client.d.ts +34 -0
  5. package/dist/api/client.d.ts.map +1 -1
  6. package/dist/api/client.js +131 -0
  7. package/dist/api/client.js.map +1 -1
  8. package/dist/config.d.ts +0 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +1 -4
  11. package/dist/config.js.map +1 -1
  12. package/dist/core/agent-tools-provider.d.ts +26 -0
  13. package/dist/core/agent-tools-provider.d.ts.map +1 -0
  14. package/dist/core/agent-tools-provider.js +130 -0
  15. package/dist/core/agent-tools-provider.js.map +1 -0
  16. package/dist/core/agent-tools.d.ts +16 -0
  17. package/dist/core/agent-tools.d.ts.map +1 -0
  18. package/dist/core/agent-tools.js +441 -0
  19. package/dist/core/agent-tools.js.map +1 -0
  20. package/dist/core/install.d.ts +22 -0
  21. package/dist/core/install.d.ts.map +1 -1
  22. package/dist/core/install.js +68 -20
  23. package/dist/core/install.js.map +1 -1
  24. package/dist/core/tokens.d.ts +2 -0
  25. package/dist/core/tokens.d.ts.map +1 -1
  26. package/dist/core/tokens.js +129 -37
  27. package/dist/core/tokens.js.map +1 -1
  28. package/dist/core/webhook-processor.d.ts +15 -0
  29. package/dist/core/webhook-processor.d.ts.map +1 -0
  30. package/dist/core/webhook-processor.js +137 -0
  31. package/dist/core/webhook-processor.js.map +1 -0
  32. package/dist/core/webhook-registration.d.ts +26 -0
  33. package/dist/core/webhook-registration.d.ts.map +1 -0
  34. package/dist/core/webhook-registration.js +165 -0
  35. package/dist/core/webhook-registration.js.map +1 -0
  36. package/dist/core/webhook.d.ts +18 -0
  37. package/dist/core/webhook.d.ts.map +1 -0
  38. package/dist/core/webhook.js +38 -0
  39. package/dist/core/webhook.js.map +1 -0
  40. package/dist/db/db.d.ts +68 -0
  41. package/dist/db/db.d.ts.map +1 -1
  42. package/dist/db/installations.d.ts +33 -6
  43. package/dist/db/installations.d.ts.map +1 -1
  44. package/dist/db/installations.js +79 -7
  45. package/dist/db/installations.js.map +1 -1
  46. package/dist/db/schema/installations.d.ts +34 -0
  47. package/dist/db/schema/installations.d.ts.map +1 -1
  48. package/dist/db/schema/installations.js +12 -2
  49. package/dist/db/schema/installations.js.map +1 -1
  50. package/dist/index.d.ts +34 -7
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +76 -8
  53. package/dist/index.js.map +1 -1
  54. package/dist/presentation/routes/install.d.ts +22 -0
  55. package/dist/presentation/routes/install.d.ts.map +1 -1
  56. package/dist/presentation/routes/install.js.map +1 -1
  57. package/dist/presentation/routes/webhooks.d.ts +8 -0
  58. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  59. package/dist/presentation/routes/webhooks.js +108 -0
  60. package/dist/presentation/routes/webhooks.js.map +1 -0
  61. package/dist/temporal/activities/index.d.ts +2 -0
  62. package/dist/temporal/activities/index.d.ts.map +1 -0
  63. package/dist/temporal/activities/index.js +3 -0
  64. package/dist/temporal/activities/index.js.map +1 -0
  65. package/dist/temporal/activities/refresh-jira-tokens.d.ts +20 -0
  66. package/dist/temporal/activities/refresh-jira-tokens.d.ts.map +1 -0
  67. package/dist/temporal/activities/refresh-jira-tokens.js +58 -0
  68. package/dist/temporal/activities/refresh-jira-tokens.js.map +1 -0
  69. package/dist/temporal/constants.d.ts +6 -0
  70. package/dist/temporal/constants.d.ts.map +1 -0
  71. package/dist/temporal/constants.js +8 -0
  72. package/dist/temporal/constants.js.map +1 -0
  73. package/dist/temporal/worker.d.ts +9 -0
  74. package/dist/temporal/worker.d.ts.map +1 -0
  75. package/dist/temporal/worker.js +26 -0
  76. package/dist/temporal/worker.js.map +1 -0
  77. package/dist/temporal/workflows/index.bundle.js +25225 -0
  78. package/dist/temporal/workflows/index.bundle.meta.json +1 -0
  79. package/dist/temporal/workflows/index.d.ts +2 -0
  80. package/dist/temporal/workflows/index.d.ts.map +1 -0
  81. package/dist/temporal/workflows/index.js +3 -0
  82. package/dist/temporal/workflows/index.js.map +1 -0
  83. package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts +2 -0
  84. package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts.map +1 -0
  85. package/dist/temporal/workflows/refresh-jira-tokens-cron.js +20 -0
  86. package/dist/temporal/workflows/refresh-jira-tokens-cron.js.map +1 -0
  87. package/dist/tsconfig.test.tsbuildinfo +1 -1
  88. package/drizzle/0001_jira-token-refresh-state.sql +12 -0
  89. package/drizzle/meta/0001_snapshot.json +245 -0
  90. package/drizzle/meta/_journal.json +7 -0
  91. package/package.json +17 -8
  92. package/src/api/client.test.ts +236 -9
  93. package/src/api/client.ts +192 -0
  94. package/src/config.test.ts +0 -1
  95. package/src/config.ts +1 -4
  96. package/src/core/agent-tools-provider.test.ts +299 -0
  97. package/src/core/agent-tools-provider.ts +175 -0
  98. package/src/core/agent-tools.ts +450 -0
  99. package/src/core/install.test.ts +179 -0
  100. package/src/core/install.ts +85 -19
  101. package/src/core/tokens-refresh.test.ts +223 -4
  102. package/src/core/tokens.test.ts +1 -1
  103. package/src/core/tokens.ts +153 -33
  104. package/src/core/webhook-processor.test.ts +418 -0
  105. package/src/core/webhook-processor.ts +166 -0
  106. package/src/core/webhook-registration.test.ts +375 -0
  107. package/src/core/webhook-registration.ts +216 -0
  108. package/src/core/webhook.ts +69 -0
  109. package/src/db/installations.test.ts +148 -2
  110. package/src/db/installations.ts +133 -7
  111. package/src/db/schema/installations.ts +21 -1
  112. package/src/index.test.ts +42 -0
  113. package/src/index.ts +148 -5
  114. package/src/presentation/routes/install.ts +12 -0
  115. package/src/presentation/routes/webhooks.test.ts +132 -0
  116. package/src/presentation/routes/webhooks.ts +115 -0
  117. package/src/temporal/activities/index.ts +5 -0
  118. package/src/temporal/activities/refresh-jira-tokens.test.ts +156 -0
  119. package/src/temporal/activities/refresh-jira-tokens.ts +88 -0
  120. package/src/temporal/constants.ts +8 -0
  121. package/src/temporal/worker.ts +39 -0
  122. package/src/temporal/workflows/index.ts +1 -0
  123. package/src/temporal/workflows/refresh-jira-tokens-cron.ts +19 -0
  124. package/test/env.ts +0 -1
  125. package/test/factories/jira-installation.ts +4 -0
  126. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,2 +1,17 @@
1
- $ shipfox-swc
2
- Successfully compiled: 20 files with swc (337.94ms)
1
+ $ shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js
2
+ Successfully compiled: 32 files with swc (419.4ms)
3
+ 2026-08-04T14:51:23.448Z [INFO] asset workflow-bundle-21cc907e680c539b74ef.js 3.17 MiB [emitted] [immutable] (name: main)
4
+ orphan modules 33.5 KiB [orphan] 21 modules
5
+ runtime modules 1.13 KiB 5 modules
6
+ modules by path ../../../../node_modules/.pnpm/ 970 KiB 196 modules
7
+ modules by path ./dist/temporal/workflows/ 1.64 KiB
8
+ ./dist/temporal/workflows/index-autogenerated-entrypoint.cjs 941 bytes [built] [code generated]
9
+ ./dist/temporal/workflows/index.js 72 bytes [built] [code generated]
10
+ ./dist/temporal/workflows/refresh-jira-tokens-cron.js 666 bytes [built] [code generated]
11
+ optional modules 30 bytes [optional]
12
+ __temporal_custom_payload_converter (ignored) 15 bytes [optional] [built] [code generated]
13
+ __temporal_custom_failure_converter (ignored) 15 bytes [optional] [built] [code generated]
14
+ ../../../shared/node/temporal/dist/workflow-error-interceptor.js 1.21 KiB [built] [code generated]
15
+ webpack 5.107.2 compiled successfully in 7200 ms
16
+ 2026-08-04T14:51:23.467Z [INFO] Workflow bundle created { size: '3.17MB' }
17
+ /home/runner/work/shipfox/shipfox/libs/api/integration/jira/dist/temporal/workflows/index.js: 3323887 bytes
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @shipfox/api-integration-jira
2
2
 
3
+ ## 12.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 70e33c0: Adds the in-process Jira REST agent-tool catalog and write-selection support.
8
+
9
+ ### Patch Changes
10
+
11
+ - af7adfc: Adds a six-hour Jira refresh-token maintenance worker that refreshes idle tokens and migrates existing installations with backfilled refresh state. Token access now fails closed for non-active connections, and rejected refresh tokens or ambiguous refresh timeouts require reconnecting Jira.
12
+
13
+ ## 12.0.0
14
+
15
+ ### Major Changes
16
+
17
+ - f13e8bb: Add Jira dynamic webhook registration and authenticated event ingestion through
18
+ the shared stored-webhook workflow. Update the SPI webhook request exports and
19
+ serialize Jira installation replacement across API replicas. Preserve Jira
20
+ delivery identifiers and require lifecycle callbacks for registration. Remove
21
+ the unused Jira webhook signing-secret configuration and allow HS256 verification
22
+ at a supplied receipt time.
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies [f78740d]
27
+ - Updated dependencies [24ef475]
28
+ - Updated dependencies [f13e8bb]
29
+ - Updated dependencies [869a792]
30
+ - Updated dependencies [54c820e]
31
+ - @shipfox/node-fastify@0.4.1
32
+ - @shipfox/api-integration-spi@1.0.0
33
+ - @shipfox/node-jwt@0.4.0
34
+ - @shipfox/node-postgres@0.5.0
35
+ - @shipfox/api-auth-context@12.0.0
36
+ - @shipfox/api-integration-jira-dto@12.0.0
37
+ - @shipfox/node-drizzle@0.3.5
38
+
3
39
  ## 11.0.0
4
40
 
5
41
  ### Patch Changes
package/README.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Jira integration
2
2
 
3
- This package owns Jira provider persistence and token storage. The provider is non-functional until ENG-1001 adds the OAuth install flow.
3
+ Jira OAuth, site links, token storage, webhooks, and token upkeep for Shipfox.
4
4
 
5
- Connect Jira with a dedicated Shipfox service account. Jira 3LO actions are authored by the authorizing account, so events from that account are dropped to prevent agent actions from triggering themselves.
5
+ ## What it does
6
+
7
+ - **`createJiraIntegrationProvider`** builds Jira OAuth and webhook routes.
8
+ - **`createJiraTokenStore`** stores Jira access and refresh tokens.
9
+ - **`createJiraMaintenanceWorker`** runs the token refresh workflow.
10
+ - **Installation functions** store Jira sites, webhooks, and token dates.
11
+
12
+ ## Token upkeep
13
+
14
+ The worker runs every six hours.
15
+
16
+ It checks active sites in small batches. It refreshes a token after 76 idle days. Jira refresh tokens expire after about 90 idle days. The 14-day lead gives time for recovery.
17
+
18
+ A rejected refresh token or timeout marks the link as `error`. The user must connect Jira again. A new OAuth flow clears this state.
19
+
20
+ ## Installation and setup
21
+
22
+ ```sh
23
+ pnpm add @shipfox/api-integration-jira
24
+ ```
25
+
26
+ The host supplies OAuth config, secret storage, connection lookup, and database access.
27
+
28
+ ## Usage
29
+
30
+ ```ts
31
+ import {createJiraIntegrationProvider} from '@shipfox/api-integration-jira';
32
+
33
+ const provider = createJiraIntegrationProvider();
34
+
35
+ console.log(provider.provider); // "jira"
36
+ ```
37
+
38
+ The composed integrations module supplies route and worker dependencies.
39
+
40
+ ## Behavior notes
41
+
42
+ Connect Jira with a dedicated Shipfox service account. Jira 3LO actions use the authorizing account. Events from that account are dropped to prevent agent self-triggering.
43
+
44
+ ## Data model
45
+
46
+ The package owns the `integrations_jira_installations` table. The `refresh_token_last_used_at` column records the last successful refresh-token use. The `refresh_token_last_attempted_at` column rotates maintenance attempts so failed or inactive connections cannot starve the capped sweep.
47
+
48
+ ## Development
49
+
50
+ ```sh
51
+ mise exec -- turbo check --filter=@shipfox/api-integration-jira
52
+ mise exec -- turbo type --filter=@shipfox/api-integration-jira
53
+ mise exec -- turbo test --filter=@shipfox/api-integration-jira
54
+ mise exec -- turbo build --filter=@shipfox/api-integration-jira
55
+ ```
56
+
57
+ Database tests need the local PostgreSQL test service.
58
+
59
+ ## License
60
+
61
+ MIT
@@ -1,3 +1,5 @@
1
+ export declare const JIRA_DYNAMIC_WEBHOOK_EVENTS: readonly ["jira:issue_created", "jira:issue_updated", "jira:issue_deleted", "comment_created", "comment_updated", "comment_deleted"];
2
+ export declare const JIRA_DYNAMIC_WEBHOOK_JQL = "";
1
3
  export interface JiraAuthorization {
2
4
  accessToken: string;
3
5
  refreshToken?: string | undefined;
@@ -13,6 +15,27 @@ export interface JiraAccessibleResource {
13
15
  export interface JiraIdentity {
14
16
  accountId: string;
15
17
  }
18
+ export interface JiraDynamicWebhookRegistration {
19
+ webhookId: number;
20
+ }
21
+ export type JiraAgentToolHttpMethod = 'GET' | 'POST' | 'PUT';
22
+ export type JiraAgentToolQueryValue = string | number | boolean | readonly (string | number)[] | undefined;
23
+ export interface JiraAgentToolRequest {
24
+ accessToken: string;
25
+ cloudId: string;
26
+ method: JiraAgentToolHttpMethod;
27
+ path: string;
28
+ query?: Record<string, JiraAgentToolQueryValue> | undefined;
29
+ body?: unknown;
30
+ operation?: string | undefined;
31
+ }
32
+ export interface JiraAgentToolResponse {
33
+ status: number;
34
+ body: unknown;
35
+ }
36
+ export interface JiraAgentToolsClient {
37
+ request(input: JiraAgentToolRequest): Promise<JiraAgentToolResponse>;
38
+ }
16
39
  export interface JiraApiClient {
17
40
  exchangeAuthorizationCode(input: {
18
41
  code: string;
@@ -27,7 +50,18 @@ export interface JiraApiClient {
27
50
  accessToken: string;
28
51
  cloudId: string;
29
52
  }): Promise<JiraIdentity>;
53
+ registerDynamicWebhook(input: {
54
+ accessToken: string;
55
+ cloudId: string;
56
+ url: string;
57
+ }): Promise<JiraDynamicWebhookRegistration>;
58
+ deleteDynamicWebhook(input: {
59
+ accessToken: string;
60
+ cloudId: string;
61
+ webhookId: number;
62
+ }): Promise<void>;
30
63
  }
31
64
  export declare function createJiraApiClient(): JiraApiClient;
65
+ export declare function createJiraAgentToolsClient(): JiraAgentToolsClient;
32
66
  export declare function mapJiraError<T>(operation: string, request: () => Promise<T>): Promise<T>;
33
67
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,yBAAyB,CAAC,KAAK,EAAE;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC7E,kBAAkB,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9E,sBAAsB,CAAC,KAAK,EAAE;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACxF,SAAS,CAAC,KAAK,EAAE;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACjF;AAoBD,wBAAgB,mBAAmB,IAAI,aAAa,CAmEnD;AAmDD,wBAAsB,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAgC9F"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,2BAA2B,sIAAwB,CAAC;AACjE,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,8BAA8B;IAC7C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,uBAAuB,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;AAE7D,MAAM,MAAM,uBAAuB,GAC/B,MAAM,GACN,MAAM,GACN,OAAO,GACP,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAC5B,SAAS,CAAC;AAEd,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,uBAAuB,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,GAAG,SAAS,CAAC;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACtE;AAED,MAAM,WAAW,aAAa;IAC5B,yBAAyB,CAAC,KAAK,EAAE;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC7E,kBAAkB,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9E,sBAAsB,CAAC,KAAK,EAAE;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACxF,SAAS,CAAC,KAAK,EAAE;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAChF,sBAAsB,CAAC,KAAK,EAAE;QAC5B,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,GAAG,EAAE,MAAM,CAAC;KACb,GAAG,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAC5C,oBAAoB,CAAC,KAAK,EAAE;QAC1B,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AA0BD,wBAAgB,mBAAmB,IAAI,aAAa,CAkGnD;AAED,wBAAgB,0BAA0B,IAAI,oBAAoB,CAEjE;AA6ID,wBAAsB,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAyC9F"}
@@ -1,9 +1,17 @@
1
+ import { jiraWebhookEventNames } from '@shipfox/api-integration-jira-dto';
1
2
  import { logger } from '@shipfox/node-opentelemetry';
2
3
  import ky, { HTTPError, TimeoutError } from 'ky';
3
4
  import { config } from '#config.js';
4
5
  import { JiraIntegrationProviderError } from '#core/errors.js';
5
6
  const JIRA_API_TIMEOUT_MS = 10_000;
6
7
  const SCOPE_SEPARATOR_RE = /[,\s]+/;
8
+ const TRAILING_SLASHES_RE = /\/+$/;
9
+ export const JIRA_DYNAMIC_WEBHOOK_EVENTS = jiraWebhookEventNames;
10
+ export const JIRA_DYNAMIC_WEBHOOK_JQL = '';
11
+ const JIRA_REFRESH_TOKEN_TERMINAL_ERROR_CODES = new Set([
12
+ 'invalid_grant',
13
+ 'unauthorized_client'
14
+ ]);
7
15
  export function createJiraApiClient() {
8
16
  return {
9
17
  async exchangeAuthorizationCode (input) {
@@ -56,9 +64,121 @@ export function createJiraApiClient() {
56
64
  return {
57
65
  accountId: body.accountId
58
66
  };
67
+ },
68
+ async registerDynamicWebhook (input) {
69
+ const body = await mapJiraError('register-dynamic-webhook', ()=>ky.post(`${config.JIRA_API_BASE_URL}/ex/jira/${input.cloudId}/rest/api/3/webhook`, {
70
+ headers: {
71
+ authorization: `Bearer ${input.accessToken}`
72
+ },
73
+ json: {
74
+ url: input.url,
75
+ webhooks: [
76
+ {
77
+ events: JIRA_DYNAMIC_WEBHOOK_EVENTS,
78
+ jqlFilter: JIRA_DYNAMIC_WEBHOOK_JQL
79
+ }
80
+ ]
81
+ },
82
+ timeout: JIRA_API_TIMEOUT_MS
83
+ }).json());
84
+ return parseDynamicWebhookRegistration(body);
85
+ },
86
+ async deleteDynamicWebhook (input) {
87
+ await mapJiraError('delete-dynamic-webhook', ()=>ky.delete(`${config.JIRA_API_BASE_URL}/ex/jira/${input.cloudId}/rest/api/3/webhook`, {
88
+ headers: {
89
+ authorization: `Bearer ${input.accessToken}`
90
+ },
91
+ json: {
92
+ webhookIds: [
93
+ input.webhookId
94
+ ]
95
+ },
96
+ timeout: JIRA_API_TIMEOUT_MS
97
+ }));
59
98
  }
60
99
  };
61
100
  }
101
+ export function createJiraAgentToolsClient() {
102
+ return {
103
+ request: requestJiraRest
104
+ };
105
+ }
106
+ async function requestJiraRest(input) {
107
+ return await mapJiraError(input.operation ?? 'agent-tool', async ()=>{
108
+ try {
109
+ const response = await ky(jiraRestUrl(input.cloudId, input.path), {
110
+ method: input.method,
111
+ headers: {
112
+ authorization: `Bearer ${input.accessToken}`
113
+ },
114
+ ...input.query === undefined ? {} : {
115
+ searchParams: jiraQueryParams(input.query)
116
+ },
117
+ ...input.body === undefined ? {} : {
118
+ json: input.body
119
+ },
120
+ timeout: JIRA_API_TIMEOUT_MS
121
+ });
122
+ return {
123
+ status: response.status,
124
+ body: await readJiraResponseBody(response)
125
+ };
126
+ } catch (error) {
127
+ if (error instanceof HTTPError && (error.response.status === 400 || error.response.status === 404)) {
128
+ return {
129
+ status: error.response.status,
130
+ body: await readJiraResponseBody(error.response)
131
+ };
132
+ }
133
+ throw error;
134
+ }
135
+ });
136
+ }
137
+ function jiraRestUrl(cloudId, path) {
138
+ const baseUrl = config.JIRA_API_BASE_URL.replace(TRAILING_SLASHES_RE, '');
139
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
140
+ return `${baseUrl}/ex/jira/${encodeURIComponent(cloudId)}/rest/api/3${normalizedPath}`;
141
+ }
142
+ function jiraQueryParams(query) {
143
+ const params = new URLSearchParams();
144
+ for (const [key, value] of Object.entries(query)){
145
+ if (value === undefined) continue;
146
+ if (Array.isArray(value)) {
147
+ for (const item of value)params.append(key, String(item));
148
+ continue;
149
+ }
150
+ params.set(key, String(value));
151
+ }
152
+ return params.size > 0 ? params : undefined;
153
+ }
154
+ async function readJiraResponseBody(response) {
155
+ const text = await response.text();
156
+ if (text.length === 0) return undefined;
157
+ try {
158
+ return JSON.parse(text);
159
+ } catch {
160
+ return text;
161
+ }
162
+ }
163
+ function parseDynamicWebhookRegistration(body) {
164
+ if (!body || typeof body !== 'object' || !Array.isArray(body.webhookRegistrationResult) || body.webhookRegistrationResult.length !== 1) {
165
+ throw malformed('Jira webhook registration response did not contain exactly one result');
166
+ }
167
+ const result = body.webhookRegistrationResult[0];
168
+ if (!result || typeof result !== 'object') {
169
+ throw malformed('Jira webhook registration result was malformed');
170
+ }
171
+ const { createdWebhookId, errors } = result;
172
+ if (errors !== undefined && (!Array.isArray(errors) || errors.length > 0)) {
173
+ throw malformed('Jira webhook registration returned errors');
174
+ }
175
+ if (typeof createdWebhookId !== 'number' || !Number.isSafeInteger(createdWebhookId) || createdWebhookId <= 0) {
176
+ throw malformed('Jira webhook registration did not return a created webhook id');
177
+ }
178
+ return {
179
+ webhookId: createdWebhookId
180
+ };
181
+ }
62
182
  function parseAuthorization(body) {
63
183
  if (typeof body.access_token !== 'string' || body.access_token.length === 0) {
64
184
  throw malformed('Jira authorization response did not include an access token');
@@ -115,6 +235,12 @@ export async function mapJiraError(operation, request) {
115
235
  if (status === 401 || status === 403) {
116
236
  throw new JiraIntegrationProviderError('access-denied', 'Jira request was rejected');
117
237
  }
238
+ if (status === 400 && operation === 'refresh-access-token') {
239
+ const errorCode = readJiraOAuthErrorCode(error.data);
240
+ if (errorCode && JIRA_REFRESH_TOKEN_TERMINAL_ERROR_CODES.has(errorCode)) {
241
+ throw new JiraIntegrationProviderError('access-denied', 'Jira refresh token was rejected; reconnect is required');
242
+ }
243
+ }
118
244
  throw malformed('Jira request was rejected');
119
245
  }
120
246
  if (error instanceof TimeoutError) {
@@ -133,6 +259,11 @@ export async function mapJiraError(operation, request) {
133
259
  function malformed(message) {
134
260
  return new JiraIntegrationProviderError('malformed-provider-response', message);
135
261
  }
262
+ function readJiraOAuthErrorCode(data) {
263
+ if (!data || typeof data !== 'object') return undefined;
264
+ const errorCode = data.error;
265
+ return typeof errorCode === 'string' ? errorCode : undefined;
266
+ }
136
267
  function retryAfterSeconds(headers) {
137
268
  const value = headers.get('retry-after');
138
269
  if (!value) return undefined;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/client.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport ky, {HTTPError, TimeoutError} from 'ky';\nimport {config} from '#config.js';\nimport {JiraIntegrationProviderError} from '#core/errors.js';\n\nconst JIRA_API_TIMEOUT_MS = 10_000;\nconst SCOPE_SEPARATOR_RE = /[,\\s]+/;\n\nexport interface JiraAuthorization {\n accessToken: string;\n refreshToken?: string | undefined;\n expiresAt?: Date | undefined;\n scopes: string[];\n}\n\nexport interface JiraAccessibleResource {\n cloudId: string;\n name: string;\n url: string;\n scopes: string[];\n}\n\nexport interface JiraIdentity {\n accountId: string;\n}\n\nexport interface JiraApiClient {\n exchangeAuthorizationCode(input: {code: string}): Promise<JiraAuthorization>;\n refreshAccessToken(input: {refreshToken: string}): Promise<JiraAuthorization>;\n getAccessibleResources(input: {accessToken: string}): Promise<JiraAccessibleResource[]>;\n getMyself(input: {accessToken: string; cloudId: string}): Promise<JiraIdentity>;\n}\n\ninterface JiraTokenResponse {\n access_token?: unknown;\n refresh_token?: unknown;\n expires_in?: unknown;\n scope?: unknown;\n}\n\ninterface JiraResourceResponse {\n id?: unknown;\n name?: unknown;\n url?: unknown;\n scopes?: unknown;\n}\n\ninterface JiraMyselfResponse {\n accountId?: unknown;\n}\n\nexport function createJiraApiClient(): JiraApiClient {\n return {\n async exchangeAuthorizationCode(input) {\n const body = await mapJiraError('exchange-authorization-code', () =>\n ky\n .post(`${config.JIRA_AUTH_BASE_URL}/oauth/token`, {\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: config.JIRA_OAUTH_CLIENT_ID,\n client_secret: config.JIRA_OAUTH_CLIENT_SECRET,\n code: input.code,\n redirect_uri: config.JIRA_OAUTH_REDIRECT_URL,\n }),\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<JiraTokenResponse>(),\n );\n return parseAuthorization(body);\n },\n\n async refreshAccessToken(input) {\n const body = await mapJiraError('refresh-access-token', () =>\n ky\n .post(`${config.JIRA_AUTH_BASE_URL}/oauth/token`, {\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: config.JIRA_OAUTH_CLIENT_ID,\n client_secret: config.JIRA_OAUTH_CLIENT_SECRET,\n refresh_token: input.refreshToken,\n }),\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<JiraTokenResponse>(),\n );\n return parseAuthorization(body);\n },\n\n async getAccessibleResources(input) {\n const body = await mapJiraError('get-accessible-resources', () =>\n ky\n .get(`${config.JIRA_API_BASE_URL}/oauth/token/accessible-resources`, {\n headers: {authorization: `Bearer ${input.accessToken}`},\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<unknown>(),\n );\n if (!Array.isArray(body)) {\n throw malformed('Jira accessible-resources response was not an array');\n }\n return body.map(parseAccessibleResource);\n },\n\n async getMyself(input) {\n const body = await mapJiraError('get-myself', () =>\n ky\n .get(`${config.JIRA_API_BASE_URL}/ex/jira/${input.cloudId}/rest/api/3/myself`, {\n headers: {authorization: `Bearer ${input.accessToken}`},\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<JiraMyselfResponse>(),\n );\n if (typeof body.accountId !== 'string' || body.accountId.length === 0) {\n throw malformed('Jira identity response did not include an accountId');\n }\n return {accountId: body.accountId};\n },\n };\n}\n\nfunction parseAuthorization(body: JiraTokenResponse): JiraAuthorization {\n if (typeof body.access_token !== 'string' || body.access_token.length === 0) {\n throw malformed('Jira authorization response did not include an access token');\n }\n return {\n accessToken: body.access_token,\n refreshToken: typeof body.refresh_token === 'string' ? body.refresh_token : undefined,\n expiresAt: parseExpiresAt(body.expires_in),\n scopes: parseScopes(body.scope),\n };\n}\n\nfunction parseAccessibleResource(value: unknown): JiraAccessibleResource {\n if (!value || typeof value !== 'object') throw malformed('Jira site response was malformed');\n const {id, name, url, scopes} = value as JiraResourceResponse;\n if (\n typeof id !== 'string' ||\n id.length === 0 ||\n typeof name !== 'string' ||\n name.length === 0 ||\n typeof url !== 'string' ||\n url.length === 0 ||\n !Array.isArray(scopes) ||\n !scopes.every((scope) => typeof scope === 'string')\n ) {\n throw malformed('Jira site response did not include a valid cloud id, name, URL, and scopes');\n }\n return {cloudId: id, name, url, scopes};\n}\n\nfunction parseScopes(scope: unknown): string[] {\n if (typeof scope === 'string')\n return scope\n .split(SCOPE_SEPARATOR_RE)\n .map((value) => value.trim())\n .filter(Boolean);\n if (Array.isArray(scope) && scope.every((value) => typeof value === 'string')) return scope;\n if (scope === undefined) return [];\n throw malformed('Jira authorization response included malformed scopes');\n}\n\nfunction parseExpiresAt(expiresIn: unknown): Date | undefined {\n if (expiresIn === undefined) return undefined;\n if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {\n throw malformed('Jira authorization response included a malformed expiry');\n }\n return new Date(Date.now() + expiresIn * 1000);\n}\n\nexport async function mapJiraError<T>(operation: string, request: () => Promise<T>): Promise<T> {\n try {\n return await request();\n } catch (error) {\n if (error instanceof JiraIntegrationProviderError) throw error;\n if (error instanceof HTTPError) {\n const {status, statusText, headers} = error.response;\n logger().warn({operation, status, statusText}, 'Jira API request rejected');\n if (status === 429) {\n throw new JiraIntegrationProviderError(\n 'rate-limited',\n 'Jira request was rate limited',\n retryAfterSeconds(headers),\n );\n }\n if (status >= 500)\n throw new JiraIntegrationProviderError('provider-unavailable', 'Jira request failed');\n if (status === 401 || status === 403) {\n throw new JiraIntegrationProviderError('access-denied', 'Jira request was rejected');\n }\n throw malformed('Jira request was rejected');\n }\n if (error instanceof TimeoutError) {\n logger().warn({operation}, 'Jira API request timed out');\n throw new JiraIntegrationProviderError('timeout', 'Jira request timed out');\n }\n logger().warn(\n {operation, errName: error instanceof Error ? error.name : typeof error},\n 'Jira API request failed',\n );\n throw new JiraIntegrationProviderError('provider-unavailable', 'Jira request failed');\n }\n}\n\nfunction malformed(message: string): JiraIntegrationProviderError {\n return new JiraIntegrationProviderError('malformed-provider-response', message);\n}\n\nfunction retryAfterSeconds(headers: Headers): number | undefined {\n const value = headers.get('retry-after');\n if (!value) return undefined;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n"],"names":["logger","ky","HTTPError","TimeoutError","config","JiraIntegrationProviderError","JIRA_API_TIMEOUT_MS","SCOPE_SEPARATOR_RE","createJiraApiClient","exchangeAuthorizationCode","input","body","mapJiraError","post","JIRA_AUTH_BASE_URL","URLSearchParams","grant_type","client_id","JIRA_OAUTH_CLIENT_ID","client_secret","JIRA_OAUTH_CLIENT_SECRET","code","redirect_uri","JIRA_OAUTH_REDIRECT_URL","timeout","json","parseAuthorization","refreshAccessToken","refresh_token","refreshToken","getAccessibleResources","get","JIRA_API_BASE_URL","headers","authorization","accessToken","Array","isArray","malformed","map","parseAccessibleResource","getMyself","cloudId","accountId","length","access_token","undefined","expiresAt","parseExpiresAt","expires_in","scopes","parseScopes","scope","value","id","name","url","every","split","trim","filter","Boolean","expiresIn","Number","isFinite","Date","now","operation","request","error","status","statusText","response","warn","retryAfterSeconds","errName","Error","message","parsed","parseInt","isNaN"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/api/client.ts"],"sourcesContent":["import {jiraWebhookEventNames} from '@shipfox/api-integration-jira-dto';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport ky, {HTTPError, TimeoutError} from 'ky';\nimport {config} from '#config.js';\nimport {JiraIntegrationProviderError} from '#core/errors.js';\n\nconst JIRA_API_TIMEOUT_MS = 10_000;\nconst SCOPE_SEPARATOR_RE = /[,\\s]+/;\nconst TRAILING_SLASHES_RE = /\\/+$/;\nexport const JIRA_DYNAMIC_WEBHOOK_EVENTS = jiraWebhookEventNames;\nexport const JIRA_DYNAMIC_WEBHOOK_JQL = '';\n\nexport interface JiraAuthorization {\n accessToken: string;\n refreshToken?: string | undefined;\n expiresAt?: Date | undefined;\n scopes: string[];\n}\n\nexport interface JiraAccessibleResource {\n cloudId: string;\n name: string;\n url: string;\n scopes: string[];\n}\n\nexport interface JiraIdentity {\n accountId: string;\n}\n\nexport interface JiraDynamicWebhookRegistration {\n webhookId: number;\n}\n\nexport type JiraAgentToolHttpMethod = 'GET' | 'POST' | 'PUT';\n\nexport type JiraAgentToolQueryValue =\n | string\n | number\n | boolean\n | readonly (string | number)[]\n | undefined;\n\nexport interface JiraAgentToolRequest {\n accessToken: string;\n cloudId: string;\n method: JiraAgentToolHttpMethod;\n path: string;\n query?: Record<string, JiraAgentToolQueryValue> | undefined;\n body?: unknown;\n operation?: string | undefined;\n}\n\nexport interface JiraAgentToolResponse {\n status: number;\n body: unknown;\n}\n\nexport interface JiraAgentToolsClient {\n request(input: JiraAgentToolRequest): Promise<JiraAgentToolResponse>;\n}\n\nexport interface JiraApiClient {\n exchangeAuthorizationCode(input: {code: string}): Promise<JiraAuthorization>;\n refreshAccessToken(input: {refreshToken: string}): Promise<JiraAuthorization>;\n getAccessibleResources(input: {accessToken: string}): Promise<JiraAccessibleResource[]>;\n getMyself(input: {accessToken: string; cloudId: string}): Promise<JiraIdentity>;\n registerDynamicWebhook(input: {\n accessToken: string;\n cloudId: string;\n url: string;\n }): Promise<JiraDynamicWebhookRegistration>;\n deleteDynamicWebhook(input: {\n accessToken: string;\n cloudId: string;\n webhookId: number;\n }): Promise<void>;\n}\n\ninterface JiraTokenResponse {\n access_token?: unknown;\n refresh_token?: unknown;\n expires_in?: unknown;\n scope?: unknown;\n}\n\ninterface JiraResourceResponse {\n id?: unknown;\n name?: unknown;\n url?: unknown;\n scopes?: unknown;\n}\n\ninterface JiraMyselfResponse {\n accountId?: unknown;\n}\n\ninterface JiraDynamicWebhookRegistrationResponse {\n webhookRegistrationResult?: unknown;\n}\n\nconst JIRA_REFRESH_TOKEN_TERMINAL_ERROR_CODES = new Set(['invalid_grant', 'unauthorized_client']);\n\nexport function createJiraApiClient(): JiraApiClient {\n return {\n async exchangeAuthorizationCode(input) {\n const body = await mapJiraError('exchange-authorization-code', () =>\n ky\n .post(`${config.JIRA_AUTH_BASE_URL}/oauth/token`, {\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: config.JIRA_OAUTH_CLIENT_ID,\n client_secret: config.JIRA_OAUTH_CLIENT_SECRET,\n code: input.code,\n redirect_uri: config.JIRA_OAUTH_REDIRECT_URL,\n }),\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<JiraTokenResponse>(),\n );\n return parseAuthorization(body);\n },\n\n async refreshAccessToken(input) {\n const body = await mapJiraError('refresh-access-token', () =>\n ky\n .post(`${config.JIRA_AUTH_BASE_URL}/oauth/token`, {\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: config.JIRA_OAUTH_CLIENT_ID,\n client_secret: config.JIRA_OAUTH_CLIENT_SECRET,\n refresh_token: input.refreshToken,\n }),\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<JiraTokenResponse>(),\n );\n return parseAuthorization(body);\n },\n\n async getAccessibleResources(input) {\n const body = await mapJiraError('get-accessible-resources', () =>\n ky\n .get(`${config.JIRA_API_BASE_URL}/oauth/token/accessible-resources`, {\n headers: {authorization: `Bearer ${input.accessToken}`},\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<unknown>(),\n );\n if (!Array.isArray(body)) {\n throw malformed('Jira accessible-resources response was not an array');\n }\n return body.map(parseAccessibleResource);\n },\n\n async getMyself(input) {\n const body = await mapJiraError('get-myself', () =>\n ky\n .get(`${config.JIRA_API_BASE_URL}/ex/jira/${input.cloudId}/rest/api/3/myself`, {\n headers: {authorization: `Bearer ${input.accessToken}`},\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<JiraMyselfResponse>(),\n );\n if (typeof body.accountId !== 'string' || body.accountId.length === 0) {\n throw malformed('Jira identity response did not include an accountId');\n }\n return {accountId: body.accountId};\n },\n\n async registerDynamicWebhook(input) {\n const body = await mapJiraError('register-dynamic-webhook', () =>\n ky\n .post(`${config.JIRA_API_BASE_URL}/ex/jira/${input.cloudId}/rest/api/3/webhook`, {\n headers: {authorization: `Bearer ${input.accessToken}`},\n json: {\n url: input.url,\n webhooks: [\n {\n events: JIRA_DYNAMIC_WEBHOOK_EVENTS,\n jqlFilter: JIRA_DYNAMIC_WEBHOOK_JQL,\n },\n ],\n },\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<JiraDynamicWebhookRegistrationResponse>(),\n );\n return parseDynamicWebhookRegistration(body);\n },\n\n async deleteDynamicWebhook(input) {\n await mapJiraError('delete-dynamic-webhook', () =>\n ky.delete(`${config.JIRA_API_BASE_URL}/ex/jira/${input.cloudId}/rest/api/3/webhook`, {\n headers: {authorization: `Bearer ${input.accessToken}`},\n json: {webhookIds: [input.webhookId]},\n timeout: JIRA_API_TIMEOUT_MS,\n }),\n );\n },\n };\n}\n\nexport function createJiraAgentToolsClient(): JiraAgentToolsClient {\n return {request: requestJiraRest};\n}\n\nasync function requestJiraRest(input: JiraAgentToolRequest): Promise<JiraAgentToolResponse> {\n return await mapJiraError(input.operation ?? 'agent-tool', async () => {\n try {\n const response = await ky(jiraRestUrl(input.cloudId, input.path), {\n method: input.method,\n headers: {authorization: `Bearer ${input.accessToken}`},\n ...(input.query === undefined ? {} : {searchParams: jiraQueryParams(input.query)}),\n ...(input.body === undefined ? {} : {json: input.body}),\n timeout: JIRA_API_TIMEOUT_MS,\n });\n return {status: response.status, body: await readJiraResponseBody(response)};\n } catch (error) {\n if (\n error instanceof HTTPError &&\n (error.response.status === 400 || error.response.status === 404)\n ) {\n return {\n status: error.response.status,\n body: await readJiraResponseBody(error.response),\n };\n }\n throw error;\n }\n });\n}\n\nfunction jiraRestUrl(cloudId: string, path: string): string {\n const baseUrl = config.JIRA_API_BASE_URL.replace(TRAILING_SLASHES_RE, '');\n const normalizedPath = path.startsWith('/') ? path : `/${path}`;\n return `${baseUrl}/ex/jira/${encodeURIComponent(cloudId)}/rest/api/3${normalizedPath}`;\n}\n\nfunction jiraQueryParams(\n query: Record<string, JiraAgentToolQueryValue>,\n): URLSearchParams | undefined {\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const item of value) params.append(key, String(item));\n continue;\n }\n params.set(key, String(value));\n }\n return params.size > 0 ? params : undefined;\n}\n\nasync function readJiraResponseBody(response: Response): Promise<unknown> {\n const text = await response.text();\n if (text.length === 0) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction parseDynamicWebhookRegistration(\n body: JiraDynamicWebhookRegistrationResponse,\n): JiraDynamicWebhookRegistration {\n if (\n !body ||\n typeof body !== 'object' ||\n !Array.isArray(body.webhookRegistrationResult) ||\n body.webhookRegistrationResult.length !== 1\n ) {\n throw malformed('Jira webhook registration response did not contain exactly one result');\n }\n\n const result = body.webhookRegistrationResult[0];\n if (!result || typeof result !== 'object') {\n throw malformed('Jira webhook registration result was malformed');\n }\n const {createdWebhookId, errors} = result as {\n createdWebhookId?: unknown;\n errors?: unknown;\n };\n if (errors !== undefined && (!Array.isArray(errors) || errors.length > 0)) {\n throw malformed('Jira webhook registration returned errors');\n }\n if (\n typeof createdWebhookId !== 'number' ||\n !Number.isSafeInteger(createdWebhookId) ||\n createdWebhookId <= 0\n ) {\n throw malformed('Jira webhook registration did not return a created webhook id');\n }\n return {webhookId: createdWebhookId};\n}\n\nfunction parseAuthorization(body: JiraTokenResponse): JiraAuthorization {\n if (typeof body.access_token !== 'string' || body.access_token.length === 0) {\n throw malformed('Jira authorization response did not include an access token');\n }\n return {\n accessToken: body.access_token,\n refreshToken: typeof body.refresh_token === 'string' ? body.refresh_token : undefined,\n expiresAt: parseExpiresAt(body.expires_in),\n scopes: parseScopes(body.scope),\n };\n}\n\nfunction parseAccessibleResource(value: unknown): JiraAccessibleResource {\n if (!value || typeof value !== 'object') throw malformed('Jira site response was malformed');\n const {id, name, url, scopes} = value as JiraResourceResponse;\n if (\n typeof id !== 'string' ||\n id.length === 0 ||\n typeof name !== 'string' ||\n name.length === 0 ||\n typeof url !== 'string' ||\n url.length === 0 ||\n !Array.isArray(scopes) ||\n !scopes.every((scope) => typeof scope === 'string')\n ) {\n throw malformed('Jira site response did not include a valid cloud id, name, URL, and scopes');\n }\n return {cloudId: id, name, url, scopes};\n}\n\nfunction parseScopes(scope: unknown): string[] {\n if (typeof scope === 'string')\n return scope\n .split(SCOPE_SEPARATOR_RE)\n .map((value) => value.trim())\n .filter(Boolean);\n if (Array.isArray(scope) && scope.every((value) => typeof value === 'string')) return scope;\n if (scope === undefined) return [];\n throw malformed('Jira authorization response included malformed scopes');\n}\n\nfunction parseExpiresAt(expiresIn: unknown): Date | undefined {\n if (expiresIn === undefined) return undefined;\n if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {\n throw malformed('Jira authorization response included a malformed expiry');\n }\n return new Date(Date.now() + expiresIn * 1000);\n}\n\nexport async function mapJiraError<T>(operation: string, request: () => Promise<T>): Promise<T> {\n try {\n return await request();\n } catch (error) {\n if (error instanceof JiraIntegrationProviderError) throw error;\n if (error instanceof HTTPError) {\n const {status, statusText, headers} = error.response;\n logger().warn({operation, status, statusText}, 'Jira API request rejected');\n if (status === 429) {\n throw new JiraIntegrationProviderError(\n 'rate-limited',\n 'Jira request was rate limited',\n retryAfterSeconds(headers),\n );\n }\n if (status >= 500)\n throw new JiraIntegrationProviderError('provider-unavailable', 'Jira request failed');\n if (status === 401 || status === 403) {\n throw new JiraIntegrationProviderError('access-denied', 'Jira request was rejected');\n }\n if (status === 400 && operation === 'refresh-access-token') {\n const errorCode = readJiraOAuthErrorCode(error.data);\n if (errorCode && JIRA_REFRESH_TOKEN_TERMINAL_ERROR_CODES.has(errorCode)) {\n throw new JiraIntegrationProviderError(\n 'access-denied',\n 'Jira refresh token was rejected; reconnect is required',\n );\n }\n }\n throw malformed('Jira request was rejected');\n }\n if (error instanceof TimeoutError) {\n logger().warn({operation}, 'Jira API request timed out');\n throw new JiraIntegrationProviderError('timeout', 'Jira request timed out');\n }\n logger().warn(\n {operation, errName: error instanceof Error ? error.name : typeof error},\n 'Jira API request failed',\n );\n throw new JiraIntegrationProviderError('provider-unavailable', 'Jira request failed');\n }\n}\n\nfunction malformed(message: string): JiraIntegrationProviderError {\n return new JiraIntegrationProviderError('malformed-provider-response', message);\n}\n\nfunction readJiraOAuthErrorCode(data: unknown): string | undefined {\n if (!data || typeof data !== 'object') return undefined;\n const errorCode = (data as {error?: unknown}).error;\n return typeof errorCode === 'string' ? errorCode : undefined;\n}\n\nfunction retryAfterSeconds(headers: Headers): number | undefined {\n const value = headers.get('retry-after');\n if (!value) return undefined;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n"],"names":["jiraWebhookEventNames","logger","ky","HTTPError","TimeoutError","config","JiraIntegrationProviderError","JIRA_API_TIMEOUT_MS","SCOPE_SEPARATOR_RE","TRAILING_SLASHES_RE","JIRA_DYNAMIC_WEBHOOK_EVENTS","JIRA_DYNAMIC_WEBHOOK_JQL","JIRA_REFRESH_TOKEN_TERMINAL_ERROR_CODES","Set","createJiraApiClient","exchangeAuthorizationCode","input","body","mapJiraError","post","JIRA_AUTH_BASE_URL","URLSearchParams","grant_type","client_id","JIRA_OAUTH_CLIENT_ID","client_secret","JIRA_OAUTH_CLIENT_SECRET","code","redirect_uri","JIRA_OAUTH_REDIRECT_URL","timeout","json","parseAuthorization","refreshAccessToken","refresh_token","refreshToken","getAccessibleResources","get","JIRA_API_BASE_URL","headers","authorization","accessToken","Array","isArray","malformed","map","parseAccessibleResource","getMyself","cloudId","accountId","length","registerDynamicWebhook","url","webhooks","events","jqlFilter","parseDynamicWebhookRegistration","deleteDynamicWebhook","delete","webhookIds","webhookId","createJiraAgentToolsClient","request","requestJiraRest","operation","response","jiraRestUrl","path","method","query","undefined","searchParams","jiraQueryParams","status","readJiraResponseBody","error","baseUrl","replace","normalizedPath","startsWith","encodeURIComponent","params","key","value","Object","entries","item","append","String","set","size","text","JSON","parse","webhookRegistrationResult","result","createdWebhookId","errors","Number","isSafeInteger","access_token","expiresAt","parseExpiresAt","expires_in","scopes","parseScopes","scope","id","name","every","split","trim","filter","Boolean","expiresIn","isFinite","Date","now","statusText","warn","retryAfterSeconds","errorCode","readJiraOAuthErrorCode","data","has","errName","Error","message","parsed","parseInt","isNaN"],"mappings":"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"}
package/dist/config.d.ts CHANGED
@@ -2,7 +2,6 @@ export declare const config: Readonly<{
2
2
  JIRA_OAUTH_CLIENT_ID: string;
3
3
  JIRA_OAUTH_CLIENT_SECRET: string;
4
4
  JIRA_OAUTH_REDIRECT_URL: string;
5
- JIRA_WEBHOOK_SIGNING_SECRET: string;
6
5
  JIRA_WEBHOOK_BASE_URL: string;
7
6
  JIRA_API_BASE_URL: string;
8
7
  JIRA_AUTH_BASE_URL: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;;;kDAwBjB,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;;kDAqBjB,CAAC"}
package/dist/config.js CHANGED
@@ -4,14 +4,11 @@ export const config = createConfig({
4
4
  desc: 'OAuth client ID of the Jira app, used to start and complete the Jira connect flow. Required.'
5
5
  }),
6
6
  JIRA_OAUTH_CLIENT_SECRET: str({
7
- desc: 'OAuth client secret of the Jira app, used to exchange authorization codes for access and refresh tokens. Required.'
7
+ desc: 'OAuth client secret of the Jira app, used for authorization-code exchange and inbound webhook JWT verification. Required.'
8
8
  }),
9
9
  JIRA_OAUTH_REDIRECT_URL: url({
10
10
  desc: 'Public client callback URL Jira redirects to after OAuth authorization, such as https://shipfox.example.com/integrations/jira/callback. Required.'
11
11
  }),
12
- JIRA_WEBHOOK_SIGNING_SECRET: str({
13
- desc: 'Secret used to verify incoming Jira webhooks. It must match the webhook signing secret configured for the Jira app. Required.'
14
- }),
15
12
  JIRA_WEBHOOK_BASE_URL: url({
16
13
  desc: 'Public base URL Jira sends webhooks to. Its origin must be added to the Jira app webhook allowlist. Required.'
17
14
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, str, url} from '@shipfox/config';\n\nexport const config = createConfig({\n JIRA_OAUTH_CLIENT_ID: str({\n desc: 'OAuth client ID of the Jira app, used to start and complete the Jira connect flow. Required.',\n }),\n JIRA_OAUTH_CLIENT_SECRET: str({\n desc: 'OAuth client secret of the Jira app, used to exchange authorization codes for access and refresh tokens. Required.',\n }),\n JIRA_OAUTH_REDIRECT_URL: url({\n desc: 'Public client callback URL Jira redirects to after OAuth authorization, such as https://shipfox.example.com/integrations/jira/callback. Required.',\n }),\n JIRA_WEBHOOK_SIGNING_SECRET: str({\n desc: 'Secret used to verify incoming Jira webhooks. It must match the webhook signing secret configured for the Jira app. Required.',\n }),\n JIRA_WEBHOOK_BASE_URL: url({\n desc: 'Public base URL Jira sends webhooks to. Its origin must be added to the Jira app webhook allowlist. Required.',\n }),\n JIRA_API_BASE_URL: url({\n desc: 'Jira API base URL. Override this only when routing Jira API requests through a proxy or test server.',\n default: 'https://api.atlassian.com',\n }),\n JIRA_AUTH_BASE_URL: url({\n desc: 'Jira OAuth base URL. Override this only when routing OAuth requests through a proxy or test server.',\n default: 'https://auth.atlassian.com',\n }),\n});\n"],"names":["createConfig","str","url","config","JIRA_OAUTH_CLIENT_ID","desc","JIRA_OAUTH_CLIENT_SECRET","JIRA_OAUTH_REDIRECT_URL","JIRA_WEBHOOK_SIGNING_SECRET","JIRA_WEBHOOK_BASE_URL","JIRA_API_BASE_URL","default","JIRA_AUTH_BASE_URL"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAEvD,OAAO,MAAMC,SAASH,aAAa;IACjCI,sBAAsBH,IAAI;QACxBI,MAAM;IACR;IACAC,0BAA0BL,IAAI;QAC5BI,MAAM;IACR;IACAE,yBAAyBL,IAAI;QAC3BG,MAAM;IACR;IACAG,6BAA6BP,IAAI;QAC/BI,MAAM;IACR;IACAI,uBAAuBP,IAAI;QACzBG,MAAM;IACR;IACAK,mBAAmBR,IAAI;QACrBG,MAAM;QACNM,SAAS;IACX;IACAC,oBAAoBV,IAAI;QACtBG,MAAM;QACNM,SAAS;IACX;AACF,GAAG"}
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, str, url} from '@shipfox/config';\n\nexport const config = createConfig({\n JIRA_OAUTH_CLIENT_ID: str({\n desc: 'OAuth client ID of the Jira app, used to start and complete the Jira connect flow. Required.',\n }),\n JIRA_OAUTH_CLIENT_SECRET: str({\n desc: 'OAuth client secret of the Jira app, used for authorization-code exchange and inbound webhook JWT verification. Required.',\n }),\n JIRA_OAUTH_REDIRECT_URL: url({\n desc: 'Public client callback URL Jira redirects to after OAuth authorization, such as https://shipfox.example.com/integrations/jira/callback. Required.',\n }),\n JIRA_WEBHOOK_BASE_URL: url({\n desc: 'Public base URL Jira sends webhooks to. Its origin must be added to the Jira app webhook allowlist. Required.',\n }),\n JIRA_API_BASE_URL: url({\n desc: 'Jira API base URL. Override this only when routing Jira API requests through a proxy or test server.',\n default: 'https://api.atlassian.com',\n }),\n JIRA_AUTH_BASE_URL: url({\n desc: 'Jira OAuth base URL. Override this only when routing OAuth requests through a proxy or test server.',\n default: 'https://auth.atlassian.com',\n }),\n});\n"],"names":["createConfig","str","url","config","JIRA_OAUTH_CLIENT_ID","desc","JIRA_OAUTH_CLIENT_SECRET","JIRA_OAUTH_REDIRECT_URL","JIRA_WEBHOOK_BASE_URL","JIRA_API_BASE_URL","default","JIRA_AUTH_BASE_URL"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAEvD,OAAO,MAAMC,SAASH,aAAa;IACjCI,sBAAsBH,IAAI;QACxBI,MAAM;IACR;IACAC,0BAA0BL,IAAI;QAC5BI,MAAM;IACR;IACAE,yBAAyBL,IAAI;QAC3BG,MAAM;IACR;IACAG,uBAAuBN,IAAI;QACzBG,MAAM;IACR;IACAI,mBAAmBP,IAAI;QACrBG,MAAM;QACNK,SAAS;IACX;IACAC,oBAAoBT,IAAI;QACtBG,MAAM;QACNK,SAAS;IACX;AACF,GAAG"}
@@ -0,0 +1,26 @@
1
+ import type { AgentToolSession, AgentToolsProvider, IntegrationConnection, OpenAgentToolsSessionInput } from '@shipfox/api-integration-spi';
2
+ import type { JiraAgentToolsClient } from '#api/client.js';
3
+ import type { JiraTokenStore } from '#core/tokens.js';
4
+ import { type JiraAgentToolRequiredScope } from './agent-tools.js';
5
+ type JiraIntegrationConnection = IntegrationConnection<'jira'>;
6
+ export type JiraToolCallResult = {
7
+ isError?: boolean | undefined;
8
+ content: readonly {
9
+ type: 'text';
10
+ text: string;
11
+ }[];
12
+ structuredContent?: Record<string, unknown> | undefined;
13
+ };
14
+ export interface JiraAgentToolsProviderOptions {
15
+ jira: Pick<JiraAgentToolsClient, 'request'>;
16
+ tokenStore: Pick<JiraTokenStore, 'getAccessToken'>;
17
+ }
18
+ export declare class JiraAgentToolsProvider implements AgentToolsProvider<JiraIntegrationConnection, JiraAgentToolRequiredScope, unknown, JiraToolCallResult> {
19
+ private readonly options;
20
+ constructor(options: JiraAgentToolsProviderOptions);
21
+ catalog(): readonly [import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry, import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry, import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry, import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry, import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry, import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry, import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry, import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry, import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry, import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry, import("@shipfox/api-integration-jira-dto").JiraAgentToolCatalogEntry];
22
+ selectionCatalog(): import("@shipfox/api-integration-spi").AgentToolSelectionCatalog;
23
+ openSession(input: OpenAgentToolsSessionInput<JiraIntegrationConnection, JiraAgentToolRequiredScope, unknown>): Promise<AgentToolSession<JiraToolCallResult>>;
24
+ }
25
+ export {};
26
+ //# sourceMappingURL=agent-tools-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-tools-provider.d.ts","sourceRoot":"","sources":["../../src/core/agent-tools-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,EACrB,0BAA0B,EAC3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAwB,oBAAoB,EAAC,MAAM,gBAAgB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAEL,KAAK,0BAA0B,EAGhC,MAAM,kBAAkB,CAAC;AAG1B,KAAK,yBAAyB,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;AAE/D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,SAAS;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,EAAE,CAAC;IACjD,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CACzD,CAAC;AAEF,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,CAAC;IAC5C,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAAC;CACpD;AAED,qBAAa,sBACX,YACE,kBAAkB,CAChB,yBAAyB,EACzB,0BAA0B,EAC1B,OAAO,EACP,kBAAkB,CACnB;IAES,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,6BAA6B;IAEnE,OAAO;IAIP,gBAAgB;IAIV,WAAW,CACf,KAAK,EAAE,0BAA0B,CAC/B,yBAAyB,EACzB,0BAA0B,EAC1B,OAAO,CACR,GACA,OAAO,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,CAAC;CAsDjD"}