@shipfox/api-integration-linear 2.0.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 (151) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +89 -0
  5. package/LICENSE +21 -0
  6. package/README.md +88 -0
  7. package/dist/api/client.d.ts +29 -0
  8. package/dist/api/client.d.ts.map +1 -0
  9. package/dist/api/client.js +189 -0
  10. package/dist/api/client.js.map +1 -0
  11. package/dist/config.d.ts +8 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +22 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core/agent-tools-provider.d.ts +32 -0
  16. package/dist/core/agent-tools-provider.d.ts.map +1 -0
  17. package/dist/core/agent-tools-provider.js +63 -0
  18. package/dist/core/agent-tools-provider.js.map +1 -0
  19. package/dist/core/agent-tools.d.ts +10 -0
  20. package/dist/core/agent-tools.d.ts.map +1 -0
  21. package/dist/core/agent-tools.js +920 -0
  22. package/dist/core/agent-tools.js.map +1 -0
  23. package/dist/core/disconnect.d.ts +18 -0
  24. package/dist/core/disconnect.d.ts.map +1 -0
  25. package/dist/core/disconnect.js +23 -0
  26. package/dist/core/disconnect.js.map +1 -0
  27. package/dist/core/errors.d.ts +35 -0
  28. package/dist/core/errors.d.ts.map +1 -0
  29. package/dist/core/errors.js +59 -0
  30. package/dist/core/errors.js.map +1 -0
  31. package/dist/core/install.d.ts +47 -0
  32. package/dist/core/install.d.ts.map +1 -0
  33. package/dist/core/install.js +102 -0
  34. package/dist/core/install.js.map +1 -0
  35. package/dist/core/scopes.d.ts +4 -0
  36. package/dist/core/scopes.d.ts.map +1 -0
  37. package/dist/core/scopes.js +17 -0
  38. package/dist/core/scopes.js.map +1 -0
  39. package/dist/core/state.d.ts +12 -0
  40. package/dist/core/state.d.ts.map +1 -0
  41. package/dist/core/state.js +54 -0
  42. package/dist/core/state.js.map +1 -0
  43. package/dist/core/tokens.d.ts +39 -0
  44. package/dist/core/tokens.d.ts.map +1 -0
  45. package/dist/core/tokens.js +117 -0
  46. package/dist/core/tokens.js.map +1 -0
  47. package/dist/core/webhook.d.ts +16 -0
  48. package/dist/core/webhook.d.ts.map +1 -0
  49. package/dist/core/webhook.js +105 -0
  50. package/dist/core/webhook.js.map +1 -0
  51. package/dist/db/db.d.ts +369 -0
  52. package/dist/db/db.d.ts.map +1 -0
  53. package/dist/db/db.js +18 -0
  54. package/dist/db/db.js.map +1 -0
  55. package/dist/db/installations.d.ts +53 -0
  56. package/dist/db/installations.d.ts.map +1 -0
  57. package/dist/db/installations.js +125 -0
  58. package/dist/db/installations.js.map +1 -0
  59. package/dist/db/migrations.d.ts +2 -0
  60. package/dist/db/migrations.d.ts.map +1 -0
  61. package/dist/db/migrations.js +5 -0
  62. package/dist/db/migrations.js.map +1 -0
  63. package/dist/db/schema/common.d.ts +2 -0
  64. package/dist/db/schema/common.d.ts.map +1 -0
  65. package/dist/db/schema/common.js +4 -0
  66. package/dist/db/schema/common.js.map +1 -0
  67. package/dist/db/schema/installations.d.ts +186 -0
  68. package/dist/db/schema/installations.d.ts.map +1 -0
  69. package/dist/db/schema/installations.js +40 -0
  70. package/dist/db/schema/installations.js.map +1 -0
  71. package/dist/index.d.ts +53 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +56 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/presentation/dto/integrations.d.ts +17 -0
  76. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  77. package/dist/presentation/dto/integrations.js +6 -0
  78. package/dist/presentation/dto/integrations.js.map +1 -0
  79. package/dist/presentation/e2eRoutes/create-connection.d.ts +16 -0
  80. package/dist/presentation/e2eRoutes/create-connection.d.ts.map +1 -0
  81. package/dist/presentation/e2eRoutes/create-connection.js +64 -0
  82. package/dist/presentation/e2eRoutes/create-connection.js.map +1 -0
  83. package/dist/presentation/e2eRoutes/index.d.ts +5 -0
  84. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
  85. package/dist/presentation/e2eRoutes/index.js +11 -0
  86. package/dist/presentation/e2eRoutes/index.js.map +1 -0
  87. package/dist/presentation/routes/errors.d.ts +2 -0
  88. package/dist/presentation/routes/errors.d.ts.map +1 -0
  89. package/dist/presentation/routes/errors.js +65 -0
  90. package/dist/presentation/routes/errors.js.map +1 -0
  91. package/dist/presentation/routes/install.d.ts +19 -0
  92. package/dist/presentation/routes/install.d.ts.map +1 -0
  93. package/dist/presentation/routes/install.js +100 -0
  94. package/dist/presentation/routes/install.js.map +1 -0
  95. package/dist/presentation/routes/webhooks.d.ts +11 -0
  96. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  97. package/dist/presentation/routes/webhooks.js +136 -0
  98. package/dist/presentation/routes/webhooks.js.map +1 -0
  99. package/dist/tsconfig.test.tsbuildinfo +1 -0
  100. package/drizzle/0000_initial.sql +15 -0
  101. package/drizzle/meta/0000_snapshot.json +126 -0
  102. package/drizzle/meta/_journal.json +13 -0
  103. package/drizzle.config.ts +7 -0
  104. package/package.json +72 -0
  105. package/src/api/client.test.ts +366 -0
  106. package/src/api/client.ts +281 -0
  107. package/src/config.test.ts +34 -0
  108. package/src/config.ts +22 -0
  109. package/src/connection-external-url.test.ts +64 -0
  110. package/src/core/agent-tools-provider.test.ts +245 -0
  111. package/src/core/agent-tools-provider.ts +106 -0
  112. package/src/core/agent-tools.ts +918 -0
  113. package/src/core/disconnect.test.ts +45 -0
  114. package/src/core/disconnect.ts +27 -0
  115. package/src/core/errors.ts +71 -0
  116. package/src/core/install.ts +162 -0
  117. package/src/core/scopes.ts +18 -0
  118. package/src/core/state.test.ts +51 -0
  119. package/src/core/state.ts +85 -0
  120. package/src/core/tokens.test.ts +281 -0
  121. package/src/core/tokens.ts +219 -0
  122. package/src/core/webhook.test.ts +318 -0
  123. package/src/core/webhook.ts +146 -0
  124. package/src/db/db.ts +18 -0
  125. package/src/db/installations.test.ts +182 -0
  126. package/src/db/installations.ts +215 -0
  127. package/src/db/migrations.ts +4 -0
  128. package/src/db/schema/common.ts +3 -0
  129. package/src/db/schema/installations.ts +42 -0
  130. package/src/index.test.ts +20 -0
  131. package/src/index.ts +153 -0
  132. package/src/presentation/dto/integrations.ts +9 -0
  133. package/src/presentation/e2eRoutes/create-connection.ts +85 -0
  134. package/src/presentation/e2eRoutes/index.test.ts +193 -0
  135. package/src/presentation/e2eRoutes/index.ts +14 -0
  136. package/src/presentation/routes/errors.ts +60 -0
  137. package/src/presentation/routes/install.test.ts +460 -0
  138. package/src/presentation/routes/install.ts +132 -0
  139. package/src/presentation/routes/webhooks.test.ts +407 -0
  140. package/src/presentation/routes/webhooks.ts +154 -0
  141. package/test/api-secrets.d.ts +28 -0
  142. package/test/env.ts +13 -0
  143. package/test/factories/index.ts +1 -0
  144. package/test/globalSetup.ts +23 -0
  145. package/test/index.ts +1 -0
  146. package/test/setup.ts +17 -0
  147. package/tsconfig.build.json +9 -0
  148. package/tsconfig.build.tsbuildinfo +1 -0
  149. package/tsconfig.json +3 -0
  150. package/tsconfig.test.json +9 -0
  151. package/vitest.config.ts +12 -0
@@ -0,0 +1,2 @@
1
+ $ shipfox-swc
2
+ Successfully compiled: 23 files with swc (45ms)
@@ -0,0 +1 @@
1
+ $ shipfox-tsc-emit
@@ -0,0 +1 @@
1
+ $ shipfox-tsc-check
package/CHANGELOG.md ADDED
@@ -0,0 +1,89 @@
1
+ # @shipfox/api-integration-linear
2
+
3
+ ## 2.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1b0d344: Publishes the complete API runtime closure with packed-consumer-safe internal imports and records its exact package set in application releases.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [c31a7e0]
12
+ - Updated dependencies [1b0d344]
13
+ - Updated dependencies [521e006]
14
+ - @shipfox/api-workspaces@2.0.0
15
+ - @shipfox/api-auth-context@2.0.0
16
+ - @shipfox/api-integration-core-dto@2.0.0
17
+ - @shipfox/api-integration-linear-dto@2.0.0
18
+ - @shipfox/api-secrets@2.0.0
19
+ - @shipfox/config@1.2.1
20
+ - @shipfox/node-drizzle@0.2.1
21
+ - @shipfox/node-fastify@0.2.1
22
+ - @shipfox/node-opentelemetry@0.5.0
23
+ - @shipfox/node-postgres@0.4.1
24
+
25
+ ## 1.0.2
26
+
27
+ ### Patch Changes
28
+
29
+ - @shipfox/api-secrets@0.1.2
30
+ - @shipfox/api-workspaces@0.1.2
31
+
32
+ ## 1.0.1
33
+
34
+ ### Patch Changes
35
+
36
+ - Updated dependencies [ec75cd5]
37
+ - Updated dependencies [6a1fb54]
38
+ - @shipfox/node-drizzle@0.2.0
39
+ - @shipfox/node-postgres@0.4.0
40
+ - @shipfox/api-secrets@0.1.1
41
+ - @shipfox/api-workspaces@0.1.1
42
+
43
+ ## 1.0.0
44
+
45
+ ### Patch Changes
46
+
47
+ - 43d7996: Adds the Linear OAuth connect experience to workspace integration settings.
48
+ - 0948b67: Scaffolds the Linear integration provider, package pair, config, and installation store behind the core provider flag.
49
+ - 8958753: Adds Linear OAuth client and token custody primitives for storing and refreshing workspace connection tokens.
50
+ - 6297b06: Adds the curated Linear agent tool catalog with broad workspace tool metadata for authoring validation and future audit.
51
+ - Updated dependencies [0948b67]
52
+ - Updated dependencies [34ba284]
53
+ - Updated dependencies [3b45d86]
54
+ - Updated dependencies [5707d6d]
55
+ - Updated dependencies [b9c3f32]
56
+ - Updated dependencies [d02c5fd]
57
+ - Updated dependencies [a81b68c]
58
+ - Updated dependencies [115655e]
59
+ - Updated dependencies [c0a883c]
60
+ - Updated dependencies [72ce351]
61
+ - Updated dependencies [e47f8da]
62
+ - Updated dependencies [ce062a9]
63
+ - Updated dependencies [7b175f5]
64
+ - Updated dependencies [f3614ae]
65
+ - Updated dependencies [f92122b]
66
+ - Updated dependencies [f8f339a]
67
+ - Updated dependencies [857fd73]
68
+ - Updated dependencies [f66f606]
69
+ - Updated dependencies [e51d464]
70
+ - Updated dependencies [b8e49ff]
71
+ - Updated dependencies [d6d4862]
72
+ - Updated dependencies [c0a883c]
73
+ - Updated dependencies [6077301]
74
+ - Updated dependencies [82d22e4]
75
+ - Updated dependencies [01be723]
76
+ - Updated dependencies [27770eb]
77
+ - Updated dependencies [2933c33]
78
+ - Updated dependencies [6181819]
79
+ - Updated dependencies [3ddde91]
80
+ - @shipfox/api-integration-linear-dto@0.0.1
81
+ - @shipfox/node-fastify@0.2.0
82
+ - @shipfox/api-secrets@0.1.0
83
+ - @shipfox/node-drizzle@0.1.0
84
+ - @shipfox/api-workspaces@0.1.0
85
+ - @shipfox/api-auth-context@0.1.0
86
+ - @shipfox/api-integration-core-dto@0.1.0
87
+ - @shipfox/node-opentelemetry@0.4.2
88
+ - @shipfox/node-postgres@0.3.2
89
+ - @shipfox/config@1.2.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shipfox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # Shipfox API Integration Linear
2
+
3
+ Shipfox API Integration Linear provides the Linear provider foundation, OAuth
4
+ routes, signed webhook ingestion, hosted MCP adapter, and agent tool catalog. It
5
+ is currently enabled behind `INTEGRATIONS_ENABLE_LINEAR_PROVIDER`.
6
+
7
+ ## OAuth callback
8
+
9
+ Set `LINEAR_OAUTH_REDIRECT_URL` to the public client callback, for example
10
+ `https://shipfox.example.com/integrations/linear/callback`. The client forwards
11
+ the signed callback query to the authenticated API endpoint, while the API uses
12
+ the same configured URL during the server-side token exchange.
13
+
14
+ ## Webhooks
15
+
16
+ Linear sends OAuth application webhooks to:
17
+
18
+ ```text
19
+ POST /webhooks/integrations/linear
20
+ ```
21
+
22
+ The receiver requires `LINEAR_WEBHOOK_SIGNING_SECRET` to match the signing secret
23
+ configured on the Linear app. It verifies `Linear-Signature` against the raw
24
+ request body before parsing JSON, rejects signed payloads whose
25
+ `webhookTimestamp` is more than 60 seconds from the API server clock, and uses
26
+ the `Linear-Delivery` header as the integration delivery id.
27
+
28
+ The OAuth app manifest is the canonical webhook configuration. Configure its
29
+ initial webhook resource types as `Issue`, `Comment`, `IssueLabel`, `Project`,
30
+ `Cycle`, and `AgentSessionEvent`; Linear applies that app-level configuration
31
+ when an organization authorizes the app. Shipfox does not issue a runtime
32
+ `webhookCreate` mutation, and there are no earlier installed organizations to
33
+ migrate. See [Linear's OAuth app manifests](https://linear.app/developers/oauth-app-manifests).
34
+
35
+ Supported data webhook events are `Issue`, `Comment`, `IssueLabel`, `Project`,
36
+ and `Cycle` with `create`, `update`, or `remove` actions. They publish
37
+ `integrations.event.received` with `provider: "linear"`, `source:
38
+ connection.slug`, and event names such as `Issue.create`.
39
+
40
+ `AgentSessionEvent.created` publishes `agentSession.created` for both an
41
+ assignment and a comment mention. The raw payload is preserved, including
42
+ `agentSession.commentId` and `agentSession.sourceCommentId`, so workflow filters
43
+ can distinguish the context. Authors can get the Linear app user id from an
44
+ observed event or the Linear app identity, then compare it with `appUserId` in a
45
+ filter literal. `AgentSessionEvent.prompted` publishes `agentSession.prompted`;
46
+ it can start a subscribed workflow or reach an explicitly configured job listener,
47
+ but never automatically resumes a run from its AgentSession. The module does not
48
+ acknowledge sessions or emit Linear agent activities. See [Linear's agent-session
49
+ behavior](https://linear.app/developers/agent-interaction).
50
+
51
+ Signed but unsupported webhook shapes are recorded and dropped with a 200
52
+ response so Linear does not retry or disable the webhook endpoint.
53
+ ## Agent Tool Catalog
54
+
55
+ The v1 Linear agent tool catalog is a broad workspace catalog over Linear's
56
+ hosted MCP tool ids. It includes read tools for issues, comments, projects,
57
+ documents, teams, users, cycles, releases, release notes, release pipelines,
58
+ agent skills, diffs, documentation search, and attachments. It also includes
59
+ write tools for issue and comment workflows plus project, document, milestone,
60
+ release, release note, label, status update, comment, and attachment mutation.
61
+
62
+ Tool ids intentionally match Linear's hosted MCP server exactly. The catalog is
63
+ the local source of truth for authoring validation, write-safety metadata, and
64
+ future audit behavior; the provider advertises `agent_tools` and proxies calls
65
+ to Linear's hosted MCP server with the connection's stored Linear token.
66
+
67
+ Sensitivity policy:
68
+
69
+ - `save_issue` is explicitly `sensitive: false`. It is still a write tool and
70
+ requires `allow_write`, but the v1 product choice treats normal issue triage as
71
+ an ordinary write.
72
+ - `save_comment` is `sensitive: false`.
73
+ - Destructive or workspace-shaping writes are `sensitive: true`, including
74
+ upload and delete tools, status update mutation, project/document/milestone
75
+ saves, release/release-note saves, and label creation.
76
+ - Attachment upload tools are included with their native hosted-MCP names:
77
+ `prepare_attachment_upload`, `create_attachment_from_upload`, and the
78
+ deprecated tiny-file fallback `create_attachment`.
79
+
80
+ ## Development
81
+
82
+ Run checks for this package:
83
+
84
+ ```sh
85
+ turbo check --filter=@shipfox/api-integration-linear
86
+ turbo type --filter=@shipfox/api-integration-linear
87
+ turbo test --filter=@shipfox/api-integration-linear
88
+ ```
@@ -0,0 +1,29 @@
1
+ export interface LinearAuthorization {
2
+ accessToken: string;
3
+ refreshToken?: string | undefined;
4
+ expiresAt?: Date | undefined;
5
+ scopes: string[];
6
+ }
7
+ export interface LinearIdentity {
8
+ appUserId: string;
9
+ organizationId: string;
10
+ organizationName: string;
11
+ organizationUrlKey: string;
12
+ }
13
+ export interface LinearApiClient {
14
+ exchangeAuthorizationCode(input: {
15
+ code: string;
16
+ }): Promise<LinearAuthorization>;
17
+ refreshAccessToken(input: {
18
+ refreshToken: string;
19
+ }): Promise<LinearAuthorization>;
20
+ revokeToken(input: {
21
+ token: string;
22
+ tokenTypeHint: 'access_token' | 'refresh_token';
23
+ }): Promise<void>;
24
+ getIdentity(input: {
25
+ accessToken: string;
26
+ }): Promise<LinearIdentity>;
27
+ }
28
+ export declare function createLinearApiClient(): LinearApiClient;
29
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,mBAAmB;IAClC,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,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,yBAAyB,CAAC,KAAK,EAAE;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC/E,kBAAkB,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAChF,WAAW,CAAC,KAAK,EAAE;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,cAAc,GAAG,eAAe,CAAC;KACjD,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,WAAW,CAAC,KAAK,EAAE;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACpE;AA2BD,wBAAgB,qBAAqB,IAAI,eAAe,CAuFvD"}
@@ -0,0 +1,189 @@
1
+ import { logger } from '@shipfox/node-opentelemetry';
2
+ import ky, { HTTPError, TimeoutError } from 'ky';
3
+ import { config } from '#config.js';
4
+ import { LinearIntegrationProviderError } from '#core/errors.js';
5
+ const LINEAR_OAUTH_TOKEN_URL = 'https://api.linear.app/oauth/token';
6
+ const LINEAR_OAUTH_REVOKE_URL = 'https://api.linear.app/oauth/revoke';
7
+ const LINEAR_GRAPHQL_URL = 'https://api.linear.app/graphql';
8
+ const LINEAR_API_TIMEOUT_MS = 10_000;
9
+ const SCOPE_SEPARATOR_RE = /[,\s]+/;
10
+ const IDENTITY_QUERY = `
11
+ query ShipfoxLinearIdentity {
12
+ viewer {
13
+ id
14
+ }
15
+ organization {
16
+ id
17
+ name
18
+ urlKey
19
+ }
20
+ }
21
+ `;
22
+ export function createLinearApiClient() {
23
+ return {
24
+ async exchangeAuthorizationCode (input) {
25
+ const body = await mapLinearError('exchange-authorization-code', ()=>ky.post(LINEAR_OAUTH_TOKEN_URL, {
26
+ body: new URLSearchParams({
27
+ grant_type: 'authorization_code',
28
+ client_id: config.LINEAR_OAUTH_CLIENT_ID,
29
+ client_secret: config.LINEAR_OAUTH_CLIENT_SECRET,
30
+ code: input.code,
31
+ redirect_uri: config.LINEAR_OAUTH_REDIRECT_URL
32
+ }),
33
+ timeout: LINEAR_API_TIMEOUT_MS
34
+ }).json());
35
+ return parseTokenResponse(body);
36
+ },
37
+ async refreshAccessToken (input) {
38
+ const body = await mapLinearError('refresh-access-token', ()=>ky.post(LINEAR_OAUTH_TOKEN_URL, {
39
+ body: new URLSearchParams({
40
+ grant_type: 'refresh_token',
41
+ client_id: config.LINEAR_OAUTH_CLIENT_ID,
42
+ client_secret: config.LINEAR_OAUTH_CLIENT_SECRET,
43
+ refresh_token: input.refreshToken
44
+ }),
45
+ timeout: LINEAR_API_TIMEOUT_MS
46
+ }).json());
47
+ return parseTokenResponse(body);
48
+ },
49
+ async revokeToken (input) {
50
+ await mapLinearError('revoke-token', async ()=>{
51
+ await ky.post(LINEAR_OAUTH_REVOKE_URL, {
52
+ body: new URLSearchParams({
53
+ client_id: config.LINEAR_OAUTH_CLIENT_ID,
54
+ client_secret: config.LINEAR_OAUTH_CLIENT_SECRET,
55
+ token: input.token,
56
+ token_type_hint: input.tokenTypeHint
57
+ }),
58
+ timeout: LINEAR_API_TIMEOUT_MS
59
+ });
60
+ });
61
+ },
62
+ async getIdentity (input) {
63
+ const body = await mapLinearError('get-identity', ()=>ky.post(LINEAR_GRAPHQL_URL, {
64
+ headers: {
65
+ authorization: `Bearer ${input.accessToken}`
66
+ },
67
+ json: {
68
+ query: IDENTITY_QUERY
69
+ },
70
+ timeout: LINEAR_API_TIMEOUT_MS
71
+ }).json(), {
72
+ classifyHttp4xx: classifyGraphqlHttp4xx
73
+ });
74
+ const data = graphqlData('get-identity', body);
75
+ const appUserId = data.viewer?.id;
76
+ const organizationId = data.organization?.id;
77
+ const organizationName = data.organization?.name;
78
+ const organizationUrlKey = data.organization?.urlKey;
79
+ if (typeof appUserId !== 'string' || typeof organizationId !== 'string' || typeof organizationName !== 'string' || typeof organizationUrlKey !== 'string') {
80
+ throw new LinearIntegrationProviderError('malformed-provider-response', 'Linear identity response did not include the app user and organization');
81
+ }
82
+ return {
83
+ appUserId,
84
+ organizationId,
85
+ organizationName,
86
+ organizationUrlKey
87
+ };
88
+ }
89
+ };
90
+ }
91
+ function parseTokenResponse(body) {
92
+ if (typeof body.access_token !== 'string' || body.access_token.length === 0) {
93
+ throw new LinearIntegrationProviderError('malformed-provider-response', 'Linear authorization response did not include an access token');
94
+ }
95
+ const scopes = parseScopes(body.scope);
96
+ const expiresAt = parseExpiresAt(body.expires_in);
97
+ const refreshToken = typeof body.refresh_token === 'string' ? body.refresh_token : undefined;
98
+ return {
99
+ accessToken: body.access_token,
100
+ refreshToken,
101
+ expiresAt,
102
+ scopes
103
+ };
104
+ }
105
+ function parseScopes(scope) {
106
+ if (typeof scope === 'string') {
107
+ return scope.split(SCOPE_SEPARATOR_RE).map((value)=>value.trim()).filter(Boolean);
108
+ }
109
+ if (Array.isArray(scope) && scope.every((value)=>typeof value === 'string')) return scope;
110
+ if (scope === undefined) return [];
111
+ throw new LinearIntegrationProviderError('malformed-provider-response', 'Linear authorization response included malformed scopes');
112
+ }
113
+ function parseExpiresAt(expiresIn) {
114
+ if (expiresIn === undefined) return undefined;
115
+ if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {
116
+ throw new LinearIntegrationProviderError('malformed-provider-response', 'Linear authorization response included a malformed expiry');
117
+ }
118
+ return new Date(Date.now() + expiresIn * 1000);
119
+ }
120
+ function graphqlData(operation, body) {
121
+ if (hasGraphqlErrors(body)) {
122
+ const reason = hasAuthGraphqlError(body.errors) ? 'access-denied' : 'malformed-provider-response';
123
+ logger().warn({
124
+ operation
125
+ }, 'Linear GraphQL request returned errors');
126
+ throw new LinearIntegrationProviderError(reason, 'Linear GraphQL request failed');
127
+ }
128
+ if (!body.data) {
129
+ throw new LinearIntegrationProviderError('malformed-provider-response', 'Linear GraphQL response did not include data');
130
+ }
131
+ return body.data;
132
+ }
133
+ function hasGraphqlErrors(body) {
134
+ return Array.isArray(body.errors) && body.errors.length > 0;
135
+ }
136
+ function hasAuthGraphqlError(errors) {
137
+ return errors?.some((error)=>{
138
+ const type = error.extensions?.type;
139
+ return typeof type === 'string' && [
140
+ 'authentication',
141
+ 'authorization'
142
+ ].includes(type.toLowerCase());
143
+ }) ?? false;
144
+ }
145
+ async function mapLinearError(operation, request, options = {}) {
146
+ try {
147
+ return await request();
148
+ } catch (error) {
149
+ if (error instanceof LinearIntegrationProviderError) throw error;
150
+ if (error instanceof HTTPError) {
151
+ const { status, statusText, headers } = error.response;
152
+ logger().warn({
153
+ operation,
154
+ status,
155
+ statusText
156
+ }, 'Linear API request rejected');
157
+ if (status === 429) {
158
+ throw new LinearIntegrationProviderError('rate-limited', 'Linear request was rate limited', retryAfterSeconds(headers));
159
+ }
160
+ if (status >= 500) {
161
+ throw new LinearIntegrationProviderError('provider-unavailable', 'Linear request failed');
162
+ }
163
+ const reason = options.classifyHttp4xx?.(status) ?? 'access-denied';
164
+ throw new LinearIntegrationProviderError(reason, 'Linear request was rejected');
165
+ }
166
+ if (error instanceof TimeoutError) {
167
+ logger().warn({
168
+ operation
169
+ }, 'Linear API request timed out');
170
+ throw new LinearIntegrationProviderError('timeout', 'Linear request timed out');
171
+ }
172
+ logger().warn({
173
+ operation,
174
+ errName: error instanceof Error ? error.name : typeof error
175
+ }, 'Linear API request failed');
176
+ throw new LinearIntegrationProviderError('provider-unavailable', 'Linear request failed');
177
+ }
178
+ }
179
+ function classifyGraphqlHttp4xx(status) {
180
+ return status === 401 || status === 403 ? 'access-denied' : 'malformed-provider-response';
181
+ }
182
+ function retryAfterSeconds(headers) {
183
+ const retryAfter = headers.get('retry-after');
184
+ if (!retryAfter) return undefined;
185
+ const parsed = Number.parseInt(retryAfter, 10);
186
+ return Number.isNaN(parsed) ? undefined : parsed;
187
+ }
188
+
189
+ //# sourceMappingURL=client.js.map
@@ -0,0 +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 {LinearIntegrationProviderError} from '#core/errors.js';\n\nconst LINEAR_OAUTH_TOKEN_URL = 'https://api.linear.app/oauth/token';\nconst LINEAR_OAUTH_REVOKE_URL = 'https://api.linear.app/oauth/revoke';\nconst LINEAR_GRAPHQL_URL = 'https://api.linear.app/graphql';\nconst LINEAR_API_TIMEOUT_MS = 10_000;\nconst SCOPE_SEPARATOR_RE = /[,\\s]+/;\n\nconst IDENTITY_QUERY = `\n query ShipfoxLinearIdentity {\n viewer {\n id\n }\n organization {\n id\n name\n urlKey\n }\n }\n`;\n\nexport interface LinearAuthorization {\n accessToken: string;\n refreshToken?: string | undefined;\n expiresAt?: Date | undefined;\n scopes: string[];\n}\n\nexport interface LinearIdentity {\n appUserId: string;\n organizationId: string;\n organizationName: string;\n organizationUrlKey: string;\n}\n\nexport interface LinearApiClient {\n exchangeAuthorizationCode(input: {code: string}): Promise<LinearAuthorization>;\n refreshAccessToken(input: {refreshToken: string}): Promise<LinearAuthorization>;\n revokeToken(input: {\n token: string;\n tokenTypeHint: 'access_token' | 'refresh_token';\n }): Promise<void>;\n getIdentity(input: {accessToken: string}): Promise<LinearIdentity>;\n}\n\ninterface LinearTokenResponse {\n access_token?: unknown;\n refresh_token?: unknown;\n expires_in?: unknown;\n scope?: unknown;\n}\n\ninterface LinearGraphqlResponse<Data> {\n data?: Data | null;\n errors?: LinearGraphqlError[] | undefined;\n}\n\ninterface LinearGraphqlError {\n extensions?: {type?: unknown} | undefined;\n}\n\ninterface LinearIdentityData {\n viewer?: {id?: unknown} | null;\n organization?: {id?: unknown; name?: unknown; urlKey?: unknown} | null;\n}\n\ninterface MapLinearErrorOptions {\n classifyHttp4xx?(status: number): 'access-denied' | 'malformed-provider-response';\n}\n\nexport function createLinearApiClient(): LinearApiClient {\n return {\n async exchangeAuthorizationCode(input) {\n const body = await mapLinearError('exchange-authorization-code', () =>\n ky\n .post(LINEAR_OAUTH_TOKEN_URL, {\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: config.LINEAR_OAUTH_CLIENT_ID,\n client_secret: config.LINEAR_OAUTH_CLIENT_SECRET,\n code: input.code,\n redirect_uri: config.LINEAR_OAUTH_REDIRECT_URL,\n }),\n timeout: LINEAR_API_TIMEOUT_MS,\n })\n .json<LinearTokenResponse>(),\n );\n\n return parseTokenResponse(body);\n },\n\n async refreshAccessToken(input) {\n const body = await mapLinearError('refresh-access-token', () =>\n ky\n .post(LINEAR_OAUTH_TOKEN_URL, {\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: config.LINEAR_OAUTH_CLIENT_ID,\n client_secret: config.LINEAR_OAUTH_CLIENT_SECRET,\n refresh_token: input.refreshToken,\n }),\n timeout: LINEAR_API_TIMEOUT_MS,\n })\n .json<LinearTokenResponse>(),\n );\n\n return parseTokenResponse(body);\n },\n\n async revokeToken(input) {\n await mapLinearError('revoke-token', async () => {\n await ky.post(LINEAR_OAUTH_REVOKE_URL, {\n body: new URLSearchParams({\n client_id: config.LINEAR_OAUTH_CLIENT_ID,\n client_secret: config.LINEAR_OAUTH_CLIENT_SECRET,\n token: input.token,\n token_type_hint: input.tokenTypeHint,\n }),\n timeout: LINEAR_API_TIMEOUT_MS,\n });\n });\n },\n\n async getIdentity(input) {\n const body = await mapLinearError(\n 'get-identity',\n () =>\n ky\n .post(LINEAR_GRAPHQL_URL, {\n headers: {authorization: `Bearer ${input.accessToken}`},\n json: {query: IDENTITY_QUERY},\n timeout: LINEAR_API_TIMEOUT_MS,\n })\n .json<LinearGraphqlResponse<LinearIdentityData>>(),\n {classifyHttp4xx: classifyGraphqlHttp4xx},\n );\n const data = graphqlData('get-identity', body);\n const appUserId = data.viewer?.id;\n const organizationId = data.organization?.id;\n const organizationName = data.organization?.name;\n const organizationUrlKey = data.organization?.urlKey;\n\n if (\n typeof appUserId !== 'string' ||\n typeof organizationId !== 'string' ||\n typeof organizationName !== 'string' ||\n typeof organizationUrlKey !== 'string'\n ) {\n throw new LinearIntegrationProviderError(\n 'malformed-provider-response',\n 'Linear identity response did not include the app user and organization',\n );\n }\n\n return {appUserId, organizationId, organizationName, organizationUrlKey};\n },\n };\n}\n\nfunction parseTokenResponse(body: LinearTokenResponse): LinearAuthorization {\n if (typeof body.access_token !== 'string' || body.access_token.length === 0) {\n throw new LinearIntegrationProviderError(\n 'malformed-provider-response',\n 'Linear authorization response did not include an access token',\n );\n }\n\n const scopes = parseScopes(body.scope);\n const expiresAt = parseExpiresAt(body.expires_in);\n const refreshToken = typeof body.refresh_token === 'string' ? body.refresh_token : undefined;\n return {accessToken: body.access_token, refreshToken, expiresAt, 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 }\n if (Array.isArray(scope) && scope.every((value) => typeof value === 'string')) return scope;\n if (scope === undefined) return [];\n throw new LinearIntegrationProviderError(\n 'malformed-provider-response',\n 'Linear authorization response included malformed scopes',\n );\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 new LinearIntegrationProviderError(\n 'malformed-provider-response',\n 'Linear authorization response included a malformed expiry',\n );\n }\n return new Date(Date.now() + expiresIn * 1000);\n}\n\nfunction graphqlData<Data>(operation: string, body: LinearGraphqlResponse<Data>): Data {\n if (hasGraphqlErrors(body)) {\n const reason = hasAuthGraphqlError(body.errors)\n ? 'access-denied'\n : 'malformed-provider-response';\n logger().warn({operation}, 'Linear GraphQL request returned errors');\n throw new LinearIntegrationProviderError(reason, 'Linear GraphQL request failed');\n }\n if (!body.data) {\n throw new LinearIntegrationProviderError(\n 'malformed-provider-response',\n 'Linear GraphQL response did not include data',\n );\n }\n return body.data;\n}\n\nfunction hasGraphqlErrors(body: LinearGraphqlResponse<unknown>): boolean {\n return Array.isArray(body.errors) && body.errors.length > 0;\n}\n\nfunction hasAuthGraphqlError(errors: LinearGraphqlError[] | undefined): boolean {\n return (\n errors?.some((error) => {\n const type = error.extensions?.type;\n return (\n typeof type === 'string' && ['authentication', 'authorization'].includes(type.toLowerCase())\n );\n }) ?? false\n );\n}\n\nasync function mapLinearError<T>(\n operation: string,\n request: () => Promise<T>,\n options: MapLinearErrorOptions = {},\n): Promise<T> {\n try {\n return await request();\n } catch (error) {\n if (error instanceof LinearIntegrationProviderError) throw error;\n if (error instanceof HTTPError) {\n const {status, statusText, headers} = error.response;\n logger().warn({operation, status, statusText}, 'Linear API request rejected');\n if (status === 429) {\n throw new LinearIntegrationProviderError(\n 'rate-limited',\n 'Linear request was rate limited',\n retryAfterSeconds(headers),\n );\n }\n if (status >= 500) {\n throw new LinearIntegrationProviderError('provider-unavailable', 'Linear request failed');\n }\n const reason = options.classifyHttp4xx?.(status) ?? 'access-denied';\n throw new LinearIntegrationProviderError(reason, 'Linear request was rejected');\n }\n if (error instanceof TimeoutError) {\n logger().warn({operation}, 'Linear API request timed out');\n throw new LinearIntegrationProviderError('timeout', 'Linear request timed out');\n }\n logger().warn(\n {operation, errName: error instanceof Error ? error.name : typeof error},\n 'Linear API request failed',\n );\n throw new LinearIntegrationProviderError('provider-unavailable', 'Linear request failed');\n }\n}\n\nfunction classifyGraphqlHttp4xx(status: number): 'access-denied' | 'malformed-provider-response' {\n return status === 401 || status === 403 ? 'access-denied' : 'malformed-provider-response';\n}\n\nfunction retryAfterSeconds(headers: Headers): number | undefined {\n const retryAfter = headers.get('retry-after');\n if (!retryAfter) return undefined;\n const parsed = Number.parseInt(retryAfter, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n"],"names":["logger","ky","HTTPError","TimeoutError","config","LinearIntegrationProviderError","LINEAR_OAUTH_TOKEN_URL","LINEAR_OAUTH_REVOKE_URL","LINEAR_GRAPHQL_URL","LINEAR_API_TIMEOUT_MS","SCOPE_SEPARATOR_RE","IDENTITY_QUERY","createLinearApiClient","exchangeAuthorizationCode","input","body","mapLinearError","post","URLSearchParams","grant_type","client_id","LINEAR_OAUTH_CLIENT_ID","client_secret","LINEAR_OAUTH_CLIENT_SECRET","code","redirect_uri","LINEAR_OAUTH_REDIRECT_URL","timeout","json","parseTokenResponse","refreshAccessToken","refresh_token","refreshToken","revokeToken","token","token_type_hint","tokenTypeHint","getIdentity","headers","authorization","accessToken","query","classifyHttp4xx","classifyGraphqlHttp4xx","data","graphqlData","appUserId","viewer","id","organizationId","organization","organizationName","name","organizationUrlKey","urlKey","access_token","length","scopes","parseScopes","scope","expiresAt","parseExpiresAt","expires_in","undefined","split","map","value","trim","filter","Boolean","Array","isArray","every","expiresIn","Number","isFinite","Date","now","operation","hasGraphqlErrors","reason","hasAuthGraphqlError","errors","warn","some","error","type","extensions","includes","toLowerCase","request","options","status","statusText","response","retryAfterSeconds","errName","Error","retryAfter","get","parsed","parseInt","isNaN"],"mappings":"AAAA,SAAQA,MAAM,QAAO,8BAA8B;AACnD,OAAOC,MAAKC,SAAS,EAAEC,YAAY,QAAO,KAAK;AAC/C,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,8BAA8B,QAAO,kBAAkB;AAE/D,MAAMC,yBAAyB;AAC/B,MAAMC,0BAA0B;AAChC,MAAMC,qBAAqB;AAC3B,MAAMC,wBAAwB;AAC9B,MAAMC,qBAAqB;AAE3B,MAAMC,iBAAiB,CAAC;;;;;;;;;;;AAWxB,CAAC;AAmDD,OAAO,SAASC;IACd,OAAO;QACL,MAAMC,2BAA0BC,KAAK;YACnC,MAAMC,OAAO,MAAMC,eAAe,+BAA+B,IAC/Df,GACGgB,IAAI,CAACX,wBAAwB;oBAC5BS,MAAM,IAAIG,gBAAgB;wBACxBC,YAAY;wBACZC,WAAWhB,OAAOiB,sBAAsB;wBACxCC,eAAelB,OAAOmB,0BAA0B;wBAChDC,MAAMV,MAAMU,IAAI;wBAChBC,cAAcrB,OAAOsB,yBAAyB;oBAChD;oBACAC,SAASlB;gBACX,GACCmB,IAAI;YAGT,OAAOC,mBAAmBd;QAC5B;QAEA,MAAMe,oBAAmBhB,KAAK;YAC5B,MAAMC,OAAO,MAAMC,eAAe,wBAAwB,IACxDf,GACGgB,IAAI,CAACX,wBAAwB;oBAC5BS,MAAM,IAAIG,gBAAgB;wBACxBC,YAAY;wBACZC,WAAWhB,OAAOiB,sBAAsB;wBACxCC,eAAelB,OAAOmB,0BAA0B;wBAChDQ,eAAejB,MAAMkB,YAAY;oBACnC;oBACAL,SAASlB;gBACX,GACCmB,IAAI;YAGT,OAAOC,mBAAmBd;QAC5B;QAEA,MAAMkB,aAAYnB,KAAK;YACrB,MAAME,eAAe,gBAAgB;gBACnC,MAAMf,GAAGgB,IAAI,CAACV,yBAAyB;oBACrCQ,MAAM,IAAIG,gBAAgB;wBACxBE,WAAWhB,OAAOiB,sBAAsB;wBACxCC,eAAelB,OAAOmB,0BAA0B;wBAChDW,OAAOpB,MAAMoB,KAAK;wBAClBC,iBAAiBrB,MAAMsB,aAAa;oBACtC;oBACAT,SAASlB;gBACX;YACF;QACF;QAEA,MAAM4B,aAAYvB,KAAK;YACrB,MAAMC,OAAO,MAAMC,eACjB,gBACA,IACEf,GACGgB,IAAI,CAACT,oBAAoB;oBACxB8B,SAAS;wBAACC,eAAe,CAAC,OAAO,EAAEzB,MAAM0B,WAAW,EAAE;oBAAA;oBACtDZ,MAAM;wBAACa,OAAO9B;oBAAc;oBAC5BgB,SAASlB;gBACX,GACCmB,IAAI,IACT;gBAACc,iBAAiBC;YAAsB;YAE1C,MAAMC,OAAOC,YAAY,gBAAgB9B;YACzC,MAAM+B,YAAYF,KAAKG,MAAM,EAAEC;YAC/B,MAAMC,iBAAiBL,KAAKM,YAAY,EAAEF;YAC1C,MAAMG,mBAAmBP,KAAKM,YAAY,EAAEE;YAC5C,MAAMC,qBAAqBT,KAAKM,YAAY,EAAEI;YAE9C,IACE,OAAOR,cAAc,YACrB,OAAOG,mBAAmB,YAC1B,OAAOE,qBAAqB,YAC5B,OAAOE,uBAAuB,UAC9B;gBACA,MAAM,IAAIhD,+BACR,+BACA;YAEJ;YAEA,OAAO;gBAACyC;gBAAWG;gBAAgBE;gBAAkBE;YAAkB;QACzE;IACF;AACF;AAEA,SAASxB,mBAAmBd,IAAyB;IACnD,IAAI,OAAOA,KAAKwC,YAAY,KAAK,YAAYxC,KAAKwC,YAAY,CAACC,MAAM,KAAK,GAAG;QAC3E,MAAM,IAAInD,+BACR,+BACA;IAEJ;IAEA,MAAMoD,SAASC,YAAY3C,KAAK4C,KAAK;IACrC,MAAMC,YAAYC,eAAe9C,KAAK+C,UAAU;IAChD,MAAM9B,eAAe,OAAOjB,KAAKgB,aAAa,KAAK,WAAWhB,KAAKgB,aAAa,GAAGgC;IACnF,OAAO;QAACvB,aAAazB,KAAKwC,YAAY;QAAEvB;QAAc4B;QAAWH;IAAM;AACzE;AAEA,SAASC,YAAYC,KAAc;IACjC,IAAI,OAAOA,UAAU,UAAU;QAC7B,OAAOA,MACJK,KAAK,CAACtD,oBACNuD,GAAG,CAAC,CAACC,QAAUA,MAAMC,IAAI,IACzBC,MAAM,CAACC;IACZ;IACA,IAAIC,MAAMC,OAAO,CAACZ,UAAUA,MAAMa,KAAK,CAAC,CAACN,QAAU,OAAOA,UAAU,WAAW,OAAOP;IACtF,IAAIA,UAAUI,WAAW,OAAO,EAAE;IAClC,MAAM,IAAI1D,+BACR,+BACA;AAEJ;AAEA,SAASwD,eAAeY,SAAkB;IACxC,IAAIA,cAAcV,WAAW,OAAOA;IACpC,IAAI,OAAOU,cAAc,YAAY,CAACC,OAAOC,QAAQ,CAACF,cAAcA,aAAa,GAAG;QAClF,MAAM,IAAIpE,+BACR,+BACA;IAEJ;IACA,OAAO,IAAIuE,KAAKA,KAAKC,GAAG,KAAKJ,YAAY;AAC3C;AAEA,SAAS5B,YAAkBiC,SAAiB,EAAE/D,IAAiC;IAC7E,IAAIgE,iBAAiBhE,OAAO;QAC1B,MAAMiE,SAASC,oBAAoBlE,KAAKmE,MAAM,IAC1C,kBACA;QACJlF,SAASmF,IAAI,CAAC;YAACL;QAAS,GAAG;QAC3B,MAAM,IAAIzE,+BAA+B2E,QAAQ;IACnD;IACA,IAAI,CAACjE,KAAK6B,IAAI,EAAE;QACd,MAAM,IAAIvC,+BACR,+BACA;IAEJ;IACA,OAAOU,KAAK6B,IAAI;AAClB;AAEA,SAASmC,iBAAiBhE,IAAoC;IAC5D,OAAOuD,MAAMC,OAAO,CAACxD,KAAKmE,MAAM,KAAKnE,KAAKmE,MAAM,CAAC1B,MAAM,GAAG;AAC5D;AAEA,SAASyB,oBAAoBC,MAAwC;IACnE,OACEA,QAAQE,KAAK,CAACC;QACZ,MAAMC,OAAOD,MAAME,UAAU,EAAED;QAC/B,OACE,OAAOA,SAAS,YAAY;YAAC;YAAkB;SAAgB,CAACE,QAAQ,CAACF,KAAKG,WAAW;IAE7F,MAAM;AAEV;AAEA,eAAezE,eACb8D,SAAiB,EACjBY,OAAyB,EACzBC,UAAiC,CAAC,CAAC;IAEnC,IAAI;QACF,OAAO,MAAMD;IACf,EAAE,OAAOL,OAAO;QACd,IAAIA,iBAAiBhF,gCAAgC,MAAMgF;QAC3D,IAAIA,iBAAiBnF,WAAW;YAC9B,MAAM,EAAC0F,MAAM,EAAEC,UAAU,EAAEvD,OAAO,EAAC,GAAG+C,MAAMS,QAAQ;YACpD9F,SAASmF,IAAI,CAAC;gBAACL;gBAAWc;gBAAQC;YAAU,GAAG;YAC/C,IAAID,WAAW,KAAK;gBAClB,MAAM,IAAIvF,+BACR,gBACA,mCACA0F,kBAAkBzD;YAEtB;YACA,IAAIsD,UAAU,KAAK;gBACjB,MAAM,IAAIvF,+BAA+B,wBAAwB;YACnE;YACA,MAAM2E,SAASW,QAAQjD,eAAe,GAAGkD,WAAW;YACpD,MAAM,IAAIvF,+BAA+B2E,QAAQ;QACnD;QACA,IAAIK,iBAAiBlF,cAAc;YACjCH,SAASmF,IAAI,CAAC;gBAACL;YAAS,GAAG;YAC3B,MAAM,IAAIzE,+BAA+B,WAAW;QACtD;QACAL,SAASmF,IAAI,CACX;YAACL;YAAWkB,SAASX,iBAAiBY,QAAQZ,MAAMjC,IAAI,GAAG,OAAOiC;QAAK,GACvE;QAEF,MAAM,IAAIhF,+BAA+B,wBAAwB;IACnE;AACF;AAEA,SAASsC,uBAAuBiD,MAAc;IAC5C,OAAOA,WAAW,OAAOA,WAAW,MAAM,kBAAkB;AAC9D;AAEA,SAASG,kBAAkBzD,OAAgB;IACzC,MAAM4D,aAAa5D,QAAQ6D,GAAG,CAAC;IAC/B,IAAI,CAACD,YAAY,OAAOnC;IACxB,MAAMqC,SAAS1B,OAAO2B,QAAQ,CAACH,YAAY;IAC3C,OAAOxB,OAAO4B,KAAK,CAACF,UAAUrC,YAAYqC;AAC5C"}
@@ -0,0 +1,8 @@
1
+ export declare const config: Readonly<{
2
+ LINEAR_OAUTH_CLIENT_ID: string;
3
+ LINEAR_OAUTH_CLIENT_SECRET: string;
4
+ LINEAR_WEBHOOK_SIGNING_SECRET: string;
5
+ LINEAR_OAUTH_REDIRECT_URL: string;
6
+ LINEAR_MCP_ENDPOINT: string;
7
+ } & import("@shipfox/config").CleanedEnvAccessors>;
8
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,MAAM;;;;;;kDAAmC,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,22 @@
1
+ import { createConfig, str, url } from '@shipfox/config';
2
+ const linearConfigSchema = {
3
+ LINEAR_OAUTH_CLIENT_ID: str({
4
+ desc: 'OAuth client ID of the Linear app, used to start and complete the Linear connect flow. Required.'
5
+ }),
6
+ LINEAR_OAUTH_CLIENT_SECRET: str({
7
+ desc: 'OAuth client secret of the Linear app, used to exchange authorization codes for workspace tokens. Required.'
8
+ }),
9
+ LINEAR_WEBHOOK_SIGNING_SECRET: str({
10
+ desc: 'Secret used to verify incoming Linear webhooks. Must match the signing secret configured on the Linear app. Required.'
11
+ }),
12
+ LINEAR_OAUTH_REDIRECT_URL: url({
13
+ desc: 'Public client callback URL Linear redirects to after OAuth authorization, such as https://shipfox.example.com/integrations/linear/callback. Required.'
14
+ }),
15
+ LINEAR_MCP_ENDPOINT: url({
16
+ desc: 'Streamable HTTP endpoint used for Linear MCP tool calls. Set this only when routing Linear tools through a compatible proxy or test server.',
17
+ default: 'https://mcp.linear.app/mcp'
18
+ })
19
+ };
20
+ export const config = createConfig(linearConfigSchema);
21
+
22
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, str, url} from '@shipfox/config';\n\nconst linearConfigSchema = {\n LINEAR_OAUTH_CLIENT_ID: str({\n desc: 'OAuth client ID of the Linear app, used to start and complete the Linear connect flow. Required.',\n }),\n LINEAR_OAUTH_CLIENT_SECRET: str({\n desc: 'OAuth client secret of the Linear app, used to exchange authorization codes for workspace tokens. Required.',\n }),\n LINEAR_WEBHOOK_SIGNING_SECRET: str({\n desc: 'Secret used to verify incoming Linear webhooks. Must match the signing secret configured on the Linear app. Required.',\n }),\n LINEAR_OAUTH_REDIRECT_URL: url({\n desc: 'Public client callback URL Linear redirects to after OAuth authorization, such as https://shipfox.example.com/integrations/linear/callback. Required.',\n }),\n LINEAR_MCP_ENDPOINT: url({\n desc: 'Streamable HTTP endpoint used for Linear MCP tool calls. Set this only when routing Linear tools through a compatible proxy or test server.',\n default: 'https://mcp.linear.app/mcp',\n }),\n};\n\nexport const config = createConfig(linearConfigSchema);\n"],"names":["createConfig","str","url","linearConfigSchema","LINEAR_OAUTH_CLIENT_ID","desc","LINEAR_OAUTH_CLIENT_SECRET","LINEAR_WEBHOOK_SIGNING_SECRET","LINEAR_OAUTH_REDIRECT_URL","LINEAR_MCP_ENDPOINT","default","config"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAEvD,MAAMC,qBAAqB;IACzBC,wBAAwBH,IAAI;QAC1BI,MAAM;IACR;IACAC,4BAA4BL,IAAI;QAC9BI,MAAM;IACR;IACAE,+BAA+BN,IAAI;QACjCI,MAAM;IACR;IACAG,2BAA2BN,IAAI;QAC7BG,MAAM;IACR;IACAI,qBAAqBP,IAAI;QACvBG,MAAM;QACNK,SAAS;IACX;AACF;AAEA,OAAO,MAAMC,SAASX,aAAaG,oBAAoB"}
@@ -0,0 +1,32 @@
1
+ import { type CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { AgentToolCallInput, AgentToolSession, AgentToolsProvider, IntegrationConnection, OpenAgentToolsSessionInput } from '@shipfox/api-integration-core-dto';
3
+ import { type LinearAgentToolRequiredScope } from '#core/agent-tools.js';
4
+ import type { LinearTokenStore } from '#core/tokens.js';
5
+ type LinearIntegrationConnection = IntegrationConnection<'linear'>;
6
+ interface LinearMcpClient {
7
+ callTool(input: AgentToolCallInput, timeoutMs: number): Promise<CallToolResult>;
8
+ close(): Promise<void>;
9
+ }
10
+ interface CreateLinearMcpClientParams {
11
+ endpoint: URL;
12
+ accessToken: string;
13
+ }
14
+ type CreateLinearMcpClient = (params: CreateLinearMcpClientParams) => Promise<LinearMcpClient>;
15
+ export interface LinearAgentToolsProviderOptions {
16
+ tokenStore: Pick<LinearTokenStore, 'getAccessToken'>;
17
+ endpoint?: string | URL | undefined;
18
+ callTimeoutMs?: number | undefined;
19
+ createClient?: CreateLinearMcpClient | undefined;
20
+ }
21
+ export declare class LinearAgentToolsProvider implements AgentToolsProvider<LinearIntegrationConnection, LinearAgentToolRequiredScope> {
22
+ private readonly options;
23
+ private readonly endpoint;
24
+ private readonly callTimeoutMs;
25
+ private readonly createClient;
26
+ constructor(options: LinearAgentToolsProviderOptions);
27
+ catalog(): readonly [import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry, import("#core/agent-tools.js").LinearAgentToolCatalogEntry];
28
+ selectionCatalog(): import("@shipfox/api-integration-core-dto").AgentToolSelectionCatalog;
29
+ openSession(input: OpenAgentToolsSessionInput<LinearIntegrationConnection, LinearAgentToolRequiredScope>): Promise<AgentToolSession<CallToolResult>>;
30
+ }
31
+ export {};
32
+ //# 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":"AAGA,OAAO,EAAC,KAAK,cAAc,EAAuB,MAAM,oCAAoC,CAAC;AAC7F,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,EACrB,0BAA0B,EAC3B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,KAAK,4BAA4B,EAGlC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iBAAiB,CAAC;AAKtD,KAAK,2BAA2B,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;AAEnE,UAAU,eAAe;IACvB,QAAQ,CAAC,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAChF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,UAAU,2BAA2B;IACnC,QAAQ,EAAE,GAAG,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,KAAK,qBAAqB,GAAG,CAAC,MAAM,EAAE,2BAA2B,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AAE/F,MAAM,WAAW,+BAA+B;IAC9C,UAAU,EAAE,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;IACrD,QAAQ,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,YAAY,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;CAClD;AAED,qBAAa,wBACX,YAAW,kBAAkB,CAAC,2BAA2B,EAAE,4BAA4B,CAAC;IAM5E,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAM;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwB;gBAExB,OAAO,EAAE,+BAA+B;IAMrE,OAAO;IAIP,gBAAgB;IAIV,WAAW,CACf,KAAK,EAAE,0BAA0B,CAAC,2BAA2B,EAAE,4BAA4B,CAAC,GAC3F,OAAO,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;CAW7C"}
@@ -0,0 +1,63 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { linearAgentToolCatalog, linearAgentToolSelectionCatalog } from '#core/agent-tools.js';
5
+ const LINEAR_MCP_ENDPOINT = 'https://mcp.linear.app/mcp';
6
+ const LINEAR_MCP_CALL_TIMEOUT_MS = 30_000;
7
+ export class LinearAgentToolsProvider {
8
+ constructor(options){
9
+ this.options = options;
10
+ this.endpoint = new URL(options.endpoint ?? LINEAR_MCP_ENDPOINT);
11
+ this.callTimeoutMs = options.callTimeoutMs ?? LINEAR_MCP_CALL_TIMEOUT_MS;
12
+ this.createClient = options.createClient ?? createSdkLinearMcpClient;
13
+ }
14
+ catalog() {
15
+ return linearAgentToolCatalog;
16
+ }
17
+ selectionCatalog() {
18
+ return linearAgentToolSelectionCatalog;
19
+ }
20
+ async openSession(input) {
21
+ const accessToken = await this.options.tokenStore.getAccessToken({
22
+ connectionId: input.connection.id
23
+ });
24
+ const client = await this.createClient({
25
+ endpoint: this.endpoint,
26
+ accessToken
27
+ });
28
+ return {
29
+ call: (call)=>client.callTool(call, this.callTimeoutMs),
30
+ close: ()=>client.close()
31
+ };
32
+ }
33
+ }
34
+ async function createSdkLinearMcpClient(params) {
35
+ const client = new Client({
36
+ name: 'shipfox-linear-tools',
37
+ version: '0.0.0'
38
+ });
39
+ const transport = new StreamableHTTPClientTransport(params.endpoint, {
40
+ requestInit: {
41
+ headers: {
42
+ authorization: `Bearer ${params.accessToken}`
43
+ }
44
+ }
45
+ });
46
+ await client.connect(transport);
47
+ return {
48
+ callTool: async (input, timeoutMs)=>{
49
+ const result = await client.callTool({
50
+ name: input.toolId,
51
+ arguments: input.arguments
52
+ }, CallToolResultSchema, {
53
+ timeout: timeoutMs
54
+ });
55
+ return result;
56
+ },
57
+ close: async ()=>{
58
+ await client.close();
59
+ }
60
+ };
61
+ }
62
+
63
+ //# sourceMappingURL=agent-tools-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/agent-tools-provider.ts"],"sourcesContent":["import {Client} from '@modelcontextprotocol/sdk/client/index.js';\nimport {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type {Transport} from '@modelcontextprotocol/sdk/shared/transport.js';\nimport {type CallToolResult, CallToolResultSchema} from '@modelcontextprotocol/sdk/types.js';\nimport type {\n AgentToolCallInput,\n AgentToolSession,\n AgentToolsProvider,\n IntegrationConnection,\n OpenAgentToolsSessionInput,\n} from '@shipfox/api-integration-core-dto';\nimport {\n type LinearAgentToolRequiredScope,\n linearAgentToolCatalog,\n linearAgentToolSelectionCatalog,\n} from '#core/agent-tools.js';\nimport type {LinearTokenStore} from '#core/tokens.js';\n\nconst LINEAR_MCP_ENDPOINT = 'https://mcp.linear.app/mcp';\nconst LINEAR_MCP_CALL_TIMEOUT_MS = 30_000;\n\ntype LinearIntegrationConnection = IntegrationConnection<'linear'>;\n\ninterface LinearMcpClient {\n callTool(input: AgentToolCallInput, timeoutMs: number): Promise<CallToolResult>;\n close(): Promise<void>;\n}\n\ninterface CreateLinearMcpClientParams {\n endpoint: URL;\n accessToken: string;\n}\n\ntype CreateLinearMcpClient = (params: CreateLinearMcpClientParams) => Promise<LinearMcpClient>;\n\nexport interface LinearAgentToolsProviderOptions {\n tokenStore: Pick<LinearTokenStore, 'getAccessToken'>;\n endpoint?: string | URL | undefined;\n callTimeoutMs?: number | undefined;\n createClient?: CreateLinearMcpClient | undefined;\n}\n\nexport class LinearAgentToolsProvider\n implements AgentToolsProvider<LinearIntegrationConnection, LinearAgentToolRequiredScope>\n{\n private readonly endpoint: URL;\n private readonly callTimeoutMs: number;\n private readonly createClient: CreateLinearMcpClient;\n\n constructor(private readonly options: LinearAgentToolsProviderOptions) {\n this.endpoint = new URL(options.endpoint ?? LINEAR_MCP_ENDPOINT);\n this.callTimeoutMs = options.callTimeoutMs ?? LINEAR_MCP_CALL_TIMEOUT_MS;\n this.createClient = options.createClient ?? createSdkLinearMcpClient;\n }\n\n catalog() {\n return linearAgentToolCatalog;\n }\n\n selectionCatalog() {\n return linearAgentToolSelectionCatalog;\n }\n\n async openSession(\n input: OpenAgentToolsSessionInput<LinearIntegrationConnection, LinearAgentToolRequiredScope>,\n ): Promise<AgentToolSession<CallToolResult>> {\n const accessToken = await this.options.tokenStore.getAccessToken({\n connectionId: input.connection.id,\n });\n const client = await this.createClient({endpoint: this.endpoint, accessToken});\n\n return {\n call: (call) => client.callTool(call, this.callTimeoutMs),\n close: () => client.close(),\n };\n }\n}\n\nasync function createSdkLinearMcpClient(\n params: CreateLinearMcpClientParams,\n): Promise<LinearMcpClient> {\n const client = new Client({name: 'shipfox-linear-tools', version: '0.0.0'});\n const transport = new StreamableHTTPClientTransport(params.endpoint, {\n requestInit: {\n headers: {authorization: `Bearer ${params.accessToken}`},\n },\n });\n\n await client.connect(transport as unknown as Transport);\n\n return {\n callTool: async (input, timeoutMs) => {\n const result = await client.callTool(\n {name: input.toolId, arguments: input.arguments},\n CallToolResultSchema,\n {\n timeout: timeoutMs,\n },\n );\n return result as CallToolResult;\n },\n close: async () => {\n await client.close();\n },\n };\n}\n"],"names":["Client","StreamableHTTPClientTransport","CallToolResultSchema","linearAgentToolCatalog","linearAgentToolSelectionCatalog","LINEAR_MCP_ENDPOINT","LINEAR_MCP_CALL_TIMEOUT_MS","LinearAgentToolsProvider","options","endpoint","URL","callTimeoutMs","createClient","createSdkLinearMcpClient","catalog","selectionCatalog","openSession","input","accessToken","tokenStore","getAccessToken","connectionId","connection","id","client","call","callTool","close","params","name","version","transport","requestInit","headers","authorization","connect","timeoutMs","result","toolId","arguments","timeout"],"mappings":"AAAA,SAAQA,MAAM,QAAO,4CAA4C;AACjE,SAAQC,6BAA6B,QAAO,qDAAqD;AAEjG,SAA6BC,oBAAoB,QAAO,qCAAqC;AAQ7F,SAEEC,sBAAsB,EACtBC,+BAA+B,QAC1B,uBAAuB;AAG9B,MAAMC,sBAAsB;AAC5B,MAAMC,6BAA6B;AAuBnC,OAAO,MAAMC;IAOX,YAAY,AAAiBC,OAAwC,CAAE;aAA1CA,UAAAA;QAC3B,IAAI,CAACC,QAAQ,GAAG,IAAIC,IAAIF,QAAQC,QAAQ,IAAIJ;QAC5C,IAAI,CAACM,aAAa,GAAGH,QAAQG,aAAa,IAAIL;QAC9C,IAAI,CAACM,YAAY,GAAGJ,QAAQI,YAAY,IAAIC;IAC9C;IAEAC,UAAU;QACR,OAAOX;IACT;IAEAY,mBAAmB;QACjB,OAAOX;IACT;IAEA,MAAMY,YACJC,KAA4F,EACjD;QAC3C,MAAMC,cAAc,MAAM,IAAI,CAACV,OAAO,CAACW,UAAU,CAACC,cAAc,CAAC;YAC/DC,cAAcJ,MAAMK,UAAU,CAACC,EAAE;QACnC;QACA,MAAMC,SAAS,MAAM,IAAI,CAACZ,YAAY,CAAC;YAACH,UAAU,IAAI,CAACA,QAAQ;YAAES;QAAW;QAE5E,OAAO;YACLO,MAAM,CAACA,OAASD,OAAOE,QAAQ,CAACD,MAAM,IAAI,CAACd,aAAa;YACxDgB,OAAO,IAAMH,OAAOG,KAAK;QAC3B;IACF;AACF;AAEA,eAAed,yBACbe,MAAmC;IAEnC,MAAMJ,SAAS,IAAIxB,OAAO;QAAC6B,MAAM;QAAwBC,SAAS;IAAO;IACzE,MAAMC,YAAY,IAAI9B,8BAA8B2B,OAAOnB,QAAQ,EAAE;QACnEuB,aAAa;YACXC,SAAS;gBAACC,eAAe,CAAC,OAAO,EAAEN,OAAOV,WAAW,EAAE;YAAA;QACzD;IACF;IAEA,MAAMM,OAAOW,OAAO,CAACJ;IAErB,OAAO;QACLL,UAAU,OAAOT,OAAOmB;YACtB,MAAMC,SAAS,MAAMb,OAAOE,QAAQ,CAClC;gBAACG,MAAMZ,MAAMqB,MAAM;gBAAEC,WAAWtB,MAAMsB,SAAS;YAAA,GAC/CrC,sBACA;gBACEsC,SAASJ;YACX;YAEF,OAAOC;QACT;QACAV,OAAO;YACL,MAAMH,OAAOG,KAAK;QACpB;IACF;AACF"}